|
|
@@ -0,0 +1,345 @@
|
|
|
+import fs from 'node:fs/promises';
|
|
|
+import { URL } from 'node:url';
|
|
|
+import { Transform } from 'node:stream';
|
|
|
+import { Readable } from 'node:stream';
|
|
|
+import { pipeline } from 'node:stream/promises';
|
|
|
+import { Hono } from 'hono';
|
|
|
+import { logger } from 'hono/logger'; // 引入 Hono 日志中间件
|
|
|
+import { createServer as createNodeServer } from 'node:http';
|
|
|
+import process from 'node:process';
|
|
|
+import { createAdaptorServer } from '@hono/node-server'
|
|
|
+
|
|
|
+
|
|
|
+// 创建 Hono 应用
|
|
|
+const app = new Hono();// API路由
|
|
|
+
|
|
|
+// 全局使用 Hono 日志中间件(记录所有请求)
|
|
|
+app.use('*', logger()); // 新增:添加请求日志中间件
|
|
|
+
|
|
|
+// 常量定义
|
|
|
+const isProduction = process.env.NODE_ENV === 'production';
|
|
|
+const port = process.env.PORT || 8080;
|
|
|
+const base = process.env.BASE || '/';
|
|
|
+const ABORT_DELAY = 10000;
|
|
|
+
|
|
|
+console.log('========================================');
|
|
|
+console.log('开始初始化服务器...');
|
|
|
+console.log(`环境: ${isProduction ? '生产环境' : '开发环境'}`);
|
|
|
+console.log(`端口: ${port}`);
|
|
|
+console.log(`基础路径: ${base}`);
|
|
|
+console.log('========================================');
|
|
|
+
|
|
|
+// 解析基础路径为 URL 对象,方便后续处理
|
|
|
+const baseUrl = new URL(base, `http://localhost:${port}`);
|
|
|
+console.log(`基础URL解析完成: ${baseUrl.href}`);
|
|
|
+
|
|
|
+// 先创建服务器实例
|
|
|
+console.log('正在创建服务器实例...');
|
|
|
+const parentServer = createAdaptorServer({
|
|
|
+ fetch: app.fetch,
|
|
|
+ createServer: createNodeServer,
|
|
|
+ port: port,
|
|
|
+})
|
|
|
+console.log('服务器实例创建成功');
|
|
|
+
|
|
|
+// 生产环境中间件
|
|
|
+let compressionMiddleware;
|
|
|
+let sirvMiddleware;
|
|
|
+if (isProduction) {
|
|
|
+ console.log('生产环境: 加载压缩和静态文件中间件...');
|
|
|
+ compressionMiddleware = (await import('compression')).default();
|
|
|
+ sirvMiddleware = (await import('sirv')).default('./dist/client', {
|
|
|
+ extensions: [],
|
|
|
+ baseUrl: base
|
|
|
+ });
|
|
|
+ console.log('生产环境中间件加载完成');
|
|
|
+}
|
|
|
+
|
|
|
+// Vite 开发服务器
|
|
|
+/** @type {import('vite').ViteDevServer | undefined} */
|
|
|
+let vite;
|
|
|
+if (!isProduction) {
|
|
|
+ console.log('开发环境: 初始化 Vite 开发服务器...');
|
|
|
+ const { createServer } = await import('vite');
|
|
|
+ vite = await createServer({
|
|
|
+ server: { middlewareMode: {
|
|
|
+ server: parentServer
|
|
|
+ },
|
|
|
+ hmr: {
|
|
|
+ port: 8081,
|
|
|
+ clientPort: 443, // 或其他可用端口
|
|
|
+ path: 'vite-hmr'
|
|
|
+ },
|
|
|
+ proxy: {
|
|
|
+ '/vite-hmr': {
|
|
|
+ target: 'ws://localhost:8081',
|
|
|
+ // 代理 WebSocket
|
|
|
+ ws: true,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ appType: 'custom',
|
|
|
+ base,
|
|
|
+ });
|
|
|
+ console.log('Vite 开发服务器初始化完成');
|
|
|
+}
|
|
|
+
|
|
|
+// 加载 API 路由
|
|
|
+if (!isProduction) {
|
|
|
+ console.log('开发环境: 从 Vite 加载 API 路由...');
|
|
|
+ const apiModule = await vite.ssrLoadModule('./src/server/api.ts');
|
|
|
+ app.route('/', apiModule.default);
|
|
|
+ console.log('API 路由加载完成');
|
|
|
+}else{
|
|
|
+ console.log('生产环境: 加载编译后的 API 路由...');
|
|
|
+ const api = (await import('./dist/api/api.js')).default
|
|
|
+ app.route('/', api);
|
|
|
+ console.log('API 路由加载完成');
|
|
|
+}
|
|
|
+
|
|
|
+// 请求处理中间件 - 通用逻辑
|
|
|
+app.use(async (c, next) => {
|
|
|
+ try {
|
|
|
+ // 使用 c.env 获取原生请求响应对象
|
|
|
+ const req = c.env.incoming;
|
|
|
+ const res = c.env.outgoing;
|
|
|
+ const url = new URL(req.url, `http://${req.headers.host}`);
|
|
|
+ const path = url.pathname;
|
|
|
+
|
|
|
+ // 检查是否匹配基础路径
|
|
|
+ if (!path.startsWith(baseUrl.pathname)) {
|
|
|
+ return c.text('未找到', 404);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 开发环境:使用 Vite 中间件
|
|
|
+ if (!isProduction && vite) {
|
|
|
+ // 使用 Vite 中间件处理请求
|
|
|
+ const handled = await new Promise((resolve) => {
|
|
|
+ vite.middlewares(req, res, () => resolve(false));
|
|
|
+ });
|
|
|
+
|
|
|
+ // 如果 Vite 中间件已经处理了请求,直接返回
|
|
|
+ if (handled) {
|
|
|
+ return c.body;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 生产环境:使用 compression 和 sirv 中间件
|
|
|
+ else if (isProduction) {
|
|
|
+ // 先尝试 compression 中间件
|
|
|
+ const compressed = await new Promise((resolve) => {
|
|
|
+ compressionMiddleware(req, res, () => resolve(false));
|
|
|
+ });
|
|
|
+
|
|
|
+ if (compressed) {
|
|
|
+ return c.body;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 再尝试 sirv 中间件处理静态文件
|
|
|
+ const served = await new Promise((resolve) => {
|
|
|
+ sirvMiddleware(req, res, () => resolve(false));
|
|
|
+ });
|
|
|
+
|
|
|
+ if (served) {
|
|
|
+ return c.body;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ await next()
|
|
|
+ } catch (e) {
|
|
|
+ if (!isProduction && vite) {
|
|
|
+ vite.ssrFixStacktrace(e);
|
|
|
+ }
|
|
|
+ console.error('请求处理中间件错误:', e.stack);
|
|
|
+ return c.text('服务器内部错误', 500);
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// 请求处理中间件 - SSR 渲染逻辑
|
|
|
+app.use(async (c) => {
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 使用 c.env 获取原生请求响应对象
|
|
|
+ const req = c.env.incoming;
|
|
|
+ const res = c.env.outgoing;
|
|
|
+ const url = new URL(req.url, `http://${req.headers.host}`);
|
|
|
+ const path = url.pathname;
|
|
|
+
|
|
|
+ // 检查是否匹配基础路径
|
|
|
+ if (!path.startsWith(baseUrl.pathname)) {
|
|
|
+ return c.text('未找到', 404);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理基础路径
|
|
|
+ const normalizedUrl = path.replace(baseUrl.pathname, '/') || '/';
|
|
|
+ console.log(`处理请求: ${normalizedUrl}`);
|
|
|
+
|
|
|
+ // 处理所有其他请求的 SSR 逻辑
|
|
|
+ /** @type {string} */
|
|
|
+ let template;
|
|
|
+ /** @type {import('./src/server/index.tsx').render} */
|
|
|
+ let render;
|
|
|
+
|
|
|
+ if (!isProduction && vite) {
|
|
|
+ console.log('开发环境: 加载模板和渲染函数...');
|
|
|
+ // 开发环境:读取最新模板并转换
|
|
|
+ const module = (await vite.ssrLoadModule('/src/server/index.tsx'));
|
|
|
+ template = module.template;
|
|
|
+ template = await vite.transformIndexHtml(normalizedUrl, template);
|
|
|
+ render = module.render;
|
|
|
+ console.log('开发环境模板处理完成');
|
|
|
+ } else {
|
|
|
+ // 生产环境:使用缓存的模板
|
|
|
+ console.log('生产环境: 加载编译后的模板和渲染函数...');
|
|
|
+ const module = await import('./dist/server/index.js');
|
|
|
+
|
|
|
+ // 读取 manifest.json 并处理模板
|
|
|
+ try {
|
|
|
+ // 读取 manifest
|
|
|
+ const manifestPath = new URL('./dist/client/.vite/manifest.json', import.meta.url);
|
|
|
+ const manifestContent = await fs.readFile(manifestPath, 'utf-8');
|
|
|
+ const manifest = JSON.parse(manifestContent);
|
|
|
+ console.log('生产环境: 成功读取 manifest.json');
|
|
|
+
|
|
|
+ // 获取 index.html 对应的资源信息
|
|
|
+ const indexManifest = manifest['index.html'];
|
|
|
+ if (!indexManifest) {
|
|
|
+ throw new Error('manifest 中未找到 index.html 入口配置');
|
|
|
+ }
|
|
|
+
|
|
|
+ template = module.template;
|
|
|
+
|
|
|
+ // 替换 CSS 链接
|
|
|
+ const cssLinks = indexManifest.css?.map(cssFile => {
|
|
|
+ // 结合基础路径生成完整 URL(处理 base 前缀)
|
|
|
+ const cssUrl = new URL(cssFile, baseUrl).pathname;
|
|
|
+ return `<link href="${cssUrl}" rel="stylesheet" />`;
|
|
|
+ }).join('\n') || ''; // 无 CSS 则清空
|
|
|
+
|
|
|
+ // 替换入口脚本
|
|
|
+ const jsEntryPath = new URL(indexManifest.file, baseUrl).pathname;
|
|
|
+ const entryScript = `<script type="module" src="${jsEntryPath}"></script>`;
|
|
|
+
|
|
|
+ // 执行替换
|
|
|
+ template = template
|
|
|
+ .replace(/<link href="\/src\/style.css" rel="stylesheet"\/>/, cssLinks)
|
|
|
+ .replace(/<script type="module" src="\/src\/client\/index.tsx"><\/script>/, entryScript);
|
|
|
+
|
|
|
+ console.log('生产环境模板处理完成');
|
|
|
+
|
|
|
+ } catch (err) {
|
|
|
+ console.error('生产环境模板处理失败:', err);
|
|
|
+ throw err; // 终止启动,避免使用错误模板
|
|
|
+ }
|
|
|
+
|
|
|
+ render = module.render;
|
|
|
+ }
|
|
|
+
|
|
|
+ let didError = false;
|
|
|
+ let abortController;
|
|
|
+
|
|
|
+ // 创建一个可读流用于 SSR 渲染内容
|
|
|
+ const [htmlStart, htmlEnd] = template.split(`<!--app-html-->`);
|
|
|
+ const ssrStream = new Readable({ read: () => {} });
|
|
|
+
|
|
|
+ // 写入 HTML 头部
|
|
|
+ ssrStream.push(htmlStart);
|
|
|
+
|
|
|
+ // 设置响应头和状态码
|
|
|
+ c.header('Content-Type', 'text/html');
|
|
|
+
|
|
|
+ // 处理渲染
|
|
|
+ const { pipe, abort } = render(normalizedUrl, {
|
|
|
+ onShellError() {
|
|
|
+ didError = true;
|
|
|
+ c.status(500);
|
|
|
+ ssrStream.push('<h1>服务器渲染出错</h1>');
|
|
|
+ ssrStream.push(null); // 结束流
|
|
|
+ },
|
|
|
+ onShellReady() {
|
|
|
+ console.log(`开始渲染页面: ${normalizedUrl}`);
|
|
|
+ // 将渲染结果通过管道传入 ssrStream
|
|
|
+ const transformStream = new Transform({
|
|
|
+ transform(chunk, encoding, callback) {
|
|
|
+ ssrStream.push(chunk, encoding);
|
|
|
+ callback();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ pipe(transformStream);
|
|
|
+
|
|
|
+ // 当 transformStream 完成时,添加 HTML 尾部
|
|
|
+ transformStream.on('finish', () => {
|
|
|
+ ssrStream.push(htmlEnd);
|
|
|
+ ssrStream.push(null); // 结束流
|
|
|
+ console.log(`页面渲染完成: ${normalizedUrl}`);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ onError(error) {
|
|
|
+ didError = true;
|
|
|
+ console.error('渲染过程出错:', error);
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ // 设置超时中止
|
|
|
+ abortController = new AbortController();
|
|
|
+ const abortTimeout = setTimeout(() => {
|
|
|
+ console.log(`渲染超时,终止请求: ${normalizedUrl}`);
|
|
|
+ abort();
|
|
|
+ abortController.abort();
|
|
|
+ }, ABORT_DELAY);
|
|
|
+
|
|
|
+ // 将流通过 Hono 响应返回
|
|
|
+ return c.body(ssrStream, {
|
|
|
+ onEnd: () => {
|
|
|
+ clearTimeout(abortTimeout);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } catch (e) {
|
|
|
+ if (!isProduction && vite) {
|
|
|
+ vite.ssrFixStacktrace(e);
|
|
|
+ }
|
|
|
+ console.error('SSR 处理错误:', e.stack);
|
|
|
+ return c.text('服务器内部错误', 500);
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+
|
|
|
+// 启动服务器
|
|
|
+console.log('准备启动服务器...');
|
|
|
+parentServer.listen(port, () => {
|
|
|
+ console.log('========================================');
|
|
|
+ console.log(`服务器已成功启动!`);
|
|
|
+ console.log(`访问地址: http://localhost:${port}`);
|
|
|
+ console.log(`环境: ${isProduction ? '生产环境' : '开发环境'}`);
|
|
|
+ console.log('========================================');
|
|
|
+})
|
|
|
+
|
|
|
+// 统一的服务器关闭处理函数
|
|
|
+const shutdownServer = async () => {
|
|
|
+ console.log('正在关闭服务器...');
|
|
|
+
|
|
|
+ // 1. 先关闭 Vite 开发服务器(包括 HMR 服务)
|
|
|
+ if (!isProduction && vite) {
|
|
|
+ console.log('正在关闭 Vite 开发服务器(包括 HMR 服务)...');
|
|
|
+ try {
|
|
|
+ await vite.close(); // 关闭 Vite 实例,会自动终止 HMR 服务(8081 端口)
|
|
|
+ console.log('Vite 开发服务器已关闭');
|
|
|
+ } catch (err) {
|
|
|
+ console.error('关闭 Vite 服务器时出错:', err);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 关闭主服务器
|
|
|
+ parentServer.close((err) => {
|
|
|
+ if (err) {
|
|
|
+ console.error('关闭主服务器时出错:', err);
|
|
|
+ process.exit(1);
|
|
|
+ }
|
|
|
+ console.log('主服务器已关闭');
|
|
|
+ process.exit(0);
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 处理进程终止信号(包括 Ctrl+C)
|
|
|
+process.on('SIGINT', shutdownServer); // 处理 Ctrl+C
|
|
|
+process.on('SIGTERM', shutdownServer); // 处理 kill 命令
|