server.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  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(
  173. ssrStream
  174. );
  175. };
  176. };
  177. // 生产环境:直接加载API路由和模板
  178. let productionApiModule = null;
  179. let productionTemplate = null;
  180. // 生产环境初始化
  181. const initProduction = async () => {
  182. if (isProduction && !productionApiModule) {
  183. try {
  184. console.log('生产环境: 加载API路由和模板...');
  185. const module = await import('./dist/server/index.js');
  186. productionApiModule = module;
  187. productionTemplate = await processProdTemplate(module.template);
  188. // 1. 先挂载API路由(优先匹配)
  189. app.route('/', module.api);
  190. console.log('生产环境: API路由已挂载到主应用');
  191. // 2. 再注册handleProduction作为兜底中间件(仅处理未被API匹配的请求)
  192. app.use(async (c) => {
  193. return handleProduction(c);
  194. });
  195. console.log('生产环境: 兜底中间件已注册');
  196. } catch (err) {
  197. console.error('生产环境: API路由加载失败:', err);
  198. throw err;
  199. }
  200. }
  201. };
  202. // 开发环境请求处理
  203. const handleDevelopment = async (c) => {
  204. try {
  205. const req = c.env.incoming;
  206. const res = c.env.outgoing;
  207. const url = new URL(req.url, `http://${req.headers.host}`);
  208. const path = url.pathname;
  209. // 检查是否匹配基础路径
  210. if (!path.startsWith(baseUrl.pathname)) {
  211. return c.text('未找到', 404);
  212. }
  213. // 处理基础路径
  214. const normalizedUrl = path.replace(baseUrl.pathname, '/') || '/';
  215. // 使用 Vite 中间件处理请求
  216. const handled = await new Promise((resolve) => {
  217. vite.middlewares(req, res, () => resolve(false));
  218. });
  219. if (handled) {
  220. return c.body;
  221. }
  222. // 动态加载最新 API 模块
  223. const apiModule = await vite.ssrLoadModule('./src/server/index.tsx');
  224. // 创建临时子应用并挂载路由
  225. const apiApp = new Hono();
  226. apiApp.route('/', apiModule.api);
  227. // 处理开发环境模板
  228. const template = await processDevTemplate(apiModule.template);
  229. apiApp.use(createSsrHandler(template, apiModule.render, normalizedUrl));
  230. return apiApp.fetch(c.req.raw, {
  231. ...c.env,
  232. incoming: c.env.incoming,
  233. outgoing: c.env.outgoing
  234. });
  235. } catch (e) {
  236. if (vite) {
  237. vite.ssrFixStacktrace(e);
  238. }
  239. console.error('开发环境请求处理错误:', e.stack);
  240. return c.text('服务器内部错误', 500);
  241. }
  242. };
  243. // 生产环境:处理压缩中间件
  244. const handleCompression = async (c) => {
  245. const req = c.env.incoming;
  246. const res = c.env.outgoing;
  247. const compressed = await new Promise((resolve) => {
  248. compressionMiddleware(req, res, () => resolve(false));
  249. });
  250. return compressed ? c.body : null;
  251. };
  252. // 生产环境:处理静态文件
  253. const handleStaticFiles = async (c) => {
  254. const req = c.env.incoming;
  255. const res = c.env.outgoing;
  256. const served = await new Promise((resolve) => {
  257. sirvMiddleware(req, res, () => resolve(false));
  258. });
  259. return served ? c.body : null;
  260. };
  261. // 生产环境:处理SSR渲染
  262. const handleSsrRendering = async (c) => {
  263. try {
  264. const normalizedUrl = c.req.path.replace(baseUrl.pathname, '/') || '/';
  265. return createSsrHandler(productionTemplate, productionApiModule.render, normalizedUrl)(c);
  266. } catch (err) {
  267. console.error('生产环境SSR渲染错误:', err);
  268. return c.text('服务器内部错误', 500);
  269. }
  270. };
  271. // 生产环境请求处理
  272. const handleProduction = async (c) => {
  273. try {
  274. // 1. 处理压缩
  275. const compressionResult = await handleCompression(c);
  276. if (compressionResult) return compressionResult;
  277. // 2. 处理静态文件
  278. const staticResult = await handleStaticFiles(c);
  279. if (staticResult) return staticResult;
  280. // 3. 处理SSR渲染(作为最后手段)
  281. return await handleSsrRendering(c);
  282. } catch (err) {
  283. console.error('生产环境请求处理错误:', err);
  284. return c.text('服务器内部错误', 500);
  285. }
  286. };
  287. // 启动服务器
  288. console.log('准备启动服务器...');
  289. // 生产环境初始化
  290. if (isProduction) {
  291. await initProduction();
  292. }else{
  293. app.use(async (c) => {
  294. return handleDevelopment(c);
  295. });
  296. }
  297. parentServer.listen(port, () => {
  298. console.log('========================================');
  299. console.log(`服务器已成功启动!`);
  300. console.log(`访问地址: http://localhost:${port}`);
  301. console.log(`环境: ${isProduction ? '生产环境' : '开发环境'}`);
  302. console.log('========================================');
  303. })
  304. // 统一的服务器关闭处理函数
  305. const shutdownServer = async () => {
  306. console.log('正在关闭服务器...');
  307. // 1. 先关闭 Vite 开发服务器(包括 HMR 服务)
  308. if (!isProduction && vite) {
  309. console.log('正在关闭 Vite 开发服务器(包括 HMR 服务)...');
  310. try {
  311. await vite.close();
  312. console.log('Vite 开发服务器已关闭');
  313. } catch (err) {
  314. console.error('关闭 Vite 服务器时出错:', err);
  315. }
  316. }
  317. // 2. 关闭主服务器
  318. parentServer.close((err) => {
  319. if (err) {
  320. console.error('关闭主服务器时出错:', err);
  321. process.exit(1);
  322. }
  323. console.log('主服务器已关闭');
  324. process.exit(0);
  325. });
  326. };
  327. // 处理进程终止信号
  328. process.on('SIGINT', shutdownServer);
  329. process.on('SIGTERM', shutdownServer);