server.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. import 'dotenv/config'
  2. import fs from 'node:fs/promises';
  3. import { URL } from 'node:url';
  4. import { Transform } from 'node:stream';
  5. import { Readable } from 'node:stream';
  6. import { Hono } from 'hono';
  7. import { logger } from 'hono/logger';
  8. import { createServer as createNodeServer } from 'node:http';
  9. import process from 'node:process';
  10. import { createAdaptorServer } from '@hono/node-server'
  11. // 创建 Hono 应用
  12. const app = new Hono();
  13. // 全局使用 Hono 日志中间件
  14. app.use('*', logger());
  15. // 常量定义
  16. const isProduction = process.env.NODE_ENV === 'production';
  17. const port = process.env.PORT || 8080;
  18. const base = process.env.BASE || '/';
  19. const ABORT_DELAY = 10000;
  20. console.log('========================================');
  21. console.log('开始初始化服务器...');
  22. console.log(`环境: ${isProduction ? '生产环境' : '开发环境'}`);
  23. console.log(`端口: ${port}`);
  24. console.log(`基础路径: ${base}`);
  25. console.log('========================================');
  26. // 解析基础路径为 URL 对象
  27. const baseUrl = new URL(base, `http://localhost:${port}`);
  28. console.log(`基础URL解析完成: ${baseUrl.href}`);
  29. // 创建服务器实例
  30. console.log('正在创建服务器实例...');
  31. const parentServer = createAdaptorServer({
  32. fetch: app.fetch,
  33. createServer: createNodeServer,
  34. port: port,
  35. })
  36. console.log('服务器实例创建成功');
  37. // 生产环境中间件
  38. let compressionMiddleware;
  39. let sirvMiddleware;
  40. if (isProduction) {
  41. console.log('生产环境: 加载压缩和静态文件中间件...');
  42. compressionMiddleware = (await import('compression')).default();
  43. sirvMiddleware = (await import('sirv')).default('./dist/client', {
  44. extensions: [],
  45. baseUrl: base
  46. });
  47. console.log('生产环境中间件加载完成');
  48. }
  49. // Vite 开发服务器
  50. /** @type {import('vite').ViteDevServer | undefined} */
  51. let vite;
  52. if (!isProduction) {
  53. console.log('开发环境: 初始化 Vite 开发服务器...');
  54. const { createServer } = await import('vite');
  55. vite = await createServer({
  56. server: {
  57. middlewareMode: {
  58. server: parentServer
  59. },
  60. hmr: {
  61. port: 8081,
  62. clientPort: 443,
  63. path: 'vite-hmr'
  64. },
  65. proxy: {
  66. '/vite-hmr': {
  67. target: 'ws://localhost:8081',
  68. ws: true,
  69. },
  70. },
  71. },
  72. appType: 'custom',
  73. base,
  74. });
  75. console.log('Vite 开发服务器初始化完成');
  76. }
  77. // 开发环境模板处理函数 - 仅处理模板转换
  78. const processDevTemplate = async (template) => {
  79. if (!isProduction && vite) {
  80. console.log('开发环境: 处理模板...');
  81. const processedTemplate = await vite.transformIndexHtml('/', template);
  82. console.log('开发环境模板处理完成');
  83. return processedTemplate;
  84. }
  85. return template;
  86. };
  87. // 生产环境模板处理函数 - 仅处理资源路径替换
  88. const processProdTemplate = async (template) => {
  89. console.log('生产环境: 处理模板资源路径...');
  90. try {
  91. // 读取 manifest
  92. const manifestPath = new URL('./dist/client/.vite/manifest.json', import.meta.url);
  93. const manifestContent = await fs.readFile(manifestPath, 'utf-8');
  94. const manifest = JSON.parse(manifestContent);
  95. console.log('生产环境: 成功读取 manifest.json');
  96. // 获取 src/client/index.tsx 对应的资源信息
  97. const indexManifest = manifest['src/client/index.tsx'];
  98. if (!indexManifest) {
  99. throw new Error('manifest 中未找到 src/client/index.tsx 入口配置');
  100. }
  101. // 获取 src/style.css 对应的资源信息
  102. const styleManifest = manifest['src/style.css'];
  103. if (!styleManifest) {
  104. throw new Error('manifest 中未找到 src/style.css 入口配置');
  105. }
  106. const cssPath = new URL(styleManifest.file, baseUrl).pathname;
  107. const cssLinks = `<link href="${cssPath}" rel="stylesheet" />`;
  108. // 替换入口脚本
  109. const jsEntryPath = new URL(indexManifest.file, baseUrl).pathname;
  110. const entryScript = `<script type="module" src="${jsEntryPath}"></script>`;
  111. // 执行替换
  112. const processedTemplate = template
  113. .replace(/<link href="\/src\/style.css" rel="stylesheet"\/>/, cssLinks)
  114. .replace(/<script type="module" src="\/src\/client\/index.tsx"><\/script>/, entryScript);
  115. console.log('生产环境模板处理完成');
  116. return processedTemplate;
  117. } catch (err) {
  118. console.error('生产环境模板处理失败:', err);
  119. throw err;
  120. }
  121. };
  122. // SSR 渲染中间件函数
  123. const createSsrHandler = (template, render, normalizedUrl) => {
  124. return async (c) => {
  125. let didError = false;
  126. let abortController;
  127. // 创建一个可读流用于 SSR 渲染内容
  128. const [htmlStart, htmlEnd] = template.split(`<!--app-html-->`);
  129. const ssrStream = new Readable({ read: () => {} });
  130. // 写入 HTML 头部
  131. ssrStream.push(htmlStart);
  132. // 设置响应头和状态码
  133. c.header('Content-Type', 'text/html');
  134. // 处理渲染
  135. const { pipe, abort } = render(normalizedUrl, {
  136. onShellError() {
  137. didError = true;
  138. c.status(500);
  139. ssrStream.push('<h1>服务器渲染出错</h1>');
  140. ssrStream.push(null); // 结束流
  141. },
  142. onShellReady() {
  143. console.log(`开始渲染页面: ${normalizedUrl}`);
  144. // 将渲染结果通过管道传入 ssrStream
  145. const transformStream = new Transform({
  146. transform(chunk, encoding, callback) {
  147. ssrStream.push(chunk, encoding);
  148. callback();
  149. }
  150. });
  151. pipe(transformStream);
  152. // 当 transformStream 完成时,添加 HTML 尾部
  153. transformStream.on('finish', () => {
  154. ssrStream.push(htmlEnd);
  155. ssrStream.push(null); // 结束流
  156. console.log(`页面渲染完成: ${normalizedUrl}`);
  157. });
  158. },
  159. onError(error) {
  160. didError = true;
  161. console.error('渲染过程出错:', error);
  162. },
  163. });
  164. // 设置超时中止
  165. abortController = new AbortController();
  166. const abortTimeout = setTimeout(() => {
  167. console.log(`渲染超时,终止请求: ${normalizedUrl}`);
  168. abort();
  169. abortController.abort();
  170. }, ABORT_DELAY);
  171. // 将流通过 Hono 响应返回
  172. return c.body(ssrStream, {
  173. onEnd: () => {
  174. clearTimeout(abortTimeout);
  175. }
  176. });
  177. };
  178. };
  179. // 统一的请求处理中间件 - 合并 API 和 SSR 渲染逻辑
  180. app.use(async (c) => {
  181. try {
  182. // 使用 c.env 获取原生请求响应对象
  183. const req = c.env.incoming;
  184. const res = c.env.outgoing;
  185. const url = new URL(req.url, `http://${req.headers.host}`);
  186. const path = url.pathname;
  187. // 检查是否匹配基础路径
  188. if (!path.startsWith(baseUrl.pathname)) {
  189. return c.text('未找到', 404);
  190. }
  191. // 处理基础路径
  192. const normalizedUrl = path.replace(baseUrl.pathname, '/') || '/';
  193. console.log(`处理请求: ${normalizedUrl}`);
  194. // 开发环境:使用 Vite 中间件
  195. if (!isProduction && vite) {
  196. // 使用 Vite 中间件处理请求
  197. const handled = await new Promise((resolve) => {
  198. vite.middlewares(req, res, () => resolve(false));
  199. });
  200. // 如果 Vite 中间件已经处理了请求,直接返回
  201. if (handled) {
  202. return c.body;
  203. }
  204. // 动态加载最新 API 模块
  205. const apiModule = await vite.ssrLoadModule('./src/server/index.tsx');
  206. // 创建临时子应用并挂载路由
  207. const apiApp = new Hono();
  208. apiApp.route('/', apiModule.api);
  209. // 处理开发环境模板
  210. const template = await processDevTemplate(apiModule.template);
  211. apiApp.get('*', createSsrHandler(template, apiModule.render, normalizedUrl));
  212. // 直接由子应用处理 API 请求
  213. return apiApp.fetch(c.req.raw, {
  214. ...c.env,
  215. // 传递原始请求对象
  216. incoming: c.env.incoming,
  217. outgoing: c.env.outgoing
  218. });
  219. }
  220. // 生产环境:使用 compression 和 sirv 中间件
  221. else if (isProduction) {
  222. // 先尝试 compression 中间件
  223. const compressed = await new Promise((resolve) => {
  224. compressionMiddleware(req, res, () => resolve(false));
  225. });
  226. if (compressed) {
  227. return c.body;
  228. }
  229. // 再尝试 sirv 中间件处理静态文件
  230. const served = await new Promise((resolve) => {
  231. sirvMiddleware(req, res, () => resolve(false));
  232. });
  233. if (served) {
  234. return c.body;
  235. }
  236. // 生产环境:使用缓存的 API 路由
  237. const { api, template: rawTemplate, render } = (await import('./dist/server/index.js'));
  238. const apiApp = new Hono();
  239. apiApp.route('/', api);
  240. // 处理生产环境模板
  241. const template = await processProdTemplate(rawTemplate);
  242. apiApp.get('*', createSsrHandler(template, render, normalizedUrl));
  243. return apiApp.fetch(c.req.raw, {
  244. ...c.env,
  245. incoming: c.env.incoming,
  246. outgoing: c.env.outgoing
  247. });
  248. }
  249. } catch (e) {
  250. if (!isProduction && vite) {
  251. vite.ssrFixStacktrace(e);
  252. console.error('请求处理错误:', e.stack);
  253. return c.text(e.stack, 500);
  254. }
  255. console.error('请求处理错误:', e.stack);
  256. return c.text('服务器内部错误', 500);
  257. }
  258. });
  259. // 移除生产环境的 API 路由预加载(已在统一中间件中处理)
  260. // 移除动态 API 路由中间件(已合并到统一中间件中)
  261. // 启动服务器
  262. console.log('准备启动服务器...');
  263. parentServer.listen(port, () => {
  264. console.log('========================================');
  265. console.log(`服务器已成功启动!`);
  266. console.log(`访问地址: http://localhost:${port}`);
  267. console.log(`环境: ${isProduction ? '生产环境' : '开发环境'}`);
  268. console.log('========================================');
  269. })
  270. // 统一的服务器关闭处理函数
  271. const shutdownServer = async () => {
  272. console.log('正在关闭服务器...');
  273. // 1. 先关闭 Vite 开发服务器(包括 HMR 服务)
  274. if (!isProduction && vite) {
  275. console.log('正在关闭 Vite 开发服务器(包括 HMR 服务)...');
  276. try {
  277. await vite.close();
  278. console.log('Vite 开发服务器已关闭');
  279. } catch (err) {
  280. console.error('关闭 Vite 服务器时出错:', err);
  281. }
  282. }
  283. // 2. 关闭主服务器
  284. parentServer.close((err) => {
  285. if (err) {
  286. console.error('关闭主服务器时出错:', err);
  287. process.exit(1);
  288. }
  289. console.log('主服务器已关闭');
  290. process.exit(0);
  291. });
  292. };
  293. // 处理进程终止信号
  294. process.on('SIGINT', shutdownServer);
  295. process.on('SIGTERM', shutdownServer);