| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- 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);
- }
- });
- // 移除生产环境的 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);
|