feat: 搭建前后端可执行程序示例
This commit is contained in:
111
src/server/app.ts
Normal file
111
src/server/app.ts
Normal 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
39
src/server/config.ts
Normal 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
7
src/server/dev.ts
Normal 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
25
src/server/server.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user