previewProxy.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import { Request, Response } from "express";
  2. import http from "http";
  3. const VITE_HOST = "127.0.0.1";
  4. const VITE_PORT = 5199;
  5. // 这些 header 需要从代理响应中剔除(hop-by-hop)
  6. const HOP_BY_HOP = new Set([
  7. "transfer-encoding",
  8. "connection",
  9. "keep-alive",
  10. "proxy-connection",
  11. "proxy-authenticate",
  12. "proxy-authorization",
  13. "te",
  14. "trailers",
  15. "upgrade",
  16. ]);
  17. /**
  18. * 将 /preview/* 请求代理到 Vite dev server。
  19. * 对 HTML 响应自动重写路径,给所有根绝对路径加上 /ads/preview/ 前缀,
  20. * 确保浏览器通过 nginx 正确加载资源。
  21. */
  22. export function previewProxy(req: Request, res: Response): void {
  23. // Express mount 会剥离 /preview 前缀,req.url 即 Vite 需要的路径
  24. const targetPath = req.url || "/";
  25. console.log(`[preview:proxy] ${req.method} ${targetPath}`);
  26. // 过滤请求 headers(去掉 hop-by-hop,设置正确的 host)
  27. const reqHeaders: Record<string, string> = {};
  28. for (const [key, value] of Object.entries(req.headers)) {
  29. if (value === undefined) continue;
  30. const lk = key.toLowerCase();
  31. if (lk === "host") continue;
  32. if (lk === "connection" || lk === "keep-alive" || lk === "upgrade") continue;
  33. reqHeaders[key] = Array.isArray(value) ? value.join(", ") : value;
  34. }
  35. reqHeaders["host"] = `${VITE_HOST}:${VITE_PORT}`;
  36. const proxyReq = http.request(
  37. {
  38. hostname: VITE_HOST,
  39. port: VITE_PORT,
  40. path: targetPath,
  41. method: req.method,
  42. headers: reqHeaders,
  43. },
  44. (proxyRes) => {
  45. const contentType = (proxyRes.headers["content-type"] || "") as string;
  46. const isHtml = contentType.includes("text/html");
  47. console.log(`[preview:proxy] response ${proxyRes.statusCode} content-type=${contentType} isHtml=${isHtml}`);
  48. // 复制响应 headers(过滤 hop-by-hop)
  49. const resHeaders: Record<string, string> = {};
  50. for (const [key, value] of Object.entries(proxyRes.headers)) {
  51. if (HOP_BY_HOP.has(key.toLowerCase())) continue;
  52. if (value !== undefined && value !== null) {
  53. resHeaders[key] = Array.isArray(value) ? value.join(", ") : value;
  54. }
  55. }
  56. res.status(proxyRes.statusCode || 200);
  57. if (isHtml) {
  58. const chunks: Buffer[] = [];
  59. proxyRes.on("data", (chunk: Buffer) => chunks.push(chunk));
  60. proxyRes.on("end", () => {
  61. let html = Buffer.concat(chunks).toString("utf-8");
  62. // 将 src="/..." 和 href="/..." 重写为 /ads/preview/...
  63. // 匹配 root-absolute 路径:以 / 开头,后跟非 / 字符(避免匹配 //)
  64. html = html.replace(
  65. /(src|href)="\/([^/][^"]*)"/g,
  66. '$1="/ads/preview/$2"',
  67. );
  68. // 日志:检查重写后的 @vite/client 行
  69. const viteClientMatch = html.match(/src="[^"]*@vite\/client[^"]*"/);
  70. console.log(`[preview:proxy] HTML vite/client: ${viteClientMatch?.[0] || "NOT FOUND"}`);
  71. const body = Buffer.from(html, "utf-8");
  72. resHeaders["content-type"] = "text/html; charset=utf-8";
  73. resHeaders["content-length"] = String(body.length);
  74. res.set(resHeaders);
  75. res.send(body);
  76. });
  77. } else {
  78. res.set(resHeaders);
  79. proxyRes.pipe(res);
  80. }
  81. },
  82. );
  83. proxyReq.on("error", (err) => {
  84. console.error(`[preview:proxy] ${err.message}`);
  85. if (!res.headersSent) {
  86. res.status(502).json({ error: "Preview server not available" });
  87. }
  88. });
  89. // POST/PUT/PATCH 请求需要转发 body
  90. if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
  91. req.pipe(proxyReq);
  92. } else {
  93. proxyReq.end();
  94. }
  95. }