首页 / 资讯中心 / 文章详情

Vue项目部署到子目录白屏404?从构建到Nginx的完整配置指南

Vue项目部署到子目录白屏404?从构建到Nginx的完整配置指南 ★ FEATURED ARTICLE
把打包好的 Vue 项目直接丢到服务器某个子目录里访问http://域名/admin/结果一片白屏控制台里全是js、css的 404。这个问题我在不同项目里见过很多次每次排查到最后都会发现问题往往不是代码逻辑而是大家对 Vue 构建工具的“默认部署假设”没概念Vue 项目默认按域名根目录输出资源地址一旦你把产物放到子路径链路就断了。这篇文章不讲抽象理论只讲 Vue 项目部署到非根目录时从构建配置、路由配置到 Nginx 落地每一层到底要改什么、为什么改、改了之后怎么验证。适合正在接手子目录部署任务、或者已经遇到白屏 404 正准备排查的朋友。1. 一场白屏事故背后的核心矛盾默认根路径与子目录部署1.1 哪些场景不得不把 Vue 项目放到子路径很多人第一次遇到这个问题都是在“本来没打算这么干”的情况下。最常见的场景大概有这么几类一个域名下已经跑着官网、后台或第三方系统新项目只能挂到/admin、/portal这类目录下无法独占域名。内网或客户现场环境只给了你某个 Web 服务器的一个目录你只有文件读写权限改不了根路径的映射关系。一台服务器或一个容器里要部署多个前端应用为了省端口和域名资源约定用不同前缀区分入口。离线交付场景打包产物要放进别人的 Web 服务器目录里对方不允许动站点级配置。你可能觉得换个子域名就万事大吉但子域名涉及 DNS、证书、跨域等一系列额外成本尤其在内网交付环境里根本不现实。子路径部署是很多项目绕不开的一步与其到时候抱佛脚不如把这套配置逻辑一次弄明白。1.2 根目录部署与子路径部署的差异究竟在哪先看一个最典型的现象把dist目录整个丢到服务器/opt/web/admin/下浏览器访问http://域名/admin/index.html正常返回了但页面就是白的。打开 Network 面板一看index.html里引用的资源长这样script src/js/app.js/script link href/css/app.css relstylesheet问题就在这个/js/app.js。开头的/表示“域名根目录”浏览器会去请求http://域名/js/app.js而不是http://域名/admin/js/app.js。服务器根目录下自然没有这个文件于是 404页面白屏。第二类典型问题发生在 history 模式路由下。在页面上点来点去一切正常一旦用户按 F5 刷新页面浏览器向服务器发出的请求是http://域名/admin/detail。这个路径在磁盘上根本不存在如果没有特殊的回退规则Nginx 会直接返回 404。所以非根目录部署绝对不是“把文件扔进目录里”那么简单它牵涉三层构建产物的资源引用前缀、前端路由的基准路径、服务端的回退规则。这三层必须指向同一个前缀少配任何一个都会出问题。后面几节我按这个思路一层一层拆开讲。2. 构建层面的路径统一publicPath 是打包时的全局开关2.1 publicPath 到底改的是什么在 Vue CLI底层是 webpack项目里决定资源引用前缀的配置叫publicPath。生产构建时它直接决定index.html中静态资源 URL 的前缀写法。默认值为/打包产物里看到的资源引用就是script src/js/chunk-vendors.js/script script src/js/app.js/script link href/css/app.css relstylesheet当你把publicPath改成/admin/重新构建后同样的资源引用会变成script src/admin/js/chunk-vendors.js/script script src/admin/js/app.js/script link href/admin/css/app.css relstylesheet注意publicPath不改磁盘目录结构也不负责把文件复制到服务器子目录它只改变 HTML 和构建产物里“找资源时用的地址前缀”。文件放在哪、引用怎么写两件事要分开看。所谓“构建产物的基础公共路径”核心就是这一个值。2.2 Vue CLI 与 Vite 的配置差异Vue CLI 项目在vue.config.js里这样配module.exports { publicPath: /admin/, }如果项目里用到 Vue Router官方推荐的写法是const router new VueRouter({ mode: history, base: process.env.BASE_URL, routes })这里的process.env.BASE_URL就是 Vue CLI 根据publicPath自动推导出来的基础路径。你不需要手动去记两套值只要publicPath配了/admin/BASE_URL构建时就会解析成/admin/。Vite 项目的配置名不一样但思路一致。在vite.config.js里export default defineConfig({ base: /admin/, })Vue Router 4 的写法const router createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes, })import.meta.env.BASE_URL就是 Vite 根据base推导出来的环境变量。两套生态换汤不换药核心都是让构建工具知道“我的最终部署前缀是什么”。2.3./相对路径是天坑什么时候能用什么时候必炸网上不少老教程会建议publicPath: ./理由是“生成相对路径放到哪个子目录都能跑”。这个思路对了一半但很多人踩了另一半的坑。./会让打包产物这样引用资源script src./js/app.js/script浏览器拿到这个相对地址后会基于当前页面的 URL 去解析。关键就在这里相对路径解析依赖“当前页面地址”的目录位置而不是你脑海里“应用所在目录”。举两个例子页面地址是http://域名/admin/./js/app.js解析后是http://域名/admin/js/app.js正常。页面地址是http://域名/admin/detail由于末尾没有斜杠浏览器会把最后一段detail当作文件相对路径的基准目录仍然是/admin/所以解析结果还是http://域名/admin/js/app.js也正常。但页面地址如果是http://域名/admin/detail/末尾带斜杠浏览器就会认为当前目录是/admin/detail/./js/app.js解析成http://域名/admin/detail/js/app.js404。hash 模式下页面地址永远是http://域名/admin/#/detail真正的路径始终是/admin/所以相对路径相对安全。但 history 模式下深层路由刷新后是否带尾斜杠取决于你的路由设计和服务端重定向行为不可控风险比较大。所以我的建议很简单如果你的服务器能配置 Nginx能用绝对路径就老老实实用publicPath: /admin/不要图省事用./。./我只推荐给这类场景静态服务器不提供任何 rewrite 能力、必须配合 hash 路由、或者产物要留在本地用file://方式打开。提示把publicPath设置成./之后如果业务代码里还有src/static/logo.png这类绝对路径引用依然会 404。publicPath管不到你手写的绝对路径只能管构建工具自动生成的引用。3. 前端路由的 base 配置history 与 hash 的抉择3.1 hash 模式为什么是子路径部署的“默认保底”http://域名/admin/#/list这种带#的地址#后面的内容不会被发送到服务器。浏览器真正请求的永远只是http://域名/admin/服务端只要能把index.html返回给/admin/就够了不需要为具体路由做任何 rewrite。这意味着 hash 模式天然适合无法修改服务端规则的部署场景。即使只有一台不能改配置的静态文件服务器你也能把 Vue 项目放到/admin/下正常跑。很多后台管理系统选择 hash 模式一部分原因就在这里。它的短板也明显URL 不美观带一堆#部分第三方统计、埋点工具对 hash 路由的识别不友好SEO 基本放弃。但对于登录后才能访问的后台系统这些短板大多数时候可以接受。3.2 history 模式下 base 必须与部署路径完全对齐history 模式去掉#URL 更干净但对配置的要求高了一个等级。它的工作逻辑是服务端把所有前端路由都回退到index.htmlvue-router 再根据当前地址的pathname减去配置的 base 前缀得到真正的内部路由地址。举个例子。部署前缀是/admin/当前地址是http://域名/admin/settings/user如果路由 base 配置成/admin/vue-router 自动认为当前内部路由是/settings/user匹配到对应组件。如果忘了配 base默认值是/vue-router 会把整个/admin/settings/user当作路由去匹配找不到任何路由记录结果又变成白屏。所以 history 模式下构建工具的 publicPath、路由的 base、Nginx 的 location 前缀三者必须严格一致。常见的组合是这样磁盘部署目录/opt/web/admin访问前缀/admin/publicPath/base/admin/路由创建createWebHistory(/admin/)或 Vue Router 3 的base: /admin/3.3 嵌套路由在子路径下不需要逐个加前缀有一个容易理解错的地方配好 base 之后代码里的子路由不需要手写/admin前缀。例如 base 是/admin/前端路由可以正常写{ path: /detail/:id, component: Detail }访问效果是http://域名/admin/detail/12。vue-router 会自动把 base 拼到路由 path 前面你不需要在每个 path 里都写/admin/。我自己见过有人不理解这一点给每个路由 path 都硬拼/admin/结果 URL 变成/admin/admin/detail/12这种双前缀。记住base 是一个全局基准值业务路由永远按照“相对 base 的路径”去写。4. Nginx 侧的真实部署location、root 与 try_files 的搭配4.1 root 与 alias两种映射写法的区别Nginx 把/admin/请求映射到磁盘目录时有root和alias两种写法很多人在这里翻车。root写法是“把 location 前缀追加到 root 后面”location /admin/ { root /opt/web; # 实际磁盘路径是 /opt/web/admin/index.html }alias写法是“用 alias 路径替换掉 location 前缀”location /admin/ { alias /opt/web/admin/; # 实际磁盘路径是 /opt/web/admin/index.html }看起来结果一样但细节影响很大。alias对斜杠非常敏感location 写/admin/alias 就应该写/opt/web/admin/首尾斜杠尽量对齐如果只写/admin不带尾斜杠或者 alias 少了尾部斜杠访问/admin/xxx时拼接出来的路径可能多一个或少一个斜杠产生 403 或 404。我的习惯是子路径部署优先用 root 写法因为逻辑更直白、斜杠问题少location /admin/ { root /opt/web; index index.html; }只要磁盘目录名和 location 前缀一致root 就不太容易出错。当然你也可以用 alias只是要记住斜杠规范。4.2 try_files 兜底与刷新 404 的根因history 模式下刷新/admin/detail时Nginx 会先检查磁盘上是否存在/opt/web/admin/detail这个文件或目录发现不存在默认返回 404。为了让这种“不存在的路径”全部落到index.html需要加一行try_fileslocation /admin/ { root /opt/web; try_files $uri $uri/ /admin/index.html; }try_files的三个参数依次解释先把$uri当作磁盘文件找找不到再当作目录找还是找不到就内部重定向到/admin/index.html。浏览器拿到index.html后vue-router 解析地址渲染对应页面。这里有一个非常隐蔽的坑fallback 必须写成/admin/index.html不能写成/index.html。我实际排查过的问题里有人把 fallback 写成/index.html首屏访问/admin/没问题但一访问深层路由刷新就 404。原因很简单/index.html这个内部重定向脱离了你期望的/admin/前缀会去找根目录下的index.html而根目录下并没有这个文件。注意try_files的 fallback 路径要写成“应用实际部署路径 /index.html”不是简单写个/index.html就算完事。4.3 API 代理也是“非根目录部署”的一部分静态资源解决后接口路径同样需要过一遍。开发环境里axios 的baseURL经常写成/api。部署到/admin/下后如果后端坚持要求请求携带/admin/api前缀Nginx 可以这样转location /admin/api/ { proxy_pass http://127.0.0.1:8080/api/; }注意proxy_pass后面带了 URI/api/这表示匹配到的 location 前缀/admin/api会被替换成/api。也就是说浏览器请求/admin/api/login转发给后端的是/api/login。这是前后端路径解耦的做法。如果你不小心把proxy_pass写成不带 URI 的形态location /admin/api/ { proxy_pass http://127.0.0.1:8080; }那么转发时保留完整路径后端收到的是/admin/api/login。后端如果只注册了/api/login接口就会 404。这个规则很基础但在子路径部署时特别容易踩因为你在浏览器里看到的请求带/admin误以为后端也应该带/admin其实两层完全可以通过 Nginx 做替换。5. 实际部署中踩过的坑与排查链路5.1 白屏排查先别怀疑配置打开 Network 看 index.html 引用了谁遇到白屏不要急着改配置。我推荐一条固定的排查链路打开浏览器开发者工具的 Network 面板刷新页面先确认index.html本身返回 200。点开index.html的响应体看里面的script和link标签的src前缀是什么。如果请求发出的地址是/js/app.js而实际应用在/admin/说明publicPath没配或者配了没生效。如果请求变成/admin//js/app.js说明publicPath的结尾斜杠和路由基础路径之间有重复通常是多写或者少写了一个/。很多时候我解决白屏根本不用碰路由和 Nginx问题就出在构建产物里的资源引用前缀不正确。先看 Network往往比乱猜配置快得多。5.2 资源 404 与字体图标裂开路径问题的三个主要来源如果页面框架能出来但样式乱、图片裂开、字体图标全不显示通常是三个来源之一业务代码或 CSS 里写死了绝对路径比如background: url(/assets/bg.png)。这种路径不受publicPath管控需要改成相对路径或统一加前缀。静态资源放在public目录里引用时写成src/static/logo.png。部署到子路径后这里也要跟着加前缀。Vue CLI 推荐用process.env.BASE_URL拼接Vite 环境用import.meta.env.BASE_URL。字体文件打包自 CSS 内部多数情况下构建工具会自动跟随publicPath但如果部署后刷新带尾斜杠的路由可能会出现资源基准错误。我处理这类问题的方法很笨但有效把页面渲染后所有 404 的请求地址逐一列出来对照它们的路径前缀哪一级前缀不对就回到对应配置层去改。最常漏掉的是public目录资源因为很多人以为放进public就能一劳永逸。5.3 多级子路径与同域多应用的扩展经验上面讲的是/admin/这一层的部署。如果遇到/a/b/project/这种多级路径原理不变publicPath、路由 base、Nginx location 全部换成/a/b/project/即可。再往前一步一个域名下部署两个 Vue 应用/app1/和/app2/我给每个应用分配独立的 Nginx location、独立的publicPath和 router base。配置时要额外留意如果/下还有默认站点try_files的 fallback 不要把请求甩到其他应用的 location 里。建议每个应用的前缀相互独立Nginx location 匹配顺序清晰不要出现一个 location 同时覆盖两个应用前缀的情况。还有一种进阶情况静态资源放到 CDN 或独立静态域名。这时publicPath可以直接设置成完整的 URL例如module.exports { publicPath: https://static.example.com/admin/, }页面会从 CDN 拉取静态资源但路由 base 仍然是/admin/不能也跟着改成 CDN 地址。静态资源和前端路由是两个维度的问题捆绑在一起改容易混乱。6. 部署前的检查清单与一个长期建议把该讲的原理都过完之后送你一份我每次部署前后都会对照的检查清单。照着这个顺序检查非根目录部署的 90% 问题都能在发布前拦住明确最终访问前缀例如/admin/。构建配置里publicPathVue CLI或baseVite与访问前缀一致。如果是 history 模式路由 base 与访问前缀一致如果是 hash 模式至少保持publicPath一致路由 base 不强求但建议也保持一致。检查public目录下的资源引用确认都通过环境变量拼了基础前缀。Nginx location 使用/admin/保持一致root 或 alias 的磁盘路径确认无误。try_files的 fallback 写成/admin/index.html不是/index.html。API 代理 location 的proxy_pass确认清楚是否需要带 URI。上线后分三步验证访问/admin/首屏正常访问/admin/detail后刷新正常访问一个更深的嵌套路由再刷新确认不 404。还有一个长期建议如果团队经常要部署到不同前缀的环境不要把publicPath写死在配置文件里。我会通过环境变量动态注入例如 Vue CLI 里module.exports { publicPath: process.env.VUE_APP_PUBLIC_PATH || /, }构建时给不同环境传不同前缀分别产出/admin/和/portal/等版本的包同一份代码不用改动就能满足多套部署环境。我在实际部署中就是用这种思路配合 CI 流水线传入前缀变量一套构建任务解决多个子路径入口省掉了大量重复手工改配置的时间。子路径部署这件事说白了三层对齐构建出正确引用、路由认准基准路径、服务端做好回退。把这三条线理顺以后不管部署到几级子目录你都可以在十分钟内搞定。
阅读完成 · 觉得有帮助?
咨询建站