
我想先说一个比较反直觉的结论Vite打包默认出来的文件如果你直接丢到Nginx的root目录里大概率能跑但只要你的项目里用了路由懒加载、图片资源引用了绝对路径、或者你想把前端项目挂到某个子路径下面就会立刻翻车。这套能跑的假象恰恰是部署阶段所有问题的起点。这篇文章我会完整走一遍Vite打包 Vue项目 Nginx部署的链路从打包配置里的坑、Nginx配置文件的逐行拆解、多级路由刷新404的根源到项目上线后常见的布局错乱、m3u8直播流播放失败、接口请求403等问题。整个过程是我在实际项目里反复踩坑之后沉淀下来的可以直接照着抄。1. 为什么选Vite而不是Webpack不只是因为快聊部署之前得先搞清楚Vite在构建环节到底做了什么。很多初接触Vite的同学只知道它很快但快只是开发服务器的表象。真正影响部署的是Vite的生产构建方式——它基于Rollup产出的是一套天然优化过的静态资源。1.1 开发与构建两套体系的区别Vite在开发环境走的是基于原生ESModule的按需加载浏览器直接请求一个个.vue文件对应的编译结果所以冷启动快、热更新也快。但生产打包时Vite全部交给Rollup处理做的事情包括将所有模块按依赖关系打包成若干chunk文件把CSS提取为独立文件默认策略是每个异步chunk对应一个CSS文件对图片、字体等静态资源自动做Base64内联低于assetsInlineLimit阈值或输出为带哈希的文件生成index.html并在其中注入正确的资源引用路径这套流程决定了你在vite.config.js里的每一项配置最终都会反映到Nginx层的文件组织方式上。举个最常见的例子base配置。开发时Vite默认把base当作/构建出的index.html里引用的资源路径也是/assets/xxx.js。如果Nginx把项目部署在根路径下这当然没问题但只要项目要放在/admin/这样的子路径下就必须把base改为/admin/否则所有资源都会404。1.2 Vite构建产物的目标结构Vite打包完成后默认输出到项目根目录的dist/文件夹结构大致如下dist/ ├── index.html ├── favicon.ico ├── assets/ │ ├── index-abc123.js │ ├── index-def456.css │ ├── About-789xyz.js │ └── logo-ghi789.pngindex.html是关键入口Nginx的所有静态资源定位逻辑都围绕它展开。看起来很简单但这里就埋伏着第一个大坑assets目录里的文件名都带哈希值这本身是为了方便浏览器缓存和版本更新但如果你的Nginx配置对.js或.css文件设置了过长的缓存时间那么每次发版后用户浏览器可能还拿着旧的index.html引用旧的哈希文件而旧的哈希文件在新版本里已经不存在了于是就会出现页面空白或样式丢失。这些问题表面上发生在Nginx根子却在打包配置。所以在聊Nginx之前先把Vite侧的配置梳理清楚。2. 打包前的关键配置index.html里的路径问题前端项目能不能被Nginx正确加载第一道关卡就是index.html。Nginx接到请求后会根据配置文件定位到index.html并返回给浏览器浏览器再根据其中写的资源路径去加载JS、CSS。所以index.html里的路径一旦写错后面全盘皆错。2.1 base属性的三种常见取值场景base是Vite构建时控制资源公共基础路径的配置项取值决定了index.html里所有资源引用的前缀。我整理了三种最常见的使用场景场景base配置效果项目部署在域名根路径base: /资源引用为/assets/index-xxx.js项目部署在子目录base: /admin/资源引用为/admin/assets/index-xxx.js纯本地文件打开或CDN分发base: ./资源引用为相对路径./assets/index-xxx.js这里要特别提醒base配置为相对路径./确实能解决一部分本地直接双击index.html的场景但在清单路由history模式下会产生新的问题。因为Nginx如果做了try_files重定向相对路径经过URL重写后可能失去锚点导致资源加载失败。所以我个人建议生产环境部署到Nginx要么用根路径要么明确写出子目录的绝对路径不要图省事用./。2.2 通过环境变量区分不同部署环境项目多了之后测试环境、预发环境、生产环境的部署路径很可能都不一样。如果每次发布前手动改base很快就会出事故。更好用的方案是利用Vite的环境变量机制// vite.config.js import { defineConfig, loadEnv } from vite import vue from vitejs/plugin-vue export default defineConfig(({ mode }) { const env loadEnv(mode, process.cwd()) return { base: env.VITE_PUBLIC_BASE_PATH || /, plugins: [vue()], build: { outDir: dist, assetsDir: assets, sourcemap: false, chunkSizeWarningLimit: 1024 } } })然后在项目根目录创建.env.productionVITE_PUBLIC_BASE_PATH/如果某天这个项目要部署到/demo/目录只改这个文件重新打包即可不需要动任何业务代码。这也是我项目中一直使用的方式用不上的人可能觉得多此一举但一旦碰上同一个项目要部署到多个客户环境的定制化交付场景这一套就能节省大量沟通成本。2.3 静态资源的缓存策略与哈希打包产物带哈希值是Vite的默认行为每次代码变更后重新构建只有变更过的文件哈希会变。这个设计的初衷是为了配合浏览器的长缓存策略。所以对于assets目录下的文件Nginx可以放心地设置较长的expires比如30天因为文件名变了就相当于一个新请求。但要注意一个例外index.html本身千万不能设置长缓存。否则用户访问站点时Nginx会直接返回缓存的index.html里面的资源引用还是旧哈希和服务器上实际部署的新文件对不上。正确的做法是给index.html配置no-cache让它每次请求都回源校验而JS、CSS这些哈希文件则大胆地长缓存。3. Nginx部署的完整配置思路从单站点到多项目共存Nginx作为静态文件服务器和反向代理是整个部署链路的后半程。这里不谈那些过于理论的配置讲解直接给出一套我实测可用的完整方案并逐行解释关键参数。3.1 最小可用配置单站点单Vue项目一个最基础的Vue单页应用部署到Nginx后配置段大致如下server { listen 80; server_name yourdomain.com; gzip on; gzip_min_length 1k; gzip_types text/plain text/css application/javascript application/json application/xml image/svgxml; root /var/www/vue-project/dist; index index.html; location /assets/ { expires 30d; add_header Cache-Control public, immutable; } location / { try_files $uri $uri/ /index.html; } }这段配置的核心在于最后那个try_files $uri $uri/ /index.html;。它的逻辑是浏览器请求某个路径Nginx先看看有没有对应的真实文件有就直接返回没有就看看有没有对应的目录还不行就回退到index.html交给前端路由处理。这是Vue Router history模式能正常刷新的根本保障。如果去掉这一行你会遇到一个非常典型的症状从首页进入后点击跳转到某个路由一切正常但只要在这个路由上按F5刷新Nginx就会返回404。原因其实特别简单——刷新时浏览器会按当前地址向服务器发请求Nginx在磁盘上找不到/about这个文件就拒绝了。try_files就是把这个请求接住并重新送回index.html。3.2 多项目共存的Nginx目录与Location设计实际工作中一个Nginx服务器上放多个前端项目的情况太常见了。我服务的客户环境里经常一个服务器上同时跑着管理后台、H5门户、数据看板三个Vue应用。这种情况下有两种做法各自有适用场景。做法一不同子域名指向不同项目。配置上创建多个server块每个server独立监听80/443端口server_name不同互不干扰。做法二同一个域名下用子路径区分。这时核心就回到了base配置。假设/admin/对应管理后台/portal/对应H5门户那么server { listen 80; server_name yourdomain.com; location /admin/ { alias /var/www/admin/dist/; try_files $uri $uri/ /admin/index.html; } location /portal/ { alias /var/www/portal/dist/; try_files $uri $uri/ /portal/index.html; } location / { root /var/www/main/dist; try_files $uri $uri/ /index.html; } }这里最有讲究的是alias和root的区别——root会把你指定的路径拼在location匹配到的URI后面比如location /admin/root /var/www/admin/dist实际查找文件时会去找/var/www/admin/dist/admin/index.html目录结构里多了一层admin通常这不是你想要的。而alias是直接替换匹配部分alias /var/www/admin/dist/;会让/admin/index.html直接对应/var/www/admin/dist/index.html干净利落。另外值得留意的是每个子路径项目的try_files里回退目标必须是/admin/index.html这样带完整子路径的形式。因为当用户在/admin/login这个地址刷新时Nginx只匹配到/admin/这段如果回退到/index.html最终就会返回主站点的首页错得莫名其妙。3.3 SSL证书配置与HTTP强制跳转现在部署到公网的项目基本都要上HTTPS。把证书配置加进来后我的Server块通常是这样的server { listen 80; server_name yourdomain.com; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name yourdomain.com; ssl_certificate /etc/nginx/ssl/yourdomain.com.pem; ssl_certificate_key /etc/nginx/ssl/yourdomain.com.key; ssl_protocols TLSv1.2 TLSv1.3; root /var/www/vue-project/dist; index index.html; location /assets/ { expires 30d; add_header Cache-Control public, immutable; } location / { try_files $uri $uri/ /index.html; } }这里有一个小细节很多人不注意HTTP跳HTTPS的return 301会丢失掉原始请求中的协议信息。如果你的Vue项目里用window.location.protocol去做一些逻辑判断在跳转后拿到的都是https:一般没问题但如果遇到页面显示正常但所有接口请求都是HTTP的诡异情况就要检查是不是某个Nginx配置里有多重代理协议头在转跳之间丢掉了。4. 多级路由与History模式刷新404问题的根源很多同学把刷新404简单归因为没有配置try_files然后抄一段配置就完了。但搞懂这背后发生了什么对排查其他定位问题非常有帮助。4.1 Vue Router的History模式与Nginx的协作原理Vue Router的history模式是依赖HTML5 History API实现的。当你在页面内通过router.push(/user/123)跳转时Vue Router会拦截这次导航更新浏览器地址栏的URL但不会向服务器发起新的页面请求。所以单页应用内部的跳转看起来像传统页面跳转实际上并没有发生网络往返。问题出在用户手动刷新的时候。刷新这个行为绕开了Vue Router由浏览器直接向当前URL发起请求。如果你的Vue应用部署在Nginx上Nginx收到的是GET /user/123而磁盘上根本不存在/user/123这个文件。如果没有兜底方案Nginx就会返回404。try_files $uri $uri/ /index.html;做的就是找不到文件就返回index.html的操作。这个方案能够让刷新的时候成功加载应用入口然后路由初始化时根据当前URL匹配到/user/123对应的页面组件。这是一个服务器端兜底 客户端接管的分工协作。4.2 多层嵌套路径下的回退路径修正当项目嵌套层级比较深时一个容易出错的地方是路由使用了嵌套子路径比如/user/profile/edit。此时刷新Nginx的try_files仍然会把请求回退到/index.htmlVue Router再从中解析出完整的嵌套路由。所以常规配置对深层路由是通用的。但如果你用的是子路径部署比如/admin/就需要特别注意try_files的回退目标。我见过好几次这种报错管理后台部署在/admin/下Vue Router里配置的是类似/user/list这样的相对完整路径但刷新时Nginx把请求回退到了/index.html而不是/admin/index.html于是应用加载的是门户首页白屏或路由错乱。解决方式就是前面提到的try_files的回退目标要写成/admin/index.html。提示如果你的项目是子路径部署建议Vue Router的createWebHistory参数也传入对应的子路径比如createWebHistory(/admin/)这样路由生成URL时才会自动带上前缀避免手动拼接时少写或多写斜杠。5. 部署后的高频异常与排查链路Nginx配置好后项目一下子跑通的情况在真实环境里其实是少数。更多时候你会面对各种明明配置正确但就是不对的现象。这里我把这几年遇到的高频问题整理出来每一条都附上排查思路而不是直接甩结论。5.1 Vue打包后布局异常刷新页面样式错乱这是热搜词里出现过的典型案例。症状是首次访问首页一切正常点击路由跳转后也没问题但只要手动刷新页面就像没有加载CSS一样完全裸奔。这个问题的成因有三类我按出现频率排序第一类assetsDir配置问题。如果你在vite.config.js里把assetsDir改成了自定义目录但Nginx的location又是严格匹配/assets/的静态缓存规则就可能导致CSS文件请求被错误地指向其他目录。解决方式是保持assetsDir默认值或者同步调整Nginx的location。第二类CSS提取与异步加载的时序问题。Vite构建后路由懒加载组件的CSS被打包成独立的.css文件由对应chunk在运行时动态注入style标签。如果Nginx对这些CSS文件设置了过短的缓存影响不大但如果是代理层缓存了旧版本CSS的响应头就可能出现样式加载完成但内容不对的情况。排查时需要看浏览器Network面板对比CSS文件实际返回内容和文件本身是否一致。第三类图片资源路径错误。Vue组件中的img标签如果src写的是相对路径经过Vite构建后可能被解析成/assets/xxx.png这种绝对路径。一旦项目是子路径部署而没有设置好base这些图片请求就会落到域名根路径下找不到文件。表现就是整个页面文字排版是正常的但所有图片都裂了视觉上也会觉得布局异常。5.2 history模式下接口返回HTML而非JSON另一个很隐蔽的坑是接口请求返回了index.html的内容前端解析JSON时报错。这个现象通常发生在你把前端路由的try_files规则误写到了接口代理上。比如这样location /api/ { proxy_pass http://backend-server; try_files $uri /index.html; # 错误示例 }这个try_files会让Nginx在代理之前先去磁盘上找对应文件找不到就回退到index.html。于是所有/api/请求都拿到了前端入口页面的HTML。正确的做法是接口代理段不要写try_files直接proxy_pass即可location /api/ { proxy_pass http://backend-server; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }提示proxy_pass后的地址写法有讲究。如果proxy_pass http://backend-server;不带路径Nginx会把完整的原始请求URI原样转发如果加了路径比如http://backend-server/;则location匹配到的部分会被替换。这个细微差别在部署Vue项目并同时代理后端接口时非常容易踩中。5.3 Nginx反向代理与HTTP五元组信息的保留热搜词里有ip头部的五元组信息 nginx转发会带吗这个问题。所谓五元组一般指源IP、源端口、协议、目的IP、目的端口。在Nginx做反向代理时后端服务看到的是Nginx的IP和端口而不是真实客户端的IP和端口。因为Nginx本身就是TCP连接的客户端代理行为属于拆了一个连接再发起新连接五元组信息不可能原样透传。如果业务需要真实客户端IP正确做法是通过HTTP头传递上面配置里的X-Real-IP和X-Forwarded-For就是干这个的。后端服务解析这些请求头来获取真实来源。所以不要把保留五元组的方向搞错了。如果后端跑的是WebSocket服务还需额外配置以下头部否则ws://连接会被Nginx拦截location /ws/ { proxy_pass http://backend-server; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; }5.4 Vue项目播放m3u8直播流失败m3u8直播流播放是Vue项目里经常要做的一个功能部署到Nginx后常见的问题是本地开发一切正常线上死活播不了。开发环境正常是因为Vite的开发服务器对跨域不敏感浏览器请求外部m3u8地址时由Vite代理转发或直接允许跨域。生产环境的问题通常出在两个层面第一个是跨域。m3u8文件所在的CDN或流媒体服务器如果没有配置允许跨域Nginx部署的Vue页面在播放时会报CORS错误。这种问题在Nginx侧没有太多能做的除非你把播放请求也走一层Nginx代理通过proxy_hide_header Access-Control-Allow-Origin; add_header Access-Control-Allow-Origin *;这种方式绕开跨域限制。但这是权宜之计生产环境更推荐在流媒体服务器侧配置跨域。第二个是HLS的m3u8相对路径解析。m3u8文件里引用的TS分片地址有时候是相对路径比如../segment1.ts播放器会基于m3u8自身的URL做相对解析。如果你在Vue项目里对m3u8地址做了层层代理分片的实际请求路径可能与预期不符导致404。排查方法很简单打开浏览器Network面板找到第一个加载失败的TS分片请求看它的URL是什么然后对比m3u8文件内容里的分片路径就能定位是哪一层代理改写了路径。5.5 Nginx部署多个Web项目时端口与ServerName冲突服务器上同时跑了几个Web服务端口和域名容易搞混。我的建议是给每个项目单独准备一个server块即使端口相同也以server_name区分。比如server { listen 80; server_name admin.example.com; root /var/www/admin/dist; } server { listen 80; server_name h5.example.com; root /var/www/h5/dist; }这样配置的好处是Nginx会根据Host请求头自动分发互不干扰。如果同一个域名下要区分端口比如8080是管理后台8081是客户门户那就在listen后写端口同时保持server_name一致。排查端口冲突时有一个很实用的命令sudo netstat -tlnp | grep nginx可以快速看到Nginx当前监听了哪些端口。如果发现某个端口被占用优先确认是否还有其他Nginx配置段或者别的进程在监听而不是直接改Nginx端口了事。6. 一套可复用的实战脚本与部署流程配置讲了不少最后给出一套我实际项目里用着的部署流程。这个流程包含打包、传输、替换、校验四个环节每一步都有明确的命令和检查点。6.1 本地构建与检查清单执行打包前先对一遍这个检查清单vite.config.js里的base是否和目标部署路径一致.env.production里的环境变量是否和目标环境匹配Vue Router的createWebHistory路径前缀是否与base一致接口代理的后端地址是否已指向生产环境确认无误后执行npm run build打包完成后进入dist目录检查关键文件cd dist ls -la cat index.html | head -20正常情况下index.html里应该能看到类似/assets/index-xxxx.js的引用路径。检查无误后再上传服务器。6.2 服务器端发布脚本备份与原子切换我习惯在服务器上保留两个发布目录一个当前版本(current)、一个待发布版本(upcoming)。发布时先解压上传到upcoming验证没问题后再切换符号链接指向实现几乎不影响业务的原子发布。这里给出一个精简版的脚本#!/bin/bash # deploy.sh 使用示例./deploy.sh admin_20250415.tar.gz PROJECT_NAMEadmin APP_DIR/var/www/${PROJECT_NAME} RELEASE_DIR${APP_DIR}/releases/$(date %Y%m%d%H%M%S) mkdir -p ${RELEASE_DIR} tar -xzf $1 -C ${RELEASE_DIR} # 替换当前版本 ln -sfn ${RELEASE_DIR} ${APP_DIR}/current # 校验Nginx配置并重新加载 nginx -t nginx -s reload echo 部署完成${RELEASE_DIR}这个脚本的灵魂在于ln -sfn这一步——发布目录和当前版本之间是软链接关系切换只改变指针指向不移动大量文件。一旦新版本出了问题恢复旧版本也只需要改一下链接目标回滚成本极低。6.3 Nginx配置的备份与回滚改Nginx配置前务必先把当前配置备份一份。我见过太多人直接改nginx.conf然后nginx -s reload改出问题后想回滚却找不到原始配置只能凭记忆恢复。推荐的做法是sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.$(date %Y%m%d%H%M%S)改完后检查语法sudo nginx -t如果输出nginx: configuration file /etc/nginx/nginx.conf test is successful再执行reload。如果报错直接把备份文件复制回去继续排查。整个流程不会对线上服务造成长时间中断。6.4 部署后的验证清单服务上线后不要急着收工按这个顺序做一轮快速验证访问域名确认首页加载正常浏览器控制台无资源404手动进入一个深层路由比如/user/list/detail并刷新确认不出现404打开Network面板确认JS、CSS请求返回200且带有正确的缓存头执行登录操作确认接口请求能正确代理到后端响应正常如果是流媒体项目播放一段m3u8直播流确认分片能正常加载这些验证全部通过才算是真正部署完成。7. 回滚与迭代发布不是终点部署这件事真正考验人的不是第一次部署成功而是后续每次迭代时如何保证稳定。7.1 新版本发布后页面白屏的应急方案如果发布后发现页面白屏最快的应急措施就是回滚软链接。基于前面提到的current软链接模式执行以下操作# 查看当前和上一个release目录 ls -lt /var/www/admin/releases/ # 将软链接指回上一个目录 ln -sfn /var/www/admin/releases/上一个版本目录 /var/www/admin/current回滚后立即刷新页面。如果问题确实出在新版本这个操作能在1分钟内恢复服务。之后再慢慢排查构建日志找出白屏原因。7.2 版本目录清理与磁盘空间多发布几次之后releases目录会积累大量历史版本每个版本动辄几十MB甚至几百MB磁盘空间不知不觉就被吃满了。我的做法是保留最近5个版本更早的定期清理cd /var/www/admin/releases ls -t | tail -n 6 | xargs rm -rf这条命令会把除了最近5个版本以外的所有目录删掉。如果对保留数量有要求把tail -n 6里的数字改成对应的N1即可。7.3 从部署反推构建配置的迭代思路部署这件事做到后面你会发现很多Nginx配置其实是对构建配置的补充。比如Vite的代码分割策略、assetsInlineLimit的调整、路由懒加载的粒度都会影响Nginx层面的缓存策略和响应速度。我现在的习惯是每次部署遇到问题不急着在Nginx里打补丁而是先回到Vite配置里去想根源。比如出现页面首次加载很慢我会拆分包开启build.rollupOptions.output.manualChunks把Vue、Element Plus这类不常变的库单独打包成大chunk利用Nginx的长缓存后续版本更新时用户只需要下载业务代码的小chunk体积和流量都大幅下降。再比如图片资源过多导致首屏白屏时间长我会调大assetsInlineLimit的阈值让小图标直接内联为Base64减少HTTP请求数。这一切看起来是构建优化但最终都要配合Nginx的缓存头、gzip压缩、HTTP/2等能力才能发挥最大效果。前端工程化走到后面前后端的边界其实是模糊的——你写下的每一行Vite配置都会在Nginx层找到对应的回声。