import { Request, Response } from "express"; import http from "http"; const VITE_HOST = "127.0.0.1"; const VITE_PORT = 5199; // 这些 header 需要从代理响应中剔除(hop-by-hop) const HOP_BY_HOP = new Set([ "transfer-encoding", "connection", "keep-alive", "proxy-connection", "proxy-authenticate", "proxy-authorization", "te", "trailers", "upgrade", ]); /** * 将 /preview/* 请求代理到 Vite dev server。 * 对 HTML 响应自动重写路径,给所有根绝对路径加上 /ads/preview/ 前缀, * 确保浏览器通过 nginx 正确加载资源。 */ export function previewProxy(req: Request, res: Response): void { // Express mount 会剥离 /preview 前缀,req.url 即 Vite 需要的路径 const targetPath = req.url || "/"; console.log(`[preview:proxy] ${req.method} ${targetPath}`); // 过滤请求 headers(去掉 hop-by-hop,设置正确的 host) const reqHeaders: Record = {}; for (const [key, value] of Object.entries(req.headers)) { if (value === undefined) continue; const lk = key.toLowerCase(); if (lk === "host") continue; if (lk === "connection" || lk === "keep-alive" || lk === "upgrade") continue; reqHeaders[key] = Array.isArray(value) ? value.join(", ") : value; } reqHeaders["host"] = `${VITE_HOST}:${VITE_PORT}`; const proxyReq = http.request( { hostname: VITE_HOST, port: VITE_PORT, path: targetPath, method: req.method, headers: reqHeaders, }, (proxyRes) => { const contentType = (proxyRes.headers["content-type"] || "") as string; const isHtml = contentType.includes("text/html"); console.log(`[preview:proxy] response ${proxyRes.statusCode} content-type=${contentType} isHtml=${isHtml}`); // 复制响应 headers(过滤 hop-by-hop) const resHeaders: Record = {}; for (const [key, value] of Object.entries(proxyRes.headers)) { if (HOP_BY_HOP.has(key.toLowerCase())) continue; if (value !== undefined && value !== null) { resHeaders[key] = Array.isArray(value) ? value.join(", ") : value; } } res.status(proxyRes.statusCode || 200); if (isHtml) { const chunks: Buffer[] = []; proxyRes.on("data", (chunk: Buffer) => chunks.push(chunk)); proxyRes.on("end", () => { let html = Buffer.concat(chunks).toString("utf-8"); // 将 src="/..." 和 href="/..." 重写为 /ads/preview/... // 匹配 root-absolute 路径:以 / 开头,后跟非 / 字符(避免匹配 //) html = html.replace( /(src|href)="\/([^/][^"]*)"/g, '$1="/ads/preview/$2"', ); // 日志:检查重写后的 @vite/client 行 const viteClientMatch = html.match(/src="[^"]*@vite\/client[^"]*"/); console.log(`[preview:proxy] HTML vite/client: ${viteClientMatch?.[0] || "NOT FOUND"}`); const body = Buffer.from(html, "utf-8"); resHeaders["content-type"] = "text/html; charset=utf-8"; resHeaders["content-length"] = String(body.length); res.set(resHeaders); res.send(body); }); } else { res.set(resHeaders); proxyRes.pipe(res); } }, ); proxyReq.on("error", (err) => { console.error(`[preview:proxy] ${err.message}`); if (!res.headersSent) { res.status(502).json({ error: "Preview server not available" }); } }); // POST/PUT/PATCH 请求需要转发 body if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") { req.pipe(proxyReq); } else { proxyReq.end(); } }