Browse Source

✨ feat(server): 集成 Hono 框架并重构服务器架构

- 添加 Hono 和 @hono/node-server 依赖包
- 重构服务器架构,从 Express 迁移到 Hono
- 改进 URL 处理逻辑,使用 URL 对象进行路径解析
- 优化开发和生产环境的中间件处理流程
- 增强错误处理和服务器稳定性

♻️ refactor(server): 优化请求处理流程和代码结构

- 重构请求处理函数,分离业务逻辑
- 改进基础路径匹配和规范化处理
- 优化 Vite 开发服务器集成方式
- 调整生产环境中间件初始化逻辑
- 增强代码注释和格式化,提高可读性
yourname 7 months ago
parent
commit
012eaa8ce4
3 changed files with 151 additions and 63 deletions
  1. 2 0
      package.json
  2. 22 0
      pnpm-lock.yaml
  3. 127 63
      server.js

+ 2 - 0
package.json

@@ -11,8 +11,10 @@
     "preview": "PORT=8080 cross-env NODE_ENV=production node server"
   },
   "dependencies": {
+    "@hono/node-server": "^1.17.1",
     "compression": "^1.8.0",
     "express": "^5.1.0",
+    "hono": "^4.8.5",
     "react": "^19.1.0",
     "react-dom": "^19.1.0",
     "sirv": "^3.0.1"

+ 22 - 0
pnpm-lock.yaml

@@ -8,12 +8,18 @@ importers:
 
   .:
     dependencies:
+      '@hono/node-server':
+        specifier: ^1.17.1
+        version: 1.17.1(hono@4.8.5)
       compression:
         specifier: ^1.8.0
         version: 1.8.1
       express:
         specifier: ^5.1.0
         version: 5.1.0
+      hono:
+        specifier: ^4.8.5
+        version: 4.8.5
       react:
         specifier: ^19.1.0
         version: 19.1.0
@@ -207,6 +213,12 @@ packages:
     cpu: [x64]
     os: [win32]
 
+  '@hono/node-server@1.17.1':
+    resolution: {integrity: sha512-SY79W/C+2b1MyAzmIcV32Q47vO1b5XwLRwj8S9N6Jr5n1QCkIfAIH6umOSgqWZ4/v67hg6qq8Ha5vZonVidGsg==}
+    engines: {node: '>=18.14.1'}
+    peerDependencies:
+      hono: ^4
+
   '@polka/url@1.0.0-next.29':
     resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
 
@@ -616,6 +628,10 @@ packages:
     resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
     engines: {node: '>= 0.4'}
 
+  hono@4.8.5:
+    resolution: {integrity: sha512-Up2cQbtNz1s111qpnnECdTGqSIUIhZJMLikdKkshebQSEBcoUKq6XJayLGqSZWidiH0zfHRCJqFu062Mz5UuRA==}
+    engines: {node: '>=16.9.0'}
+
   http-errors@2.0.0:
     resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
     engines: {node: '>= 0.8'}
@@ -972,6 +988,10 @@ snapshots:
   '@esbuild/win32-x64@0.25.8':
     optional: true
 
+  '@hono/node-server@1.17.1(hono@4.8.5)':
+    dependencies:
+      hono: 4.8.5
+
   '@polka/url@1.0.0-next.29': {}
 
   '@rolldown/pluginutils@1.0.0-beta.27': {}
@@ -1363,6 +1383,8 @@ snapshots:
     dependencies:
       function-bind: 1.1.2
 
+  hono@4.8.5: {}
+
   http-errors@2.0.0:
     dependencies:
       depd: 2.0.0

+ 127 - 63
server.js

@@ -1,104 +1,168 @@
-import fs from 'node:fs/promises'
-import express from 'express'
-import { Transform } from 'node:stream'
+import fs from 'node:fs/promises';
+import http from 'node:http';
+import { URL } from 'node:url';
+import { Transform } from 'node:stream';
 
 // Constants
-const isProduction = process.env.NODE_ENV === 'production'
-const port = process.env.PORT || 5173
-const base = process.env.BASE || '/'
-const ABORT_DELAY = 10000
+const isProduction = process.env.NODE_ENV === 'production';
+const port = process.env.PORT || 5173;
+const base = process.env.BASE || '/';
+const ABORT_DELAY = 10000;
+
+// 解析基础路径为 URL 对象,方便后续处理
+const baseUrl = new URL(base, `http://localhost:${port}`);
 
 // Cached production assets
-const templateHtml = isProduction
-  ? await fs.readFile('./dist/client/index.html', 'utf-8')
-  : ''
+let templateHtml = '';
+if (isProduction) {
+  templateHtml = await fs.readFile('./dist/client/index.html', 'utf-8');
+}
 
-// Create http server
-const app = express()
+// 生产环境中间件
+let compressionMiddleware;
+let sirvMiddleware;
+if (isProduction) {
+  compressionMiddleware = (await import('compression')).default();
+  sirvMiddleware = (await import('sirv')).default('./dist/client', { 
+    extensions: [],
+    baseUrl: base 
+  });
+}
 
-// Add Vite or respective production middlewares
+// Vite 开发服务器
 /** @type {import('vite').ViteDevServer | undefined} */
-let vite
+let vite;
 if (!isProduction) {
-  const { createServer } = await import('vite')
+  const { createServer } = await import('vite');
   vite = await createServer({
     server: { middlewareMode: true },
     appType: 'custom',
     base,
-  })
-  app.use(vite.middlewares)
-} else {
-  const compression = (await import('compression')).default
-  const sirv = (await import('sirv')).default
-  app.use(compression())
-  app.use(base, sirv('./dist/client', { extensions: [] }))
+  });
 }
 
