Przeglądaj źródła

✨ feat(server): 创建新的服务器实现

- 初始化基于 Hono 的服务器架构,支持生产和开发环境
- 实现基础路径解析和服务器配置
- 添加生产环境压缩和静态文件中间件
- 开发环境集成 Vite 开发服务器
- 实现 SSR 渲染中间件和流式响应处理
- 添加请求超时处理和优雅关闭机制
- 统一处理 API 请求和 SSR 渲染逻辑
- 支持环境变量配置和启动日志输出
yourname 7 miesięcy temu
rodzic
commit
de32067f9f
1 zmienionych plików z 345 dodań i 0 usunięć
  1. 345 0
      server2.js

+ 345 - 0
server2.js

@@ -0,0 +1,345 @@
+import 'dotenv/config'
+import fs from 'node:fs/promises';
+import { URL } from 'node:url';
+import { Transform } from 'node:stream';
+import { Readable } from 'node:stream';
+import { Hono } from 'hono';
+import { logger } from 'hono/logger';
+import { createServer as createNodeServer } from 'node:http';
+import process from 'node:process';
+import { createAdaptorServer } from '@hono/node-server'  
+
+// 创建 Hono 应用
+const app = new Hono();
+
+// 全局使用 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',
+          ws: true,
+        },
+      },
+    },
+    appType: 'custom',
+    base,
+  });
+  console.log('Vite 开发服务器初始化完成');
+}
+
+
+// 开发环境模板处理函数 - 仅处理模板转换
+const processDevTemplate = async (template) => {
+  if (!isProduction && vite) {
+    console.log('开发环境: 处理模板...');
+    const processedTemplate = await vite.transformIndexHtml('/', template);
+    console.log('开发环境模板处理完成');
+    return processedTemplate;
+  }
+  return template;
+};
+
+// 生产环境模板处理函数 - 仅处理资源路径替换
+const processProdTemplate = async (template) => {
+  console.log('生产环境: 处理模板资源路径...');
+  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');
+
+    // 获取 src/client/index.tsx 对应的资源信息
+    const indexManifest = manifest['src/client/index.tsx'];
+    if (!indexManifest) {
+      throw new Error('manifest 中未找到 src/client/index.tsx 入口配置');
+    }
+
+    // 获取 src/style.css 对应的资源信息
+    const styleManifest = manifest['src/style.css'];
+    if (!styleManifest) {
+      throw new Error('manifest 中未找到 src/style.css 入口配置');
+    }
+
+    const cssPath = new URL(styleManifest.file, baseUrl).pathname;
+    const cssLinks = `<link href="${cssPath}" rel="stylesheet" />`;
+
+    // 替换入口脚本
+    const jsEntryPath = new URL(indexManifest.file, baseUrl).pathname;
+    const entryScript = `<script type="module" src="${jsEntryPath}"></script>`;
+
+    // 执行替换
+    const processedTemplate = template
+      .replace(/<link href="\/src\/style.css" rel="stylesheet"\/>/, cssLinks)
+      .replace(/<script type="module" src="\/src\/client\/index.tsx"><\/script>/, entryScript);
+
+    console.log('生产环境模板处理完成');
+    return processedTemplate;
+
+  } catch (err) {
+    console.error('生产环境模板处理失败:', err);
+    throw err;
+  }
+};
+
+// SSR 渲染中间件函数
+const createSsrHandler = (template, render, normalizedUrl) => {
+  return async (c) => {
+    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);
+      }
+    });
+  };
+};
+
+// 统一的请求处理中间件 - 合并 API 和 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}`);
+
+    // 开发环境:使用 Vite 中间件
+    if (!isProduction && vite) {
+      // 使用 Vite 中间件处理请求
+      const handled = await new Promise((resolve) => {
+        vite.middlewares(req, res, () => resolve(false));
+      });
+      
+      // 如果 Vite 中间件已经处理了请求,直接返回
+      if (handled) {
+        return c.body;
+      }
+
+
+      // 动态加载最新 API 模块
+      const apiModule = await vite.ssrLoadModule('./src/server/index.tsx');
+      
+      // 创建临时子应用并挂载路由
+      const apiApp = new Hono();
+      apiApp.route('/', apiModule.api);
+      
+      // 处理开发环境模板
+      const template = await processDevTemplate(apiModule.template);
+      apiApp.get('*', createSsrHandler(template, apiModule.render, normalizedUrl));
+
+      // 直接由子应用处理 API 请求
+      return apiApp.fetch(c.req.raw, {
+        ...c.env,
+        // 传递原始请求对象
+        incoming: c.env.incoming,
+        outgoing: c.env.outgoing
+      });
+    }
+    // 生产环境:使用 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;
+      }
+
+      // 生产环境:使用缓存的 API 路由
+      const { api, template: rawTemplate, render } = (await import('./dist/server/index.js'));
+      const apiApp = new Hono();
+      apiApp.route('/', api);
+
+      // 处理生产环境模板
+      const template = await processProdTemplate(rawTemplate);
+      apiApp.get('*', createSsrHandler(template, render, normalizedUrl));
+
+      return apiApp.fetch(c.req.raw, {
+        ...c.env,
+        incoming: c.env.incoming,
+        outgoing: c.env.outgoing
+      });
+    }
+
+  } catch (e) {
+    if (!isProduction && vite) {
+      vite.ssrFixStacktrace(e);
+      console.error('请求处理错误:', e.stack);
+      return c.text(e.stack, 500);
+    }
+    console.error('请求处理错误:', e.stack);
+    return c.text('服务器内部错误', 500);
+  }
+});
+
+// 移除生产环境的 API 路由预加载(已在统一中间件中处理)
+// 移除动态 API 路由中间件(已合并到统一中间件中)
+
+// 启动服务器
+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();
+      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);
+  });
+};
+
+// 处理进程终止信号
+process.on('SIGINT', shutdownServer);
+process.on('SIGTERM', shutdownServer);