最近把一个 Vue3 + Vite 项目正式部署到 Nginx 上,从打包到上线前后折腾了两天。中途踩过几个不算罕见却很难一眼发现的坑,比如打包后白屏、history 路由刷新 404、浏览器缓存旧文件、接口路径多了一层 /api。这类问题网上答案很多但很零散,今天把这套流程完整串起来,从 Vite 配置、打包产物、Nginx 配置到排查技巧,一次性讲清楚。适合正在用 Vite 开发 Vue 项目、准备把构建产物部署到 Nginx 的前端开发者,也适合刚接触部署,想知道“为什么这么配”的新手。
1. 部署前把这几件事想清楚
1.1 Vite为什么要打包再部署
很多朋友第一次接触部署时会有一个疑问:本地开发都是 npm run dev,为什么不能直接把项目源码丢到服务器上?
Vite 开发服务器之所以快,是因为它将浏览器原生 ES Modules 作为加载方式。开发阶段它不需要把所有代码提前打包,而是按需启动 Dev Server,代码在浏览器请求时动态编译,所以冷启动和热更新都很快。但生产环境不能这么干,原因是浏览器无法直接识别 .vue 文件,也缺少语法转换和依赖树合并的环节。Vite 在生产构建时使用 Rollup 将入口文件、组件、第三方依赖统一打包成 JS、CSS、HTML 等静态资源,同时完成压缩、tree-shaking、Hash 版本号等优化。
所以最终放在 Nginx 上的是 dist 目录,而不是整个 Vue 工程。理解这一点非常重要,后面排查路径、缓存、404 时,你的思路会清晰很多。
1.2 base路径决定资源加载成败
Vite 配置里有一个非常不起眼但直接影响项目能不能打开的选项:base。它决定了构建产物中资源引用的公共路径。
默认值是 /,也就是说打包后 index.html 里的静态资源路径是 /assets/index-xxxx.js。如果应用部署在域名根路径,比如 http://example.com/,那没有任何问题。但如果你把站点放在某个子目录下,比如 http://example.com/myapp/,或者 Nginx root 指向了一个子目录,而访问 URL 不是域名根路径,资源引用就会自动去域名根目录下找 /assets/xxx,结果必然是 404,页面白屏。
我的做法是,在部署环境不是绝对域名根路径的情况下,将 base 配置为相对路径:
javascript复制export default defineConfig({
base: './'
})
这样打包产物中会引用相对路径 ./assets/xxx,在子目录、二级域名、IP+端口访问时都能正确加载。如果项目必须用 history 路由且部署在子路径,还是建议用绝对路径,比如 base: '/myapp/',同时保证 Nginx 层对子路径做正确跳转。base 的选型没有绝对标准,但你必须知道它和实际部署路径是强关联的。
1.3 环境变量管理不同环境接口地址
如果项目里有多个环境,比如测试环境、预发环境、生产环境,接口地址不应该硬编码在业务代码里。Vite 支持基于环境文件注入变量。
在项目根目录创建 .env.development 和 .env.production:
bash复制# .env.development
VITE_API_BASE_URL=/api
bash复制# .env.production
VITE_API_BASE_URL=/api
代码里通过 import.meta.env.VITE_API_BASE_URL 获取。构建时会根据当前模式自动加载对应的 .env 文件,npm run dev 加载 development,npm run build 加载 production。如果还想区分预发环境,可以建一个 .env.staging,然后用 npm run build -- --mode staging 指定模式。
注意环境变量必须以 VITE_ 开头才能暴露到客户端代码中。把接口地址归一成同域 /api 代理,是部署到 Nginx 后最省心的方案,后面配置反向代理时只需要把 /api 转发到真实后端地址即可。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. vite.config.js怎么配才稳
2.1 一份可上线的生产配置
直接给出一份我目前在用的 Vue3 + Vite 生产配置,兼顾了路径、别名、拆包、代理:
javascript复制import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
base: './',
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false,
chunkSizeWarningLimit: 1500,
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia']
}
}
}
},
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})
重点说一下几个关键项:
sourcemap: false 会关闭生产环境内的 source map 文件,避免源码暴露到公网,也能减少发版体积。chunkSizeWarningLimit 用于控制警告阈值,如果某个三方库很大,Vite 会在构建时提示 chunk 大小超过 500KB,这个选项不是解决体积问题,只是让 CI 日志干净一些。
manualChunks 会把 Vue 全家桶这些不常更换的依赖单独打包。为什么要这么做?因为第三方库的代码基本不会变,第一次加载后浏览器会把它缓存住。如果所有代码都塞进一个 JS 里,每次业务代码一改,整个文件 Hash 都会变,用户就要重新下载全部代码。拆包后,业务代码更新对公共依赖包没有影响,缓存命中率明显提高。
2.2 hash还是history路由,别等部署再改
Vue Router 有两种模式:hash 和 history。如果路由模式没在开发阶段确认好,部署到 Nginx 时经常会被打一个措手不及。
hash 模式地址长这样:http://example.com/#/dashboard,它依靠浏览器锚点变化切换路由,不向服务器发送额外请求。这种模式部署成本最低,但 URL 不美观,部分场景下分享链接还会带上 #。
history 模式地址是干净的:http://example.com/dashboard。它使用 History API 修改地址,但刷新 /dashboard 时浏览器会向 Nginx 请求这个路径,服务器上根本没有对应的物理文件,Nginx 如果没配置 fallback,就会返回 404。
我的建议是:尽量用 history 模式,同时必须在 Nginx 里配置 try_files 来兜底。如果你们团队没有 Nginx 修改权限,或者目标环境很特殊,那 hash 模式才是稳妥选择。总之路由模式不能拍脑袋选,要在开发前告诉部署方。
2.3 拆包和压缩,首屏加载更轻
除了上面提到的 manualChunks,构建时生成 .gz 文件是我强烈推荐的一个步骤。项目里可以用 vite-plugin-compression:
bash复制npm install vite-plugin-compression -D
javascript复制import viteCompression from 'vite-plugin-compression'
plugins: [
vue(),
viteCompression({
threshold: 10240,
algorithm: 'gzip'
})
]
这样构建产物中会为超过 10KB 的文件额外生成 .gz 版本。Nginx 开启 gzip_static on 后,会优先把 .gz 文件直接发给浏览器,省去服务器实时压缩的 CPU 消耗,传输体积也能减少 60% 以上。
需要注意的是,不要只压缩而不在 Nginx 里配置,否则压缩包全浪费了。后面第 4 章我会给出完整配置。
3. 打包构建与本地验收
3.1 打包命令与Node版本要求
打包前先确认 Node 版本。Vite 5 要求 Node.js 18+,版本过低会在 npm run build 时报各种看不懂的错误。先执行:
bash复制node -v
npm -v
然后安装依赖:
bash复制npm install
如果公司内网依赖下载慢,可以配置临时镜像:
bash复制npm install --registry=https://registry.npmmirror.com
安装完成执行生产构建:
bash复制npm run build
构建成功后终端会列出产物文件列表和体积。如果加了 --mode staging,则打包过程中会加载 .env.staging 的环境变量。
3.2 看懂dist目录
默认构建输出目录是 dist,大致结构如下:
text复制dist/
├── index.html
├── assets/
│ ├── index-abc123.js
│ ├── vendor-def456.js
│ └── index-abc123.css
index.html 是应用入口,里面引用了带 Hash 的 JS/CSS 文件。Hash 是内容哈希,只要文件内容不变,文件名就保持不变;一旦业务代码更新,Hash 就会变化。线上静态资源策略往往就依赖这个机制:用户首次访问后,浏览器把 vendor-xxx.js 缓存起来,下次访问时不需要重新下载;而业务代码变了,新的 index-yyy.js 会被 index.html 直接引用,也不会出现缓存串号。
3.3 本地预览的正确姿势
很多人在 npm run build 后直接 npm run dev 验证,这是错的。dev 是开发模式,跑的是源码,不是 dist 产物。想验证打包结果,Vite 提供了 preview:
bash复制npm run preview
它会启动一个静态服务指向 dist,适合检查资源加载路径是否正常。但 preview 对 history 路由的 fallback 支持并不完全等同于 Nginx,如果项目用了 history 模式,我建议在本机直接用一个支持 SPA fallback 的静态服务器来测试:
bash复制npx serve -s dist -l 8080
-s 参数会让所有未匹配到文件的请求都 fallback 到 index.html,这和 Nginx 里 try_files 的效果基本一致。本地能跑通,再上传到服务器会少踩很多坑。
4. Nginx安装与配置
4.1 安装Nginx
如果是 Linux 服务器,Ubuntu/Debian 系使用:
bash复制sudo apt update
sudo apt install nginx
CentOS/RHEL 系使用:
bash复制sudo yum install nginx
Windows 也可以下载 Nginx 的 zip 包解压后直接运行 nginx.exe,但生产环境建议还是用 Linux。安装后先确认版本:
bash复制nginx -v
然后看一下默认配置文件的位置,比如 Ubuntu 的默认站点在 /etc/nginx/sites-available/default,CentOS 的默认配置在 /etc/nginx/conf.d/default.conf。我个人习惯不使用默认文件,而是在 /etc/nginx/conf.d/ 下新建一个独立配置,比如 vue-app.conf,方便回滚和排查。
4.2 配置一个可用的server块
下面是一份支持 Vue3 + Vite 产物部署的 Nginx 配置,包含静态托管、history 路由 fallback、静态资源缓存和接口代理:
nginx复制server {
listen 80;
server_name example.com;
root /var/www/my-vue-app;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /assets/ {
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
}
location /api/ {
proxy_pass http://127.0.0.1:8080/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
这里最关键的是这一行:
nginx复制try_files $uri $uri/ /index.html;
它的执行逻辑是:先按当前 URL 查找物理文件,比如访问 /assets/index.js 时,能匹配到文件就直接返回;如果匹配不到,再尝试作为目录找;最后还是找不到,就把请求交给 /index.html。对于 history 模式下的 /dashboard 这类前端路由,Nginx 会返回应用首页 HTML,再由 Vue Router 接管并渲染对应页面。
location /api/ 的代理规则同样需要仔细核对。proxy_pass 的路径是否带 /api/,直接影响转发结果:
proxy_pass http://127.0.0.1:8080/api/;表示保留/api/前缀,转发到后端地址为http://127.0.0.1:8080/api/login。proxy_pass http://127.0.0.1:8080;不带 URI 时,转发地址是http://127.0.0.1:8080/api/login,也会保留/api/。- 如果后端真正接口不含
/api,需要用rewrite或proxy_pass http://127.0.0.1:8080/;把/api/前缀去掉。
这块建议写个简单的接口试一下,不要凭感觉配。
4.3 缓存策略
为了让静态资源缓存不越界,需要区分两类文件:带 Hash 的 assets 和入口 index.html。
带 Hash 的静态资源可以放心长缓存,因为内容变了文件名就变了:
nginx复制location /assets/ {
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
}
但 index.html 绝对不能强缓存,否则发布新版本后用户可能一直停留在旧页面。可以单独给它设置 no-cache:
nginx复制location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
这里说下为什么:index.html 是入口文件,浏览器必须先请求它,才能拿到 assets 的新文件名。如果入口文件被缓存了,用户浏览器看到的还是旧 HTML,再引用旧资源,新版本自然不会生效。把 index.html 设置为每次重新验证,再配合资源文件名 Hash,就能做到“页面每次检查更新,资源尽量走缓存”。
4.4 gzip压缩
即便构建时已经生成了 .gz 文件,Nginx 侧还需要开启对应功能:
nginx复制gzip on;
gzip_static on;
gzip_min_length 1k;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
gzip_static on 表示如果存在 .gz 文件,直接发送预压缩文件,不再实时压缩。开启后,我可以明确告诉你,一个 300KB 的 JS 文件经过 gzip 传输,体积通常会降到 100KB 左右,首屏加载速度提升非常明显。
需要注意,如果使用默认的 Nginx 包,gzip_static 可能需要 --with-http_gzip_static_module 编译参数,不过多数发行版的官方包已经内置了这个模块。可以通过 nginx -V 查看编译参数确认。
5. 部署、验证与后续更新
5.1 上传dist到服务器
本机确认 dist 没问题后,把产物上传到服务器。最简单的是用 scp:
bash复制scp -r dist/* root@your-server:/var/www/my-vue-app/
如果是多次更新,我更推荐 rsync,它只传输变更部分,比 scp 全量覆盖高效很多:
bash复制rsync -avz --delete dist/ root@your-server:/var/www/my-vue-app/
--delete 会删除服务器目标目录里存在但本地 dist 中没有的文件,保证旧版本的 assets 不会残留。使用它要特别小心,别把目标目录指错,否则会误删文件。建议先确认 root 后面没有多写斜杠,必要时先备份目录。
5.2 nginx -t 和 reload
改完 Nginx 配置后不能直接重启生效,先检查语法:
bash复制nginx -t
如果输出 syntax is ok,再平滑重载配置:
bash复制nginx -s reload
reload 不会中断当前连接,对于正在运行的业务来说很友好。如果语法检查不过,Nginx 会提示错误位置,一般是缺分号、括号没闭合、路径配错。
5.3 浏览器验收流程
新版本上线后,打开浏览器直接访问域名/IP,打开开发者工具 Network 面板,按这几个维度确认:
index.html请求返回 200,并且响应头里的Cache-Control是no-cache。assets下的 JS/CSS 请求都返回 200,没有 404。- 如果配置了接口代理,找一个实际接口请求看看转发路径是否正确,返回状态是否是 200。
- 刷新一个子路由,比如
/dashboard,确认页面不 404。 - 如果项目用了 history 模式,直接在地址栏输入
/dashboard回车测试,不要只在站内点击跳转。
这一套检查做完,基本可以确定部署成功。
5.4 版本回滚与灰度小技巧
部署没出问题时用不到,但万一新版本有问题,不能临时改代码再打包,那太慢。我的习惯是保留上个版本的 dist 目录,比如:
text复制/var/www/my-vue-app/releases/20250101-v1.0.0
/var/www/my-vue-app/releases/20250201-v1.0.1
/var/www/my-vue-app/current -> releases/20250201-v1.0.1
Nginx 的 root 指向 current 软链接,发布时先复制新目录,然后修改软链接指向,再 nginx -s reload。出问题就几秒钟切回旧版本。这个做法比较朴素的,但非常实用。
6. 高频问题排查实录
6.1 白屏排查
部署后最常见的现象是白屏。打开控制台,可能会看到 Failed to load resource,JS/CSS 加载失败。
排查顺序建议从外到内:
| 现象 | 原因 | 解决 |
|---|---|---|
| 白屏,assets 404 | base 路径和部署路径不一致 |
把 base 改为 ./ 或子路径绝对路径 |
| 白屏,assets 200,控制台报跨域 | 接口跨域或代理未生效 | 检查 Nginx proxy_pass 和后端 CORS |
| 白屏,首页 200,刷新子路由 404 | 未配置 try_files |
Nginx location / 增加 try_files |
| 白屏,接口 200,但 JS 有报错 | 业务代码问题 | 看控制台具体错误堆栈 |
白屏很多时候不是 Vite 或 Vue 本身的问题,而是资源路径和路由 fallback 组合起来的结果。你只要把 base/root/try_files 这三项全部核对一遍,大多数问题都能定位。
6.2 刷新404
项目使用 history 路由模式时,站内点击跳转正常,但手动刷新 /detail/123 或者地址栏直接输入这个 URL 就 404。原因很明确:浏览器把 /detail/123 当作真实路径请求服务器,Nginx 找不到这个文件。
解决办法就是前面说的,在 location / 中配置:
nginx复制try_files $uri $uri/ /index.html;
如果你的应用部署在子路径,还需要带上子路径:
nginx复制location /myapp/ {
alias /var/www/my-vue-app/;
try_files $uri $uri/ /myapp/index.html;
}
这种情况下 base 也必须是 /myapp/,不能继续用 ./,否则路由推跳的路径会和部署路径不一致。
6.3 接口请求404与跨域
接口出现 404 时,先看请求 URL:
- 前端请求
http://example.com/api/login。 - Nginx 匹配
location /api/。 - 转发到
http://127.0.0.1:8080/api/login。
如果实际上后端接口是 http://127.0.0.1:8080/login,你就不能用保留 /api/ 的写法,而是要改写:
nginx复制location /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:8080;
}
rewrite 会把路径里的 /api/ 去掉再转发。这个细节如果不弄清楚,接口部署后很容易报 404 或 404 前面带 301。
跨域则是另一种情况:页面部署在 http://example.com,直接请求 http://other-server:8080/api,浏览器会拦截。最佳方案是通过 Nginx 把 /api 代理到后端,让前后端同域,从根上消除 CORS 问题。少量特殊环境下走 CORS 也行,但要精确配置 Access-Control-Allow-Origin,不建议全站放开。
6.4 浏览器缓存旧版本
发布新版本后,用户浏览器还是老页面,这种情况在 Nginx 部署里太常见了。原因几乎都是 index.html 被缓存。
解决办法有两层:
第一层,Nginx 配置里给 index.html 设置 no-cache。第二层,确保静态资源文件名带 Hash,并且 assets 目录开启长缓存。这两层配合好后,用户每次打开页面都会拉取最新的 index.html,再加载新 Hash 的资源。
如果你改完配置后自己测试还是旧版,先强制刷新一次,确认是响应头没有生效还是缓存问题。浏览器开发者工具里取消勾选 Disable cache,也可以直接无痕窗口验证。
6.5 关于Nginx代理配置的心得
最后分享一个我实际踩过的坑:不要把多个站点的 Nginx 配置全堆在 nginx.conf 里。不同项目建独立配置文件,通过 include 引入,并且修改前先备份。这样每次上线影响面最小,查问题也不用在一大段配置里大海捞针。
如果你团队后续要做自动部署,可以把 npm run build 和 rsync 串联到 CI 流水线里,但部署逻辑仍然要遵循这套流程:先构建,再测试,再上传,再 reload。流程标准化之后,花在环境问题上的时间会少很多。
我个人实际部署完这个项目后最大的体会是:Vite 和 Nginx 本身都不复杂,真正让人头疼的是路径、路由、缓存这些“接口缝隙”。把 base、try_files、Cache-Control 这几个关键点理解透,部署一次基本就能管用很久。
