|
|
@@ -84,202 +84,76 @@ if (!isProduction) {
|
|
|
}
|
|
|
|
|
|
|
|
|
-// 请求处理中间件 - 通用逻辑
|
|
|
-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);
|
|
|
+// 开发环境模板处理函数 - 仅处理模板转换
|
|
|
+const processDevTemplate = async (template) => {
|
|
|
+ if (!isProduction && vite) {
|
|
|
+ console.log('开发环境: 处理模板...');
|
|
|
+ const processedTemplate = await vite.transformIndexHtml('/', template);
|
|
|
+ console.log('开发环境模板处理完成');
|
|
|
+ return processedTemplate;
|
|
|
}
|
|
|
-});
|
|
|
-
|
|
|
-// 生产环境:启动时加载 API 路由
|
|
|
-if (isProduction) {
|
|
|
- console.log('生产环境: 加载编译后的 API 路由...');
|
|
|
- const api = (await import('./dist/api/api.js')).default;
|
|
|
- app.route('/', api);
|
|
|
- console.log('API 路由加载完成');
|
|
|
-}
|
|
|
-// 开发环境:不在此处加载 API 路由,改为在中间件中动态加载
|
|
|
+ return template;
|
|
|
+};
|
|
|
|
|
|
-// 添加动态 API 路由中间件
|
|
|
-app.use(async (c, next) => {
|
|
|
+// 生产环境模板处理函数 - 仅处理资源路径替换
|
|
|
+const processProdTemplate = async (template) => {
|
|
|
+ console.log('生产环境: 处理模板资源路径...');
|
|
|
try {
|
|
|
- // 开发环境:每次请求动态加载 API 路由
|
|
|
- if (!isProduction && vite) {
|
|
|
- // 动态加载最新 API 模块
|
|
|
- const apiModule = await vite.ssrLoadModule('./src/server/api.ts');
|
|
|
-
|
|
|
- // 创建临时子应用并挂载路由
|
|
|
- const apiApp = new Hono();
|
|
|
- apiApp.route('/', apiModule.default);
|
|
|
-
|
|
|
- // 检查是否为 API 请求
|
|
|
- if (
|
|
|
- c.req.path.startsWith('/api')
|
|
|
- || c.req.path.startsWith('/ui')
|
|
|
- || c.req.path.startsWith('/doc')
|
|
|
- ) {
|
|
|
- // 直接由子应用处理 API 请求
|
|
|
- return apiApp.fetch(c.req.raw, {
|
|
|
- ...c.env,
|
|
|
- // 传递原始请求对象
|
|
|
- incoming: c.env.incoming,
|
|
|
- outgoing: c.env.outgoing
|
|
|
- });
|
|
|
- }
|
|
|
+ // 读取 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 入口配置');
|
|
|
}
|
|
|
|
|
|
- await next();
|
|
|
- } catch (e) {
|
|
|
- if (!isProduction && vite) {
|
|
|
- vite.ssrFixStacktrace(e);
|
|
|
+ // 获取 src/style.css 对应的资源信息
|
|
|
+ const styleManifest = manifest['src/style.css'];
|
|
|
+ if (!styleManifest) {
|
|
|
+ throw new Error('manifest 中未找到 src/style.css 入口配置');
|
|
|
}
|
|
|
- console.error('API 路由加载错误:', e.stack);
|
|
|
- return c.text('API 服务器错误', 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;
|
|
|
+ const cssPath = new URL(styleManifest.file, baseUrl).pathname;
|
|
|
+ const cssLinks = `<link href="${cssPath}" rel="stylesheet" />`;
|
|
|
|
|
|
- // 检查是否匹配基础路径
|
|
|
- if (!path.startsWith(baseUrl.pathname)) {
|
|
|
- return c.text('未找到', 404);
|
|
|
- }
|
|
|
+ // 替换入口脚本
|
|
|
+ const jsEntryPath = new URL(indexManifest.file, baseUrl).pathname;
|
|
|
+ const entryScript = `<script type="module" src="${jsEntryPath}"></script>`;
|
|
|
|
|
|
- // 处理基础路径
|
|
|
- const normalizedUrl = path.replace(baseUrl.pathname, '/') || '/';
|
|
|
- console.log(`处理请求: ${normalizedUrl}`);
|
|
|
+ // 执行替换
|
|
|
+ const processedTemplate = template
|
|
|
+ .replace(/<link href="\/src\/style.css" rel="stylesheet"\/>/, cssLinks)
|
|
|
+ .replace(/<script type="module" src="\/src\/client\/index.tsx"><\/script>/, entryScript);
|
|
|
|
|
|
- // 处理所有其他请求的 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; // 终止启动,避免使用错误模板
|
|
|
- }
|
|
|
+ console.log('生产环境模板处理完成');
|
|
|
+ return processedTemplate;
|
|
|
|
|
|
- render = module.render;
|
|
|
- }
|
|
|
+ } 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() {
|
|
|
@@ -299,7 +173,7 @@ app.use(async (c) => {
|
|
|
});
|
|
|
|
|
|
pipe(transformStream);
|
|
|
-
|
|
|
+
|
|
|
// 当 transformStream 完成时,添加 HTML 尾部
|
|
|
transformStream.on('finish', () => {
|
|
|
ssrStream.push(htmlEnd);
|
|
|
@@ -327,15 +201,107 @@ app.use(async (c) => {
|
|
|
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('SSR 处理错误:', e.stack);
|
|
|
+ console.error('请求处理错误:', e.stack);
|
|
|
return c.text('服务器内部错误', 500);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
+// 移除生产环境的 API 路由预加载(已在统一中间件中处理)
|
|
|
+// 移除动态 API 路由中间件(已合并到统一中间件中)
|
|
|
+
|
|
|
// 启动服务器
|
|
|
console.log('准备启动服务器...');
|
|
|
parentServer.listen(port, () => {
|