-// Serve HTML
-app.use('*all', async (req, res) => {
+// 处理请求的函数
+async function handleRequest(req, res) {
   try {
-    const url = req.originalUrl.replace(base, '')
+    const url = new URL(req.url, `http://${req.headers.host}`);
+    const path = url.pathname;
+
+    // 检查是否匹配基础路径
+    if (!path.startsWith(baseUrl.pathname)) {
+      res.writeHead(404, { 'Content-Type': 'text/plain' });
+      res.end('Not found');
+      return;
+    }
+
+    // 处理基础路径
+    const normalizedUrl = path.replace(baseUrl.pathname, '/') || '/';
+
+    // 开发环境:使用 Vite 中间件
+    if (!isProduction && vite) {
+      // 使用 Vite 中间件处理请求
+      const handled = await new Promise((resolve) => {
+        vite.middlewares(req, res, () => resolve(false));
+      });
+      
+      // 如果 Vite 中间件已经处理了请求,直接返回
+      if (handled) return;
+    } 
+    // 生产环境:使用 compression 和 sirv 中间件
+    else if (isProduction) {
+      // 先尝试 compression 中间件
+      const compressed = await new Promise((resolve) => {
+        compressionMiddleware(req, res, () => resolve(false));
+      });
+      
+      if (compressed) return;
+      
+      // 再尝试 sirv 中间件处理静态文件
+      const served = await new Promise((resolve) => {
+        sirvMiddleware(req, res, () => resolve(false));
+      });
+      
+      if (served) return;
+    }
 
+    // 处理所有其他请求的 SSR 逻辑
     /** @type {string} */
-    let template
+    let template;
     /** @type {import('./src/entry-server.ts').render} */
-    let render
-    if (!isProduction) {
-      // Always read fresh template in development
-      template = await fs.readFile('./index.html', 'utf-8')
-      template = await vite.transformIndexHtml(url, template)
-      render = (await vite.ssrLoadModule('/src/entry-server.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/entry-server.tsx')).render;
     } else {
-      template = templateHtml
-      render = (await import('./dist/server/entry-server.js')).render
+      // 生产环境:使用缓存的模板
+      template = templateHtml;
+      render = (await import('./dist/server/entry-server.js')).render;
     }
 
-    let didError = false
+    let didError = false;
 
-    const { pipe, abort } = render(url, {
+    const { pipe, abort } = render(normalizedUrl, {
       onShellError() {
-        res.status(500)
-        res.set({ 'Content-Type': 'text/html' })
-        res.send('<h1>Something went wrong</h1>')
+        res.writeHead(500, { 'Content-Type': 'text/html' });
+        res.end('<h1>Something went wrong</h1>');
       },
       onShellReady() {
-        res.status(didError ? 500 : 200)
-        res.set({ 'Content-Type': 'text/html' })
+        res.writeHead(didError ? 500 : 200, { 'Content-Type': 'text/html' });
 
         const transformStream = new Transform({
           transform(chunk, encoding, callback) {
-            res.write(chunk, encoding)
-            callback()
+            res.write(chunk, encoding);
+            callback();
           },
-        })
+        });
 
-        const [htmlStart, htmlEnd] = template.split(`<!--app-html-->`)
+        const [htmlStart, htmlEnd] = template.split(`<!--app-html-->`);
 
-        res.write(htmlStart)
+        res.write(htmlStart);
 
         transformStream.on('finish', () => {
-          res.end(htmlEnd)
-        })
+          res.end(htmlEnd);
+        });
 
-        pipe(transformStream)
+        pipe(transformStream);
       },
       onError(error) {
-        didError = true
-        console.error(error)
+        didError = true;
+        console.error(error);
       },
-    })
+    });
+
+    // 设置超时中止
+    const abortTimeout = setTimeout(() => {
+      abort();
+    }, ABORT_DELAY);
+
+    // 清理超时
+    res.on('finish', () => {
+      clearTimeout(abortTimeout);
+    });
 
-    setTimeout(() => {
-      abort()
-    }, ABORT_DELAY)
   } catch (e) {
-    vite?.ssrFixStacktrace(e)
-    console.log(e.stack)
-    res.status(500).end(e.stack)
+    if (!isProduction && vite) {
+      vite.ssrFixStacktrace(e);
+    }
+    console.error(e.stack);
+    res.writeHead(500, { 'Content-Type': 'text/plain' });
+    res.end(e.stack);
   }
-})
+}
+
+// 创建 HTTP 服务器
+const server = http.createServer(handleRequest);
+
+// 启动服务器
+server.listen(port, () => {
+  console.log(`Server started at http://localhost:${port}`);
+});
 
-// Start http server
-app.listen(port, () => {
-  console.log(`Server started at http://localhost:${port}`)
-})
+// 处理服务器错误
+server.on('error', (err) => {
+  console.error('Server error:', err);
+});