server.js 11 KB

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