|
|
@@ -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);
|
|
|
+});
|