
简介一份面向 Vue 项目部署与运维人员的 PDF 技术笔记专门解决 Vue Router 开启 History 模式后发布到 Nginx刷新非根路径返回 404 的经典问题。内容从项目打包后的 index.html 入手逐步排查资源引用路径并给出两处关键修改在 webpack.config.js 中将 output.publicPath 调整为绝对路径以及为 Nginx 增加 try_files $uri $uri/ /index.html 的回退规则使未知路由统一交给前端入口文件处理。同时补充了修改配置后重启服务、跨域场景下 CORS/代理设置等注意事项适合遇到类似部署问题的前端开发、运维人员参考。资源为 1 个 PDF 文件体积仅 70KB便于快速查阅已有 6065 人学习。对于刚接触 History 模式或 Nginx 部署的读者来说这份资料提供了完整且可操作的排错思路和配置文件示例。1. Vue History 模式 404 的根源服务器不懂前端的“假路由”刚上线的 Vue 项目入口页点得通一刷新就 404 白屏白天好好的同事部署到预发环境就原形毕露——这种问题在 vue-router 的 history 模式下几乎必现排查起来却常常被归咎于“打包有问题”。其实代码没错错在服务器还停留在静态站点的思维SPA 打包出来只有一个 index.htmlhistory 模式下的 URL 只是浏览器地址栏里的“假路径”服务器按物理文件去找它自然找不到。这一篇就把 history 模式 404 的根因、Nginx 与宝塔的修法、多服务器对照配置、子目录部署的联动坑以及最后的验证与兜底一次性理清。适合正在部署 Vue 项目、被刷新 404 困扰的前端以及负责服务器配置的运维一起看。2. Nginx 下修复 History 模式 404 的标准配置2.1 先按响应码区分“路由 404”与“资源 404”修之前先定位这个 404 到底是“页面路由 404”还是“静态资源 404”。在浏览器 Network 面板刷新一下页面看最上面那条 document 请求的状态码——如果它返回 404而 JS/CSS 请求都正常那就是服务器没把未知路径交给 Vue Router如果 html 返回 200 但页面白屏接下来多半是若干 js/css 请求 404属于资源路径错位根因通常在后文的 base 配置不是 try_files 能解决的。命令行环境下用 curl 看得更直观# 模拟浏览器直接访问前端路由不跟随重定向 curl -s -o /dev/null -w %{http_code}\n http://127.0.0.1/setting/profile # 修复前预期输出 404修复后预期输出 200-s是静默模式不打印进度条-o /dev/null丢弃响应体只看状态码-w把 HTTP 状态码按格式输出。这样一条命令就能判断“document 请求是否 404”比打开浏览器按 F12 省事得多。如果这里返回 404基本可以确认是服务器没有做 SPA 回退直接进 2.2。2.2 用 try_files 把未知路径重写到 index.htmlNginx 下的标准解法是改站点配置让所有不存在的路径都回退到 index.html。找到server {}块里的location / {}改成下面这样server { listen 80; server_name example.com; root /usr/share/nginx/html; index index.html; location / { # 按顺序查找真实文件 - 真实目录 - 都找不到就内部重定向到 index.html try_files $uri $uri/ /index.html; } }try_files的三个参数从左到右依次尝试$uri直接按请求路径找文件命中则直接返回$uri/按目录找命中则返回目录下的 index 文件前两个都落空时最后一个参数是内部重定向目标这里写成/index.html表示把请求交给 Vue Router 去处理。注意这里是“内部重定向”浏览器地址栏 URL 不变返回的却是 index.html 的内容状态码是 200页面由前端路由接管渲染。提示如果 Nginx 配置里原本写了root指向其他目录保留你自己的那一行只替换location /里的try_files不要照抄上面的 root 路径。还有一类误伤要提前防住图片、字体、片段的 JS/CSS 如果也不存在会被一并重写回 index.html导致本该 404 的资源返回 200前端报解析错误。我一般会加一条正则 location 专门处理静态资源# 静态资源存在就返回不存在直接 404不参与 SPA 回退 location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ { expires 30d; try_files $uri 404; }正则 location 的优先级高于普通前缀 location因此这条规则会先拦截静态资源请求expires 30d给资源加 30 天缓存对打包后带 hash 的文件是安全的但如果 index.html 被误缓存就麻烦了所以不要在根 location 里加长缓存。2.3 宝塔面板可视化站点如何落地这段配置用宝塔部署 Vue 项目时很多人会把 dist 内容丢进/www/wwwroot/你的域名/就以为完事了刷新 404 后不知从哪下手。宝塔本质上就是 Nginx只是把配置文件藏到了面板里。正确步骤分三步。第一步在“网站”里找到对应站点点“设置”切到“配置文件”标签页第二步在server {}里找到location / {}把里面原有的内容替换为location / { root /www/wwwroot/你的站点目录; index index.php index.html index.htm; try_files $uri $uri/ /index.html; }第三步保存配置。宝塔保存时会自动执行nginx -t检查语法报错会提示具体行号排错方便如果面板里的伪静态规则和这个 location 冲突先注释掉伪静态再保存。保存后直接刷新页面document 请求的 404 就会变成 200。2.4 同时代理后端 API避免把接口也重写掉凡是前后端分离的项目前端部署的同一个 Nginx 上通常还要反代后端接口热词里常见的“springboot vue 前后端分离”就是这个场景。如果你把接口请求也放在根路径下会被try_files一股脑重写回 index.html前端拿到一坨 HTML 去解析 JSON报错信息五花八门。常规做法是给 API 单独开一条前缀 location并且必须放在location /之前# /api 开头的请求全部转发给后端 Spring Boot不参与前端回退 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; }proxy_pass http://127.0.0.1:8080;结尾不带斜杠会把完整的/api/xxx路径原样传给后端如果后端接口本身没有/api前缀就要在 proxy_pass 里加斜杠写成http://127.0.0.1:8080/把前缀剥掉再转发。改了这段之后如果页面还是 404但 Network 里看到的是类似{detail:not found}的 JSON 响应说明请求已经到达后端是后端路由不存在或路径拼错跟前端 history 模式无关了。3. Apache、IIS 与 Node 服务下的 History 模式 404 对应解法3.1 Apache 用 FallbackResource 一行搞定Apache 上最常见的做法是用.htaccess做 URL 重写但更简洁的方式其实是FallbackResource。在站点根目录新建或编辑.htaccess# 所有找不到文件和目录的请求统一交给 index.html 处理 FallbackResource /index.html一行配置就把回退逻辑说清了请求对应的物理文件不存在时Apache 直接内部转发给/index.html。需要显式声明DirectoryIndex index.html吗如果站点根目录下自带 index.html一般不用多写但为了保证行为一致可以加上。有些老主机默认禁用.htaccess需要先确认AllowOverride All已开启。如果服务器上同时还有别的重写规则用传统写法更稳妥逻辑可控性也更强IfModule mod_rewrite.c RewriteEngine On RewriteBase / RewriteRule ^index\.html$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.html [L] /IfModuleRewriteCond %{REQUEST_FILENAME} !-f表示“请求的不是已存在文件”!-d表示“请求的不是已存在目录”两个条件同时成立才执行最后的RewriteRule . /index.html [L]。这样做的好处是静态资源仍然按物理路径返回只有真正的“假路由”会被重写。3.2 IIS 用 URL Rewrite 模块实现同款回退Windows Server 上部署 Vue 项目IIS 的默认行为同样是访问不存在的路径直接 404。需要先确认服务器装了 URL Rewrite 扩展然后在站点根目录放一份web.configsystem.webServer rewrite rules rule nameVue History Mode stopProcessingtrue match url.* / conditions logicalGroupingMatchAll add input{REQUEST_FILENAME} matchTypeIsFile negatetrue / add input{REQUEST_FILENAME} matchTypeIsDirectory negatetrue / /conditions action typeRewrite url/index.html / /rule /rules /rewrite /system.webServer这段 XML 的逻辑与 Apache 的写法几乎一一对应matchTypeIsFile negatetrue等价于“不是文件”negatetrue等价于“不是目录”两个条件用logicalGroupingMatchAll做与运算最终动作是把请求重写到/index.html。注意web.config对 XML 格式敏感注释要放在configuration节点内否则 IIS 直接报 500。3.3 Node 服务用 connect-history-api-fallback 中间件用 Express 托管打包产物时可以在静态目录之前挂一个回退中间件常见的 npm 包是connect-history-api-fallbackconst express require(express); const history require(connect-history-api-fallback); const app express(); // 必须放在 express.static 之前注册 app.use(history({ rewrites: [ // /api 开头的请求保持原样让后续中间件接管 { from: /^\/api\/.*$/, to: (context) context.parsedUrl.path } ] })); app.use(express.static(dist)); app.listen(3000);app.use(history())会拦截所有请求把不存在的路径重写到/index.htmlrewrites数组用来定义例外规则命中规则的请求不重写。这里的from是正则to可以是一个函数返回原始 path 即为放行这样/api请求能继续往下走到接口路由。中间件位置很关键写在express.static前面才会先处理回退逻辑写反了静态目录找不到文件的请求会被 Express 直接 404。3.4 Vite 开发环境下为什么很少遇到这个 404平时在本地跑npm run devVite 的 dev server 默认就开启了 SPA fallback访问不存在的路径会自动回退到 index.html所以开发期几乎感觉不到这个问题。真正容易踩的反而是vite preview预览打包产物时如果项目部署在子路径且没有配置base预览服务器按根路径托管文件一刷新还是会 404。开发环境的排查重点不在回退而在server.proxy是否把/api代理到了正确的后端端口。比如 Vue 项目后端跑在 8080前端开发端口是 5173vite.config.js里写proxy后浏览器 Network 里/api请求 404先看代理目标端口和 context-path 配没配错。服务环境配置位置核心指令适用情况Nginx站点 conf 的 location /try_files $uri $uri/ /index.html;最常见性能好Apache.htaccess 或 vhostFallbackResource /index.html老牌虚机改配置最轻量IISweb.config URL Rewrite 扩展action typeRewrite url/index.html/Windows Server 部署ExpressNode 入口文件connect-history-api-fallback前后端同 Node 服务托管Vite dev/previewvite.config / vite preview默认支持 SPA 回退本地调试不做生产方案4. 子目录部署的 History 模式 404 与资源路径联动问题4.1 子路径部署时404 不再只是 try_files 的事很多企业的 Vue 项目不是独占一个域名而是部署在/app/或/console/这类子路径下。这时 404 会分两种表现直接访问example.com/app/setting刷新服务器找不到物理路径404 由try_files处理但还有一种是首页能开点路由跳转也正常刷新后样式全乱、白屏打开 Network 一看一堆 js/css 请求 404——这已经不是回退规则的问题而是打包时资源路径写错了也就是常说的“vue 打包后布局异常”的典型来源。4.2 Vite 项目用 base 对齐路由与资源路径Vite 构建的项目子目录部署要同时改两处构建时的base和 Vue Router 的historybase。先说构建配置// vite.config.js import { defineConfig, loadEnv } from vite; import vue from vitejs/plugin-vue; export default defineConfig(({ mode }) { // 读取 .env.production 里的 VITE_PUBLIC_PATH默认根路径 const env loadEnv(mode, process.cwd(), ); return { base: env.VITE_PUBLIC_PATH || /, plugins: [vue()] }; });对应的.env.production文件里写VITE_PUBLIC_PATH/app/然后 Vue Router 的创建方式改为// router/index.js import { createRouter, createWebHistory } from vue-router; const router createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes });import.meta.env.BASE_URL是 Vite 根据base自动注入的全局变量配置了/app/之后打包出的 index.html 里引用资源会带上前缀Router 的 base 也同步对齐。这里最容易犯的错是只改了 Router 的 base没改 Vite 的 base结果路由刷新 404 解决了但首页加载的 JS/CSS 依然按根路径请求还是 404反过来只改 base 不改 Router路由前缀对不上跳转一会儿就白屏。4.3 Webpack/Vue CLI 项目的 publicPath 配置还在用 Vue CLI 的旧项目对应的是publicPath写法如下// vue.config.js module.exports { publicPath: process.env.NODE_ENV production ? /app/ : / };同时 Router 侧要显式传 base// router/index.js const router new VueRouter({ mode: history, base: process.env.NODE_ENV production ? /app/ : /, routes });publicPath管静态资源base管路由前缀两者必须改到同一个值。漏改publicPath的表现为刷新不 404但 CSS 错乱、图片图标全挂漏改base的表现为地址栏输入/app/setting正常但点击站内链接后 URL 变成/setting再次刷新回归 404。排查时对着打包出来的 index.html 看一眼资源引用路径问题立刻清晰。4.4 子目录 Nginx 的成组配置示例最后把子目录场景的 Nginx 配置串起来。假设构建产物放在/var/www/html/app/访问入口是example.com/app/server { listen 80; server_name example.com; root /var/www/html; index index.html; location /app/ { # 找不到文件时回退到 app 目录下的 index.html try_files $uri $uri/ /app/index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; } }关键点在于最后一个重写目标写的是/app/index.html不是/index.html。因为请求 URL 是/app/setting资源根路径在/var/www/html/app/下写成根 index.html 会让 Vue 实例加载的是错误页面。如果用的是alias指向/var/www/html/app/则try_files的回退目标也要跟着调整root和alias混用是子目录部署里报错率最高的两个写法。提示改完 Nginx 后记得nginx -t nginx -s reload不要只改配置不重载内网环境里常有改完忘了 reload 然后白等半小时的情况。5. 验证与兜底把 History 模式 404 治理得更彻底5.1 用 curl 做部署后的自动化验证每次发版后我会把下面几条命令写进部署脚本的 smoke test避免“验证时人不在服务器前”的盲区# 1. 直接访问前端路由期望 200 curl -s -o /dev/null -w %{http_code}\n https://example.com/setting/profile # 2. 访问一个不存在的静态资源期望 404 curl -s -o /dev/null -w %{http_code}\n https://example.com/assets/not-exist.js # 3. 检查首页确实指向我们的 SPA curl -s https://example.com/setting/profile | grep -o title项目名/title第一条验证路由回退第二条验证资源 404 没有被误重写第三条确认返回的确实是 index.html 内容而不是错误页。-w %{http_code}会把状态码单独打出来grep 标题是快速确认页面内容的土办法但它非常可靠能一眼看出服务器是不是把请求重写到了别的地方。5.2 Vue Router 的 404 兜底路由服务器层的回退解决的是“刷新 404”但用户手滑输入了一个不存在的路由比如/setting/profile/xxx这时页面加载成功了Vue Router 却没有任何匹配项表现为白屏。兜底做法是在路由表最后加一条捕获所有路径的规则// router/index.js { path: /:pathMatch(.*)*, name: NotFound, component: () import(/views/NotFound.vue) }:pathMatch(.*)*是 Vue Router 4 的语法用来匹配所有未注册路径把控制权交给 NotFound 页面组件。注意这条路由必须放在routes数组最后否则会抢占所有路由的匹配。它兜的是“前端路由不匹配”的 404而不是“服务器文件不存在”的 404两者是不同层的事不能混为一谈。5.3 懒加载 chunk 失败时的降级处理还有一个和 404 相关的隐蔽场景页面用() import()做路由懒加载部署新版本后旧页面用户停留了一段时间再点击跳转请求的 chunk 文件名带旧 hash而服务器上文件已经被覆盖于是报Loading chunk failed。这个错误不长在页面刷新而长在路由跳转瞬间。我会在 router 实例上挂一个全局错误处理// router/index.js router.onError((error) { if (/Loading chunk .* failed/.test(error.message)) { // 通常是部署后 chunk hash 变化重载一次拿最新版本 window.location.reload(); } });error.message匹配到Loading chunk ... failed时直接刷新页面让浏览器重新请求最新的 index.html 和对应 chunk不要对没匹配到的错误做无条件重载避免接口异常时陷入刷新死循环。这条配合 history 模式的服务器回退配置基本能把部署场景下能想到的 404 都堵住了剩下的就是在前端路由表里把/:pathMatch(.*)*的兜底页面做得友好一点让用户知道是自己输入错了而不是站点挂了。本文还有配套的精品资源点击获取