Explorar el Código

✨ feat(server): 集成Hono React渲染器并重构SSR逻辑

- 添加@hono/react-renderer依赖包以支持React服务端渲染
- 创建新的渲染器组件文件src/server/renderer.tsx
- 重构server.js中的请求处理中间件,分离为两个处理阶段
- 修改src/server/index.tsx,添加template导出用于生成静态HTML
- 移除客户端入口文件中未使用的样式导入

📝 docs: 更新SSR模板结构和全局配置注入方式

- 实现Rooter组件作为HTML根模板
- 添加全局配置常量GLOBAL_CONFIG并注入到客户端
- 优化vConsole初始化逻辑,仅在开发环境或指定参数时加载
- 添加样式表链接到HTML头部

🔧 chore: 调整项目依赖和文件结构

- 更新pnpm-lock.yaml以反映新添加的依赖
- 优化服务器中间件调用顺序,添加next()调用支持中间件链
- 修复模板渲染路径处理逻辑
yourname hace 7 meses
padre
commit
ae5b1b68fe
Se han modificado 6 ficheros con 214 adiciones y 8 borrados
  1. 1 0
      package.json
  2. 17 0
      pnpm-lock.yaml
  3. 110 6
      server.js
  4. 0 1
      src/client/index.tsx
  5. 11 1
      src/server/index.tsx
  6. 75 0
      src/server/renderer.tsx

+ 1 - 0
package.json

@@ -14,6 +14,7 @@
     "@ant-design/icons": "^6.0.0",
     "@ant-design/icons": "^6.0.0",
     "@heroicons/react": "^2.2.0",
     "@heroicons/react": "^2.2.0",
     "@hono/node-server": "^1.17.1",
     "@hono/node-server": "^1.17.1",
+    "@hono/react-renderer": "^1.0.1",
     "@hono/zod-openapi": "^1.0.2",
     "@hono/zod-openapi": "^1.0.2",
     "@tanstack/react-query": "^5.83.0",
     "@tanstack/react-query": "^5.83.0",
     "antd": "^5.26.6",
     "antd": "^5.26.6",

+ 17 - 0
pnpm-lock.yaml

@@ -17,6 +17,9 @@ importers:
       '@hono/node-server':
       '@hono/node-server':
         specifier: ^1.17.1
         specifier: ^1.17.1
         version: 1.17.1(hono@4.8.5)
         version: 1.17.1(hono@4.8.5)
