server.js 11 KB

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