Vue3+Vite项目部署到Nginx:避坑指南与配置模板 最近把一个 Vue3 Vite 的项目部署到 Nginx 上前后折腾了一整天踩了不少坑。和以往部署纯静态页面不一样Vite 构建出来的项目有自己的路由模式、资源路径和缓存策略如果 Nginx 这边配置没跟上线上就是一堆白屏、404 和资源加载失败。这篇东西不聊那些虚的我把整个部署过程中踩过的坑、用过的配置、排查的思路全部记录下来包括构建阶段要留意的细节、Nginx 里几个容易写错的配置、history 路由刷新 404 的应对方式以及我自己整理的一份可复用的配置模板希望正准备做这件事的朋友能少走点弯路。这篇文章适合这几类人第一次把 Vite 构建产物部署到 Nginx 的前端同学负责前端发布但不太熟 Nginx 的运维朋友自己买了服务器想低成本部署个人项目的独立开发者。基础部分我也会讲但不会啰嗦地重复官方文档尽量把“为什么这么配”讲清楚。1. 部署前先搞清这层关系从 Vite 构建产物到 Nginx 服务很多人在本地开发跑得好好的npm run build 也成功了传到服务器上就白屏。问题往往出在没理解 Vite 构建产物和 Nginx 服务之间的关系。1.1 Vite 构建到底产出了什么Vite 默认执行npm run build之后会在项目根目录生成一个dist文件夹。里面最常见的是这几类东西index.html整个应用的入口文件assets/打包后的 JavaScript、CSS、图片、字体各类静态资源比如public目录下未被编译的文件有一点容易忽略打包出来的 JS 和 CSS 文件名通常带哈希值比如index-2c0e5f4a.js。这个哈希是文件内容计算出来的内容一变文件名就变。好处是浏览器可以“永久缓存”这些带哈希的文件坏处是如果 Nginx 的缓存策略设置不对发布新版后用户还在加载旧文件页面就会表现异常。index.html则是整个应用的核心入口它不参与哈希内容里引用了带哈希的 JS 和 CSS 文件。Vite 默认会把所有逻辑都挂载到 index.html 里的一个挂载点上比如div idapp/div。1.2 为什么前端部署绕不开 NginxVite 构建出来的是纯静态文件。静态文件理论上放在任何能提供 HTTP 服务的软件上都可以比如 Node.js 的 serve、Python 的 http.server、Tomcat。那我为什么还是建议用 Nginx第一Nginx 处理静态文件的能力非常强高并发下内存和 CPU 消耗都比 Node 服务小很多。第二Nginx 支持反向代理前端项目里请求/api接口、WebSocket、文件预览都能通过它转发到后端不需要前端代码里写死后端地址。第三Nginx 对路由回退try_files、gzip 压缩、缓存策略的控制非常灵活这些正是部署 SPA 应用的刚需。打个比方Vite 构建产物是一堆已经“装修好”的家具和材料Nginx 是货架和展示厅它决定这些家具怎么摆放、以多快的速度递给用户、用户在哪个入口进来。家具本身没问题入口和摆放方式错了用户照样逛不了。1.3 部署方案先选好正式部署之前要先确定项目放在服务器哪个位置。最常见的两种网站的根目录路径部署比如https://example.com/子路径部署比如https://example.com/admin/两种方案的 Nginx 配置差别很大尤其体现在 Vite 的base参数上。我自己第一次部署时没注意项目放在子目录下资源全部 404就是因为base没有配置。2. 构建阶段的坑别等部署了才返工很多人把注意力全放在 Nginx 配置上其实很多线上问题源头在构建阶段。这节把我在构建阶段踩过的几个关键点列一下。2.1 base 路径决定资源前缀Vite 有一个配置项base默认值是/。它影响构建产物中所有静态资源的引用路径。如果项目部署在域名根路径用默认值没问题如果部署在子路径就必须修改。我举个实际例子。项目部署在https://example.com/adminNginx 的根目录指向dist内容如果 Vite 的base保持默认/构建出来的 index.html 里资源地址是/assets/index-xxxx.js。浏览器访问https://example.com/admin时会尝试加载https://example.com/assets/index-xxxx.js。如果 Nginx 的站点根目录是dist那它会在dist/assets/...下找文件实际上文件确实在但注意 Nginx 只会在location /匹配到的路径下处理。如果你的 server 没有对/assets做正确映射就会 404。正确做法是修改vite.config.jsexport default { base: /admin/ }构建之后index.html 里的资源路径就变成/admin/assets/index-xxxx.jsNginx 配置里再相应调整问题就解决了。如果是开发环境base 一般不用改但部署前一定要确认。我的经验是在项目根目录搜索一下打包后的 index.html看里面的script src前缀是否和线上访问路径一致这一步能排查掉大量白屏问题。2.2 history 路由 vs hash 路由Vue Router 支持的两种路由模式createWebHistoryhistory 模式和createWebHashHistoryhash 模式。history 模式的特点是 URL 干净比如https://example.com/user/123但它在服务器端有一个关键要求当用户直接访问这个地址或者刷新页面时服务器必须返回 index.html 的内容让前端路由接管。如果服务器返回 404前端就挂了。hash 模式的 URL 长这样https://example.com/#/user/123hash 后面的部分不会发送到服务器所以刷新时服务器永远只收到/这个请求默认返回 index.html 就行。它省心但 URL 丑部分场景下 SEO 不友好分享链接也难看。我的建议是项目是内部管理系统或者工具类应用为了省事可以用 hash 模式对外门户、需要分享链接、对 URL 形态有要求的用 history 模式然后通过 Nginx 的try_files来解决刷新 404 的问题。import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(/admin/), // 注意这里和 base 对应 routes: [...] })需要多说一句history 模式的 base 参数要和 Vite 的base保持一致不然路由跳转时会出问题。2.3 环境变量和构建模式Vue3 项目中开发环境一般用.env.development生产环境用.env.production。以VITE_开头的变量会通过import.meta.env暴露给前端代码。部署时最容易犯的错是把后端的接口地址写死在前端代码里或者忘记区分环境。比如在.env.production里配了VITE_API_BASE_URL/api结果 nginx 那边的反向代理配的是根路径/接口请求到了 Nginx 之后没有被转发到真实的后端服务上就会一直报 404 或者跨域错。构建时确认一下环境文件里的配置是否正确能省很多后面对照排查的时间。2.4 一个容易被忽略的坑public 目录Vite 的public目录里的文件会被原样复制到dist根目录。如果项目里有public/favicon.ico构建之后它会在dist/favicon.ico。页面加载 favicon 时浏览器请求的是/favicon.ico如果在子路径部署就是/admin/favicon.ico没处理好就会一直刷 404。这类小文件问题排查起来比较隐蔽因为它不影响主功能但每次看日志都会看到红红的 404。处理方式要么把 favicon 放在public目录并在index.html里用相对路径引用要么用绝对路径配合 Nginx 的 alias自己权衡即可。3. Nginx 配置核心一份能直接改的部署模板配置 Nginx 部署 Vue3Vite 项目核心就三块路由回退、静态资源缓存、压缩传输。把这三大块处理好项目基本稳了。下面我直接给出一份我目前还在用的配置模板再逐行解释关键点。3.1 try_files 和 SPA 路由回退随便搜 Nginx 部署 Vue都会看到try_files但很多人不理解它的工作机制。先给配置再解释。server { listen 80; server_name example.com; root /var/www/dist; index index.html; location / { try_files $uri $uri/ /index.html; } }location /表示匹配所有以/开头的请求。当用户访问/user/123时Nginx 会依次尝试$uri查找 disk 下是否存在user/123这个文件$uri/查找是否存在user/123/这个目录/index.html如果前两步都没找到直接返回index.html/index.html最后兜底前端的 Vue Router 拿到 index.html 后会解析 URL 并渲染对应的页面。这就是 history 模式刷新不 404 的真相。你可能会问那真正的接口请求过来比如/api/user/123会不会也被try_files兜底返回 index.html当然会如果这个路径下没有对应的接口服务。所以还需要下面这点把接口路径单独配置反向代理不让它走前端静态资源匹配。配置的顺序很重要。Nginx 的 location 匹配规则不是按书写顺序从头到尾找第一个匹配而是有一个优先级规则精确匹配最高前缀匹配^~次之然后是正则匹配最后才是普通前缀匹配。实际配置时静态资源路径和服务接口路径要分开配避免互相干扰。注意try_files中/index.html前面的路径是基于root或alias指令拼接的。如果配置了alias路径拼接逻辑会不同这也是一个常见的配置错误源。3.2 静态资源的强缓存策略Vite 构建出来的带哈希文件比如assets/index-2c0e5f4a.js理论上一旦发布上线文件名永远不会变直到你重新构建。所以这类文件可以设置非常长时间的强缓存让浏览器直接从本地读不用每次刷新都重新下载。index.html则不能缓存或者只能设置很短的缓存时间。原因很简单每次发版index.html 里引用的 JS 文件名都会变如果浏览器把旧的 index.html 缓存在本地它就一直引用旧的 JS 文件永远加载不到新版本代码。所以配置里要区分这两类文件location /assets/ { expires 1y; add_header Cache-Control public, immutable; } location / { add_header Cache-Control no-cache, no-store, must-revalidate; }第一段告诉 Nginx/assets/下的文件允许浏览器缓存一年。第二段告诉 Nginx其他所有文件都不缓存。location / { try_files $uri $uri/ /index.html; add_header Cache-Control no-cache, no-store, must-revalidate; }这样就能做到“常规文件不缓存带哈希的静态资源长时间缓存”新版发布后用户访问依然会重新拉取 index.html然后加载最新的带哈希文件既保证更新及时又最大限度地利用缓存。有一点需要注意add_header在 location 里配置多次后面的会覆盖前面的但不同 location 之间是独立的。如果某个 location 里没有定义add_header那 Nginx 继承的只是本 location 的配置不是整个 server 的。我在做缓存排查时就遇到过明明 server 级别配了add_header X-Frame-Options SAMEORIGIN但某个 location 里配了add_header Cache-Control结果把 server 级的 header 覆盖掉了。3.3 gzip 压缩白给的性能优化Nginx 可以直接给静态资源做 gzip 压缩或者用预先压缩好的.gz文件后者的性能更优。Nginx 动态 gzip 配置是这样的gzip on; gzip_min_length 1k; gzip_comp_level 5; gzip_types text/html text/css application/javascript application/json image/svgxml;光这样配还不够因为 Nginx 是“动态压缩”首次请求时实时压缩会消耗 CPU。更好的方案是用 Vite 插件提前把文件压缩成.gz格式比如vite-plugin-compression构建时生成xx.js.gzNginx 通过gzip_static on直接读取压缩文件返回省掉运行时压缩的开销。gzip_static on; gzip_types text/html text/css application/javascript application/json image/svgxml;需要注意gzip_static这个模块默认不一定编译进 Nginx需要确认一下nginx -V输出里有没有--with-http_gzip_static_module。如果用的是系统包管理器装的 Nginx一般都有如果是编译安装就要自己加参数。3.4 完整的 server 配置模板把上面所有内容合并一份可复用的基础配置大概是这样的server { listen 80; server_name example.com; root /var/www/dist; index index.html; # 静态资源带哈希一年强缓存 location /assets/ { expires 1y; add_header Cache-Control public, immutable; } # 入口文件不缓存 location / { try_files $uri $uri/ /index.html; add_header Cache-Control no-cache, no-store, must-revalidate; } # 接口转发按真实情况调整 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } gzip on; gzip_min_length 1k; gzip_comp_level 5; gzip_types text/html text/css application/javascript application/json image/svgxml; }改完配置后执行nginx -t检查语法再systemctl reload nginx重载生效。4. 接口联调与反向代理最容易写错的一块前端项目基本都要请求后端接口。开发时 Vite 的 proxy 很好用但上线后就要在 Nginx 里配置反向代理。这块地方配置起来不难但细节极多最容易踩坑。4.1 proxy_pass 带不带斜杠语义完全不同这个坑我印象太深了。同样一个locationproxy_pass末尾带不带的差别非常大。# 场景一保留原路径 location /api/ { proxy_pass http://127.0.0.1:8080; } # 请求 /api/user/1 转发到 http://127.0.0.1:8080/api/user/1 # 场景二去掉前缀 location /api/ { proxy_pass http://127.0.0.1:8080/; } # 请求 /api/user/1 转发到 http://127.0.0.1:8080/user/1当proxy_pass后面不带路径没有/结尾Nginx 会把原始 URI 原样转发当带上了/会把 location 前缀api替换成代理地址的根路径。很多后端接口定义里没有/api前缀所以第二种写法更常见。如果你的后端有统一的前缀又不想重复写那就用第一种。关键是要知道这个区别然后在代码里写接口地址时不要写成两层前缀否则就变成/api/api/user/1这类诡异请求。4.2 路径重写和跨域问题前端代码里如果统一走VITE_API_BASE_URL/apiNginx 配置里最常见的组合是location /api/ { rewrite ^/api/(.*)$ /$1 break; proxy_pass http://127.0.0.1:8080; }这里rewrite先把/api/去掉再由proxy_pass转发。和后端对接时后端拿到的路径就没有/api前缀了。还有一个问题有些后端会返回跨域错误。如果后端接口没开 CORS你可以在 Nginx 里把跨域头加上location /api/ { proxy_pass http://127.0.0.1:8080; add_header Access-Control-Allow-Origin *; # 视情况还要加 Allow-Headers、Allow-Methods }但在真实项目中我更建议让后端自己处理好 CORS或者用 Nginx 统一处理。前端不跨域的方式是让接口域名和页面域名保持一致通过 Nginx 反向代理天然同源这比让后端逐接口配置跨域更省事。4.3 WebSocket 的反向代理如果 Vue3 项目里有 WebSocket 连接比如实时消息、聊天Nginx 默认代理是不支持协议升级的需要显式配置location /ws/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; }proxy_http_version 1.1是因为 HTTP/1.0 不支持 WebSocket 升级。proxy_read_timeout用来延长连接空闲超时时间默认 60 秒容易导致连接断开。4.4 Vite 开发代理和 Nginx 配置怎么对应本地开发时Vite 的 proxy 是这样写的export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })上线后Nginx 的配置就要做到同样效果location /api/ { rewrite ^/api/(.*)$ /$1 break; proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; }理解了这种对应关系以后环境切换就不会手忙脚乱。开发环境本地代理测试环境、生产环境则分别配一套 Nginx。5. 多环境、多站点与 Docker 部署实操部署方式上有人直接 apt 装 Nginx 然后手动传文件也有人用 Docker。我个人更推荐 Docker 方案尤其是测试环境和生产环境需要快速保持一致时。但无论哪种方式Nginx 里面的逻辑是一样的。5.1 一套 Nginx 跑多个前端站点如果服务器上要跑多个 Vue3 项目比如一个后台管理系统、一个官网只要在/etc/nginx/conf.d/下建多个配置文件即可。每个文件对应一个 server 块分别指定 server_name 和 root。# /etc/nginx/conf.d/admin.conf server { listen 80; server_name admin.example.com; root /var/www/admin/dist; index index.html; location / { try_files $uri $uri/ /index.html; } }# /etc/nginx/conf.d/www.conf server { listen 80; server_name www.example.com; root /var/www/www/dist; index index.html; location / { try_files $uri $uri/ /index.html; } }热词里提到的“本地 虚拟机多端口 nginx 开发环境多站点自定义域名配置”本质上就是这种方式本地开发时通过修改hosts文件把自定义域名指向127.0.0.1再让 Nginx 监听不同端口每个 server 块绑定不同的 server_name 和端口实现一台机器上同时跑多个环境的效果。比如/etc/hosts里写127.0.0.1 admin.dev.example.com 127.0.0.1 www.dev.example.com然后在 Nginx 里分别配置两个 server 块一个监听 8081 对应 admin一个监听 8082 对应 www。本地访问http://admin.dev.example.com:8081就能精确命中配置的站点。5.2 多端口开发环境的 Nginx 套路部分开发者不想用域名想直接用多端口区分服务。这也完全可以server { listen 8081; server_name localhost; root /opt/projects/admin/dist; location / { try_files $uri $uri/ /index.html; } } server { listen 8082; server_name localhost; root /opt/projects/website/dist; location / { try_files $uri $uri/ /index.html; } }这样访问http://localhost:8081和http://localhost:8082就是两个完全独立的前端应用。要注意的是端口不能冲突并且如果 Nginx 以非 root 身份运行监听 1024 以下端口需要额外授权。5.3 用 Docker 部署 Vue3 Vite 项目Docker 部署的主要优势是不用在宿主机上装 Node、Nginx构建和发布流程可以在容器里隔离完成。多阶段构建的 Dockerfile 大概是这样的# 构建阶段 FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build # 运行阶段 FROM nginx:alpine COPY --frombuilder /app/dist /usr/share/nginx/html COPY nginx/default.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD [nginx, -g, daemon off;]default.conf的内容就是我们上面配置过的 server 块需要把root改为/usr/share/nginx/html和 Nginx 官方镜像约定一致。构建镜像docker build -t my-vue-app .启动容器docker run -d -p 8080:80 --name my-vue-app my-vue-app这里有个细节容器内的 Nginx 默认监听 80 端口宿主机映射为 8080。如果项目需要反向代理到宿主机上的某个服务proxy_pass 不能写localhost或127.0.0.1因为容器里的 localhost 是容器自身不是宿主机。需要在启动容器时加--network host或者使用宿主机在 Docker 网桥上的 IP比如172.17.0.1。踩过一次这个坑后我现在倾向于把后端服务也容器化放到同一个 Docker 网络里通过容器名通信配置反而更清晰。还有一个小细节npm install和npm ci的区别。在 Dockerfile 里我建议用npm ci因为它在package-lock.json存在的情况下会严格按锁文件安装构建结果更可复现。6. 常见问题排查速查表白屏、404、缓存、权限以下是我自己在部署和帮别人排查时遇到的高频问题整理成了一张速查表现象可能原因解决方向部署后首页白屏Vite base 配置不对检查 index.html 中资源路径前缀是否和访问路径一致刷新某个路由 404没有配置 try_files加上try_files $uri $uri/ /index.html;图片加载 404public 目录或 base 路径拼错确认图片请求路径和 dist 目录下实际路径接口请求 404proxy_pass 配置不对检查后端接口前缀和 Nginx 转发规则接口请求跨域后端未开 CORS 或 Nginx 未加跨域头统一同源部署或添加跨域头新版本发布后用户看不到最新代码index.html 被缓存给 index.html 设置no-cache首屏加载很慢没开 gzip或静态资源没走 CDN开启 gzip / gzip_static按需引入 CDN刷新页面资源 404base 配了子路径但 router 的 history base 没配让 Vite base 和 Vue Router history base 保持一致Nginx 启动失败80 端口被占用netstat -tlnp查看端口占用调整监听端口访问显示 403站点目录权限不足检查目录读权限和 Nginx 用户的写权限访问静态文件变成下载MIME 类型没配置确认include mime.types;已配置sitemap.xml 或 favicon.ico 404文件在 public 里但路径引用错用绝对子路径或调整引用地址排查的时候给自己一个清晰的顺序先看浏览器 Network 面板哪个资源 404、哪个请求报错然后对应看 Nginx 错误日志/var/log/nginx/error.log和访问日志/var/log/nginx/access.log。线上问题基本都能通过这两样定位到 80% 的原因。注意改完 Nginx 配置一定要先nginx -t检查语法再nginx -s reload。不要直接 restartrestart 会把当前连接全部断开线上项目会有短暂的访问中断。7. 我自己部署时优化体验的几个小技巧最后分享几个不常用但很好用的小技巧属于顺手记录不是常规文档里会写的东西。第一个是调试时临时返回 JSON。有时候后端还没联调好想让前端先跑起来可以在 Nginx 里临时加一段配置location /api/mock { add_header Content-Type application/json; return 200 {code:0,data:[]}; }这样前端请求该接口时就拿到了假数据方便前端先开发不用一直等后端。第二个是配置日志格式在排查问题时非常有用。默认的 access log 信息太少我一般会自定义 log_formatlog_format main $remote_addr - [$time_local] $request $status $body_bytes_sent $http_user_agent $proxy_host $upstream_addr; access_log /var/log/nginx/access.log main;这样能看到转发到了哪个后端地址对于排查反向代理问题信息量直接翻倍。第三个是给 history 模式下的页面加一个 404 页面兜底。try_files $uri $uri/ /index.html虽然解决了刷新 404但如果你真要匹配一个完全无效的路径比如根目录下没有的文件Nginx 还是会返回 index.html然后 Vue Router 内部去匹配路由。如果不存在的路由没有做兜底页面还是白屏。可以在 Vue Router 里加一个全局的 404 路由把它渲染成一个友好页面这样体验就好很多。{ path: /:pathMatch(.*)*, redirect: / }第四个是关于资源映射。如果项目里使用了对真实文件路径有要求的离线地图、OFD 预览、大文件上传这类功能要留意 Nginx 的location和alias配合方式。比如地图瓦片数据放在服务器某个目录前端请求/map/{z}/{x}/{y}.png就可以配location /map/ { alias /data/map/; }alias会把请求路径中 location 前缀去掉然后拼上 alias 后面的路径。比如请求/map/1/2/3.png实际读取的是/data/map/1/2/3.png。注意root和alias的差异很多人栽在这上面。部署 Vue3 Vite 项目这个事说难不算难但坑位不少。大多数问题本质上就三类路径没对上、缓存策略不对、反向代理配置不对。把这三大方向理顺再配上一手好用的排查命令基本能应对绝大多数场景了。我在实际部署中最大的体会是改配置前先想清楚这层路径关系改完配置后先nginx -t再看浏览器表现线上排查第一步永远是日志最后加缓存策略时才需要谨慎再谨慎。希望这篇记录能帮正在部署的朋友少踩几个坑。