+      '@hono/react-renderer':
+        specifier: ^1.0.1
+        version: 1.0.1(hono@4.8.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
       '@hono/zod-openapi':
       '@hono/zod-openapi':
         specifier: ^1.0.2
         specifier: ^1.0.2
         version: 1.0.2(hono@4.8.5)(zod@4.0.8)
         version: 1.0.2(hono@4.8.5)(zod@4.0.8)
@@ -330,6 +333,14 @@ packages:
     peerDependencies:
     peerDependencies:
       hono: ^4
       hono: ^4
 
 
+  '@hono/react-renderer@1.0.1':
+    resolution: {integrity: sha512-vjQ/70hVrbgsi2O44N7w5sO0v51lRcuXau/4caVzw0A1hje+U2zAnuhiBC3JhX56gGfhGT4kO5B0di4SROx0Lg==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      hono: '*'
+      react: ^19.0.0
+      react-dom: ^19.0.0
+
   '@hono/zod-openapi@1.0.2':
   '@hono/zod-openapi@1.0.2':
     resolution: {integrity: sha512-zbxUZEnA+NaeotXRI2YPL5GEbz38DiO7Zp1nx/7yXOA2ITkcASYsYe/My/I38c44GCu+oUVM899zn4TBVn7JRg==}
     resolution: {integrity: sha512-zbxUZEnA+NaeotXRI2YPL5GEbz38DiO7Zp1nx/7yXOA2ITkcASYsYe/My/I38c44GCu+oUVM899zn4TBVn7JRg==}
     engines: {node: '>=16.0.0'}
     engines: {node: '>=16.0.0'}
@@ -1870,6 +1881,12 @@ snapshots:
     dependencies:
     dependencies:
       hono: 4.8.5
       hono: 4.8.5
 
 
+  '@hono/react-renderer@1.0.1(hono@4.8.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+    dependencies:
+      hono: 4.8.5
+      react: 19.1.0
+      react-dom: 19.1.0(react@19.1.0)
+
   '@hono/zod-openapi@1.0.2(hono@4.8.5)(zod@4.0.8)':
   '@hono/zod-openapi@1.0.2(hono@4.8.5)(zod@4.0.8)':
     dependencies:
     dependencies:
       '@asteasolutions/zod-to-openapi': 8.0.0(zod@4.0.8)
       '@asteasolutions/zod-to-openapi': 8.0.0(zod@4.0.8)

+ 110 - 6
server.js

@@ -82,7 +82,7 @@ if (!isProduction) {
 }
 }
 
 
 // 将请求处理逻辑适配为 Hono 中间件
 // 将请求处理逻辑适配为 Hono 中间件
-app.use(async (c) => {
+app.use(async (c, next) => {
   try {
   try {
     // 使用 c.env 获取原生请求响应对象(关键修复)
     // 使用 c.env 获取原生请求响应对象(关键修复)
     const req = c.env.incoming;
     const req = c.env.incoming;
@@ -131,6 +131,108 @@ app.use(async (c) => {
       }
       }
     }
     }
 
 
+    // // 处理所有其他请求的 SSR 逻辑
+    // /** @type {string} */
+    // let template;
+    // /** @type {import('./src/server/index.tsx').render} */
+    // let render;
+    
+    // if (!isProduction && vite) {
+    //   // 开发环境:读取最新模板并转换
+    //   template = await fs.readFile('./index.html', 'utf-8');
+    //   template = await vite.transformIndexHtml(normalizedUrl, template);
+    //   render = (await vite.ssrLoadModule('/src/server/index.tsx')).render;
+    // } else {
+    //   // 生产环境:使用缓存的模板
+    //   template = templateHtml;
+    //   render = (await import('./dist/server/index.js')).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>Something went wrong</h1>');
+    //     ssrStream.push(null); // 结束流
+    //   },
+    //   onShellReady() {
+    //     // 将渲染结果通过管道传入 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); // 结束流
+    //     });
+    //   },
+    //   onError(error) {
+    //     didError = true;
+    //     console.error(error);
+    //   },
+    // });
+
+    // // 设置超时中止
+    // abortController = new AbortController();
+    // const abortTimeout = setTimeout(() => {
+    //   abort();
+    //   abortController.abort();
+    // }, ABORT_DELAY);
+
+    // // 将流通过 Hono 响应返回
+    // return c.body(ssrStream, {
+    //   onEnd: () => {
+    //     clearTimeout(abortTimeout);
+    //   }
+    // });
+    await next()
+  } catch (e) {
+    if (!isProduction && vite) {
+      vite.ssrFixStacktrace(e);
+    }
+    console.error(e.stack);
+    return c.text(e.stack, 500);
+  }
+});
+
+// 将请求处理逻辑适配为 Hono 中间件
+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('Not found', 404);
+    }
+
+    // 处理基础路径
+    const normalizedUrl = path.replace(baseUrl.pathname, '/') || '/';
+
     // 处理所有其他请求的 SSR 逻辑
     // 处理所有其他请求的 SSR 逻辑
     /** @type {string} */
     /** @type {string} */
     let template;
     let template;
@@ -139,13 +241,16 @@ app.use(async (c) => {
     
     
     if (!isProduction && vite) {
     if (!isProduction && vite) {
       // 开发环境:读取最新模板并转换
       // 开发环境:读取最新模板并转换
-      template = await fs.readFile('./index.html', 'utf-8');
+      // template = await fs.readFile('./index.html', 'utf-8');
+      const module = (await vite.ssrLoadModule('/src/server/index.tsx'));
+      template = module.template;
       template = await vite.transformIndexHtml(normalizedUrl, template);
       template = await vite.transformIndexHtml(normalizedUrl, template);
-      render = (await vite.ssrLoadModule('/src/server/index.tsx')).render;
+      render = module.render;
     } else {
     } else {
       // 生产环境:使用缓存的模板
       // 生产环境:使用缓存的模板
-      template = templateHtml;
-      render = (await import('./dist/server/index.js')).render;
+      const module = (await import('./dist/server/index.js'))
+      template = module.template;
+      render = module.render;
     }
     }
 
 
     let didError = false;
     let didError = false;
@@ -205,7 +310,6 @@ app.use(async (c) => {
         clearTimeout(abortTimeout);
         clearTimeout(abortTimeout);
       }
       }
     });
     });
