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";
}