server.js 10 KB

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