1
0

feat: 搭建前后端可执行程序示例

This commit is contained in:
2026-05-09 12:25:39 +08:00
commit 5b412c624d
27 changed files with 1860 additions and 0 deletions

111
src/server/app.ts Normal file
View File

@@ -0,0 +1,111 @@
import type { ApiErrorResponse, DemoResponse, HealthResponse, RuntimeMode } from "../shared/api";
export interface StaticAssets {
indexHtml: Blob;
files: Record<string, Blob>;
}
export interface AppOptions {
mode: RuntimeMode;
staticAssets?: StaticAssets;
}
export function createFetchHandler(options: AppOptions) {
return (request: Request): Response => {
const url = new URL(request.url);
if (url.pathname === "/health") {
return Response.json(createHealthResponse());
}
if (url.pathname === "/api/demo") {
return Response.json(createDemoResponse(options.mode));
}
if (url.pathname.startsWith("/api/")) {
return Response.json(createApiError("API route not found", 404), { status: 404 });
}
if (options.staticAssets) {
return serveStaticAsset(url.pathname, options.staticAssets);
}
return new Response("开发期请通过 Vite 前端地址访问页面。", {
status: 404,
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
};
}
function createDemoResponse(mode: RuntimeMode): DemoResponse {
return {
message: "Bun 后端已通过 /api/demo 连接到 React 前端。",
runtime: {
mode,
bunVersion: Bun.version,
platform: process.platform,
arch: process.arch,
timestamp: new Date().toISOString(),
},
};
}
function createHealthResponse(): HealthResponse {
return {
ok: true,
service: "gateway-checker",
timestamp: new Date().toISOString(),
};
}
function createApiError(error: string, status: number): ApiErrorResponse {
return { error, status };
}
function serveStaticAsset(pathname: string, staticAssets: StaticAssets): Response {
if (pathname === "/") {
return htmlResponse(staticAssets.indexHtml);
}
const asset = staticAssets.files[pathname];
if (asset) {
return new Response(asset, {
headers: {
"Content-Type": contentTypeFor(pathname),
"Cache-Control": "public, max-age=31536000, immutable",
},
});
}
if (pathname.startsWith("/assets/") || hasFileExtension(pathname)) {
return new Response("Not Found", { status: 404 });
}
return htmlResponse(staticAssets.indexHtml);
}
function htmlResponse(indexHtml: Blob): Response {
return new Response(indexHtml, {
headers: {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-cache",
},
});
}
function hasFileExtension(pathname: string): boolean {
return /\/[^/]+\.[^/]+$/.test(pathname);
}
function contentTypeFor(pathname: string): string {
if (pathname.endsWith(".js") || pathname.endsWith(".mjs")) return "text/javascript; charset=utf-8";
if (pathname.endsWith(".css")) return "text/css; charset=utf-8";
if (pathname.endsWith(".svg")) return "image/svg+xml";
if (pathname.endsWith(".json")) return "application/json; charset=utf-8";
if (pathname.endsWith(".png")) return "image/png";
if (pathname.endsWith(".jpg") || pathname.endsWith(".jpeg")) return "image/jpeg";
if (pathname.endsWith(".ico")) return "image/x-icon";
return "application/octet-stream";
}

39
src/server/config.ts Normal file
View File

@@ -0,0 +1,39 @@
export interface RuntimeConfig {
host: string;
port: number;
}
const DEFAULT_HOST = "127.0.0.1";
const DEFAULT_PORT = 3000;
export function readRuntimeConfig(
argv: string[] = process.argv.slice(2),
env: Record<string, string | undefined> = Bun.env,
): RuntimeConfig {
const host = readOption(argv, "host") ?? env.HOST ?? DEFAULT_HOST;
const portValue = readOption(argv, "port") ?? env.PORT ?? String(DEFAULT_PORT);
const port = Number(portValue);
if (!Number.isInteger(port) || port < 0 || port > 65535) {
throw new Error(`无效端口: ${portValue}`);
}
return { host, port };
}
function readOption(argv: string[], name: string): string | undefined {
const prefix = `--${name}=`;
const inline = argv.find((value) => value.startsWith(prefix));
if (inline) {
return inline.slice(prefix.length);
}
const index = argv.indexOf(`--${name}`);
if (index >= 0) {
return argv[index + 1];
}
return undefined;
}

7
src/server/dev.ts Normal file
View File

@@ -0,0 +1,7 @@
import { readRuntimeConfig } from "./config";
import { startServer } from "./server";
startServer({
config: readRuntimeConfig(),
mode: "development",
});

25
src/server/server.ts Normal file
View File

@@ -0,0 +1,25 @@
import type { RuntimeMode } from "../shared/api";
import { createFetchHandler, type StaticAssets } from "./app";
import { readRuntimeConfig, type RuntimeConfig } from "./config";
export interface StartServerOptions {
config?: RuntimeConfig;
mode: RuntimeMode;
staticAssets?: StaticAssets;
}
export function startServer(options: StartServerOptions) {
const config = options.config ?? readRuntimeConfig();
const server = Bun.serve({
hostname: config.host,
port: config.port,
fetch: createFetchHandler({
mode: options.mode,
staticAssets: options.staticAssets,
}),
});
console.log(`Gateway Checker listening on ${server.url}`);
return server;
}