| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116 |
- 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<string, string> = {};
- 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;
- // 强制完整响应,避免 304 缓存导致 HTML 无法被重写
- if (lk === "if-none-match" || lk === "if-modified-since" || lk === "cache-control") continue;
- reqHeaders[key] = Array.isArray(value) ? value.join(", ") : value;
- }
- reqHeaders["host"] = `${VITE_HOST}:${VITE_PORT}`;
- reqHeaders["cache-control"] = "no-cache";
- 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 isText = contentType.includes("text/html") ||
- contentType.includes("text/javascript") ||
- contentType.includes("application/javascript");
- console.log(`[preview:proxy] response ${proxyRes.statusCode} content-type=${contentType} isText=${isText}`);
- // 复制响应 headers(过滤 hop-by-hop)
- const resHeaders: Record<string, string> = {};
- 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 (isText) {
- const chunks: Buffer[] = [];
- proxyRes.on("data", (chunk: Buffer) => chunks.push(chunk));
- proxyRes.on("end", () => {
- let body = Buffer.concat(chunks).toString("utf-8");
- // 重写 HTML (src="/...", href="/...") 和 JS (from "/...", import "/...")
- // 为 /ads/preview/...(跳过已有前缀的,支持单双引号)
- body = body.replace(
- /((?:src|href)=["']|(?:from|import)\s*["'])\/(?!ads\/preview\/)([^"']+)(["'])/g,
- '$1/ads/preview/$2$3',
- );
- // 日志
- if (contentType.includes("text/html")) {
- const viteClientMatch = body.match(/src="[^"]*@vite\/client[^"]*"/);
- console.log(`[preview:proxy] HTML vite/client: ${viteClientMatch?.[0] || "NOT FOUND"}`);
- }
- resHeaders["content-type"] = contentType.includes("text/html")
- ? "text/html; charset=utf-8"
- : contentType;
- resHeaders["content-length"] = String(Buffer.byteLength(body, "utf-8"));
- 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();
- }
- }
|