-
   } catch (e) {
   } catch (e) {
     if (!isProduction && vite) {
     if (!isProduction && vite) {
       vite.ssrFixStacktrace(e);
       vite.ssrFixStacktrace(e);

+ 0 - 1
src/client/index.tsx

@@ -1,5 +1,4 @@
 // 如果当前是在 /big 下
 // 如果当前是在 /big 下
-import '../style.css'
 if (window.location.pathname.startsWith('/admin')) {
 if (window.location.pathname.startsWith('/admin')) {
   import('./admin/index')
   import('./admin/index')
 } else {
 } else {

+ 11 - 1
src/server/index.tsx

@@ -12,4 +12,14 @@ export function render(_url: string, options?: RenderToPipeableStreamOptions) {
     </StrictMode>,
     </StrictMode>,
     options,
     options,
   )
   )
-}
+}
+
+
+import { renderToStaticMarkup } from 'react-dom/server'
+import { Rooter } from './renderer';
+
+// 使用renderToStaticMarkup - 不会包含React内部属性,生成纯静态HTML
+export const template = renderToStaticMarkup(
+  <Rooter />
+);
+

+ 75 - 0
src/server/renderer.tsx

@@ -0,0 +1,75 @@
+import { GlobalConfig } from '@/share/types'
+import { reactRenderer } from '@hono/react-renderer'
+// import { Script, Link } from 'hono-vite-react-stack-node/components'
+import process from 'node:process'
+
+// 全局配置常量
+const GLOBAL_CONFIG: GlobalConfig = {
+  OSS_BASE_URL: process.env.OSS_BASE_URL || 'https://oss.d8d.fun',
+  APP_NAME: process.env.APP_NAME || '多八多Aider',
+}
+
+// export const renderer = reactRenderer(({ children }) => {
+//   return (
+//     <html>
+//       <head>
+//         <meta charSet="UTF-8" />
+//         <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+//         <script src="https://ai-oss.d8d.fun/umd/vconsole.3.15.1.min.js"></script>
+//         <script dangerouslySetInnerHTML={{ __html: `
+//           const init = () => {
+//             const urlParams = new URLSearchParams(window.location.search);
+//             if (${import.meta.env?.PROD ? "true":"false"} && !urlParams.has('vconsole')) return;
+//             var vConsole = new VConsole({
+//               theme: urlParams.get('vconsole_theme') || 'light',
+//               onReady: function() {
+//                 console.log('vConsole is ready');
+//               }
+//             });
+//           }
+//           init();
+//         `}} />
+//         {/* 注入全局配置 */}
+//         <script dangerouslySetInnerHTML={{ __html: `window.CONFIG = ${JSON.stringify(GLOBAL_CONFIG)};` }} />
+            
+//       </head>
+//       <body>
+//         {children}
+//         <script type="module" src="/src/client/index.tsx"></script>
+//       </body>
+//     </html> 
+//   )
+// })
+
+export const Rooter = () => {
+  return (
+    <html>
+      <head>
+        <meta charSet="UTF-8" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+        <link href='/src/style.css' rel="stylesheet" />
+        <script src="https://ai-oss.d8d.fun/umd/vconsole.3.15.1.min.js"></script>
+        <script dangerouslySetInnerHTML={{ __html: `
+          const init = () => {
+            const urlParams = new URLSearchParams(window.location.search);
+            if (${import.meta.env?.PROD ? "true":"false"} && !urlParams.has('vconsole')) return;
+            var vConsole = new VConsole({
+              theme: urlParams.get('vconsole_theme') || 'light',
+              onReady: function() {
+                console.log('vConsole is ready');
+              }
+            });
+          }
+          init();
+        `}} />
+        {/* 注入全局配置 */}
+        <script dangerouslySetInnerHTML={{ __html: `window.CONFIG = ${JSON.stringify(GLOBAL_CONFIG)};` }} />
+            
+      </head>
+      <body>
+        <div id='root' dangerouslySetInnerHTML={{ __html: '<!--app-html-->'}}></div>
+        <script type="module" src="/src/client/index.tsx"></script>
+      </body>
+    </html> 
+  )
+}