Vite HMR 代理后端配置

解读

国内主流 PHP 项目(Laravel、ThinkPHP、Hyperf 等)在前后端分离后,前端团队普遍用 Vite 做构建工具。开发阶段,Vite 自带的热更新(HMR)走的是 WebSocket + ES Module,端口默认 5173,而 PHP 后端通常跑在 80/443 或 9000 端口,并借助 Nginx/Apache 做虚拟主机。此时浏览器访问 https://local.test 时,静态资源与 HMR 通道必须被“无痕”转发到 Vite 服务,否则会出现“热更新 426/502/timeout”或“页面刷新后样式丢失”等典型故障。面试官问“怎么配”,既考察候选人对 Vite 底层协议的理解,也验证其能否在真实 LEMP/LNMP 环境中把“前端端口”与“后端域名”打通,同时兼顾 HTTPS 证书、路径重写、WebSocket 升级、Cookie 域一致性等细节,属于“线上出过事故”的高频考点。

知识点

  1. Vite HMR 协议:WebSocket 路径固定 /vite-hmr,端口默认 5173,可配置 server.hmr
  2. Nginx 反向代理指令:proxy_passproxy_set_header Upgradeproxy_set_header Connectionproxy_read_timeout
  3. 路径重写:用 ^~ /vite-hmrlocation ~* \.(js|css|svg)$ 把静态资源和 HMR 流量分开转发,避免把 PHP 路由污染。
  4. HTTPS 终端:若 Nginx 负责 SSL 终端,需在 proxy_set_header X-Forwarded-Proto $scheme 保证 Vite 识别 wss
  5. Vite 配置项:server.originserver.hostserver.strictPortserver.https,以及 .env.development 中写死 VITE_HMR_PROTOCOL=wss
  6. 同源策略:Cookie 的 Path=/; SameSite=Lax 需与代理后域名一致,否则后端 Session 刷新失败。
  7. 多入口场景:微前端或 Monorepo 时,用 server.proxy/app1/app2 分别指到不同 Vite 实例,避免端口冲突。
  8. 性能安全:开发阶段关闭 server.cors.origin: true,生产阶段禁止把 /vite-hmr 暴露到外网,防止未授权热更新脚本注入。

答案

以“Laravel + Vite + Nginx + HTTPS”为例,给出可直接落地的最小可用配置,分三步完成。

第一步:Vite 端统一域名与协议
vite.config.js

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
  plugins: [
    laravel({
      input: ['resources/js/app.js', 'resources/css/app.css'],
      refresh: true,
    }),
  ],
  server: {
    host: '0.0.0.0',
    port: 5173,
    strictPort: true,
    https: false,                       // 由 Nginx 统一做 SSL 终端
    origin: 'https://local.test',       // 告诉 Vite 生成的 URL 以域名开头
    hmr: {
      host: 'local.test',
      protocol: 'wss',
      clientPort: 443,                  // 浏览器只认 443,不走 5173
    },
  },
});

.env.development

VITE_HMR_PROTOCOL=wss
VITE_HMR_HOST=local.test

第二步:Nginx 虚拟主机把 HMR 与静态资源全部代理到 Vite
/etc/nginx/conf.d/local.test.conf

server {
    listen 443 ssl http2;
    server_name local.test;
    index index.php;
    root /data/www/laravel/public;

    ssl_certificate     /etc/ssl/local.test.pem;
    ssl_certificate_key /etc/ssl/local.test.key;

    # 1. 先处理 HMR WebSocket
    location ^~ /vite-hmr {
        proxy_pass http://127.0.0.1:5173;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 86400;
    }

    # 2. 再处理 Vite 生成的静态资源(带 hash 的文件)
    location ~* ^.+\.(js|css|svg|png|jpg|jpeg|gif|ico|woff2?)$ {
        proxy_pass http://127.0.0.1:5173;
        proxy_set_header Host $host;
        expires off;          # 开发阶段禁用缓存
    }

    # 3. 其余请求交给 PHP-FPM
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass  unix:/run/php/php8.2-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

重载 Nginx:nginx -s reload

第三步:启动开发脚本

# 终端 1
php artisan serve --host=0.0.0.0 --port=8000   # 可选,若不想用 Nginx 解析 PHP
# 终端 2
npm run dev                                    # 启动 Vite,监听 5173

浏览器访问 https://local.test,F12 网络面板应看到 wss://local.test/vite-hmr 101 状态码,且修改 resources/js/app.js 后页面无刷新即时更新,即代理成功。

拓展思考

  1. Docker Compose 场景:把 Vite 容器别名设为 vite,Nginx 容器通过 proxy_pass http://vite:5173 通信,避免宿主机端口暴露,同时利用 networks.frontend 隔离开发网段。
  2. 多后端语言共存:同一域名下既有 PHP 接口 /api,又有 Go 微服务 /svc,可在 Nginx 里用 location ^~ /svc 把流量转发到 Go 容器,而 /vite-hmr 继续指向 Vite,实现“三端一域”开发。
  3. 生产环境禁止代理:上线前执行 npm run build,把产物放到 public/build,Nginx 直接 try_files $uri $uri/ /index.php,并加 Cache-Control: max-age=31536000, immutable;同时把 /vite-hmr 路径 return 404,防止外部探测。
  4. 高级调试:若 HMR 偶发断连,可在 vite.config.jsserver.hmr.overlay: false 关闭错误遮罩,或在 Nginx 侧把 proxy_buffering off 避免 WebSocket 被缓存,快速定位是网络层还是 Vite 层问题。