
这个问题我太有发言权了。差不多每隔一段时间就能在技术群里看到有人发一张浏览器控制台截图满屏红的404配一句“本地好好的一部署就废了”然后底下清一色回复检查下publicPath。但真去问publicPath怎么设能一次说清楚的人并不多。我自己第一次遇到这个坑时也折腾了一整天从怀疑nginx配置、怀疑服务器路径权限到怀疑是不是打包工具坏了最后发现就是vue-cli里publicPath这个配置项搞的鬼。从那次之后我对静态资源路径这件事就特别敏感打包配置、部署方案、服务器转发规则会一起看。这篇文章就把我这些年在vue-cli项目里处理publicPath的经验做个系统总结从原理到配置从常见场景到疑难排查一次性讲透。如果你正在被“本地正常、上线404”折磨或者正准备把vue项目部署到子目录、CDN、云存储这类非根路径再或者你想搞明白vue-router的history模式和publicPath到底什么关系这篇文章就是为你准备的。1. publicPath到底是干什么的1.1 一个打包产物引发的血案——从404说起先还原一下最常见的翻车现场。你在项目根目录执行npm run build一切顺利dist目录生成里面的结构大概是这样的dist/ ├── index.html ├── css/ │ └── app.3a2b4c.css ├── js/ │ ├── app.3a2b4c.js │ ├── chunk-vendors.7d8e9f.js │ └── ... └── static/ └── img/logo.xxx.png把dist目录传到服务器上假设服务器地址是http://yourdomain.comnginx把站点根目录指向dist文件夹。打开首页咦页面白屏。按F12控制台一堆红色报错比如GET http://yourdomain.com/js/app.3a2b4c.js 404 (Not Found) GET http://yourdomain.com/css/app.3a2b4c.css 404 (Not Found)但是你看服务器上的文件js/app.3a2b4c.js明明就躺在那个位置。这就诡异了。这时候你把浏览器地址栏里的网址复制出来看一眼发现访问的并不是http://yourdomain.com/而是类似http://yourdomain.com/some/path/这样的二级路径。或者你根本是把项目部署在了http://yourdomain.com/demo/这个子目录下。于是真相浮出水面html文件被正确加载了但这个html内部引用资源的路径全部指向了域名根目录/js/...而不是当前所在的子目录/demo/js/...。这个“资源引用路径的根”就是publicPath控制的。1.2 publicPath的三种典型姿势绝对路径、相对路径、CDN路径publicPath本质上就是webpack的output.publicPathvue-cli把它提升到了顶层配置让你在vue.config.js里就能改。它管的是打包后的index.html内部所有静态资源的引用前缀。它有三种典型的取值姿势。第一种默认值/。这也是vue-cli在没有额外配置时用的值表示“所有资源从域名根目录开始找”。打包出来的index.html里资源引用长这样script src/js/app.3a2b4c.js/script link href/css/app.3a2b4c.css relstylesheet这个配置适合项目放在域名根目录的场景。但是只要你的项目被嵌套在任意一级子路径下这个配置必炸。第二种相对路径./。打包出来的资源引用变成script srcjs/app.3a2b4c.js/script link hrefcss/app.3a2b4c.css relstylesheet注意这里没有开头的斜杠。浏览器解析的时候会基于当前页面URL的相对路径去拼接所以无论你把dist文件夹放在哪个位置只要整个目录原封不动地搬过去资源路径就是对的。这个配置适合你不知道最终部署路径是什么或者部署路径经常变化的场景。但相对的如果用了history模式的路由这种配置会导致二级路由下刷新时资源路径算错这个问题后面细说。第三种完整的CDN绝对地址比如https://cdn.example.com/my-app/。打包出来的资源引用长这样script srchttps://cdn.example.com/my-app/js/app.3a2b4c.js/script很明显这是把静态资源托管到CDN或独立域名时用的让浏览器直接从CDN拉资源减轻源站压力。这三种模式就是publicPath最基础的全部样子。所有复杂的配置都是在这三种基础上根据部署环境动态切换。2. 不同部署场景下的publicPath配置2.1 部署在域名根目录最简单的情况如果你的nginx配置是server { listen 80; server_name yourdomain.com; root /var/www/my-project/dist; index index.html; }这种情况下直接用vue-cli的默认配置就行什么都不用改。因为所有资源都以/开头自动拼接成http://yourdomain.com/js/xxx.js完全匹配服务器上的文件路径。不过这里有个隐藏问题如果你用了vue-router的history模式还得加一条try_files规则否则用户在http://yourdomain.com/about这种二级路由下刷新nginx会去找/about这个文件找到到就404。一般这么配location / { try_files $uri $uri/ /index.html; }这也是老生常谈了这里提一下是因为后面所有的部署场景都要叠加这个规则但很多人只改了publicPath忘了改nginx。2.2 部署在子目录nginx子路径场景这是日常开发和测试环境里最容易遇到的场景。比如你在一台服务器上已经跑着一个主站http://yourdomain.com/新项目要用http://yourdomain.com/demo/访问dist文件也放在了服务器的/var/www/demo/dist目录下。nginx配置可能是location /demo/ { alias /var/www/demo/dist/; index index.html; try_files $uri $uri/ /demo/index.html; }这种情况下如果publicPath还是默认的/index.html加载没问题但里面的script src/js/app.js/script会跑到http://yourdomain.com/js/app.js自然就404了。正确答案是把publicPath设成子路径// vue.config.js module.exports { publicPath: /demo/ }这样打包出来的资源引用就变成script src/demo/js/app.3a2b4c.js/script浏览器会正确请求http://yourdomain.com/demo/js/app.3a2b4c.jsnginx通过alias规则映射到服务器磁盘上的对应文件一切正常。其实这里很多人分不清root和alias的差异。简单说root会把location后面的路径拼接在root目录后面比如root/var/www location/demo/最后去/var/www/demo/找文件而alias是直接把location路径替换为alias指定的路径alias/var/www/demo/dist/ 请求/demo/js/app.js最后找/var/www/demo/dist/js/app.js。理解了这个区别你配置子目录部署时就不会一头雾水。2.3 部署在CDN或对象存储带跨域前缀的完整URL再往后如果你做的是独立的前端项目打算把js、css、图片这些全部推送到CDN或者直接用OSS/S3这类对象存储来做静态托管那publicPath就需要设成完整的URL。// vue.config.js module.exports { publicPath: process.env.CDN_BASE_URL || / }在CI/CD的构建阶段注入不同环境的CDN_BASE_URL环境变量比如https://cdn.example.com/project-a/打包出来的资源就全部带上CDN前缀。这样index.html可以放在任意位置资源从CDN拉取加载速度和并发能力都能得到保障。这里有一个容易踩的坑如果你用了对象存储并且配置了CDN加速CDN回源时如果没配好资源路径会变成双重前缀比如https://cdn.example.com/project-a/project-a/js/app.js这就是base路径和CDN上的目录结构没对齐导致的。我的建议是CDN上的存储路径前缀和publicPath保持一致比如publicPath是https://cdn.example.com/project-a/那文件在存储桶里也应该放在project-a这个目录下。3. 用环境变量区分测试和生产配置3.1 vue.config.js中的动态配置方案实际项目里很少只有一个部署环境。本地开发、测试服、生产服、预发布可能路径都不一样。这时候硬编码publicPath就不合适了。vue-cli原生支持环境变量文件机制你可以创建.env # 所有环境都会加载的基础配置 .env.development # 仅开发模式加载 .env.production # 仅生产构建加载在文件里定义# .env.production VUE_APP_PUBLIC_PATH/demo/然后在vue.config.js里读取// vue.config.js module.exports { publicPath: process.env.VUE_APP_PUBLIC_PATH || / }注意了vue-cli对VUE_APP_开头的变量会做静态替换允许你在vue.config.js和业务代码里通过process.env.VUE_APP_PUBLIC_PATH访问。这样不同环境用不同.env文件配置不同的路径构建脚本不用改。如果你需要更灵活的控制还可以直接结合打包命令传参。在package.json里配置{ scripts: { build:test: vue-cli-service build --mode production --env-mode test, build:prod: vue-cli-service build --mode production } }然后在构建脚本里根据环境变量再覆盖// vue.config.js const publicPathMap { test: /test-app/, prod: /, cdn: https://cdn.example.com/app/ }; module.exports { publicPath: publicPathMap[process.env.DEPLOY_ENV] || / };在CI管道里执行DEPLOY_ENVtest npm run build:test就能灵活控制最终打出来的包用哪个publicPath。3.2 基于shell脚本或CI流的完整打包方案上面的环境变量虽然能区分但如果你和我一样经常同时维护好几个前端项目每个项目的部署根路径都不一样每次手改.env很容易出错。我自己的做法是写一个构建脚本统一管理。#!/bin/bash # build.sh DEPLOY_ENV$1 case $DEPLOY_ENV in test) export VUE_APP_PUBLIC_PATH/test/ ;; stage) export VUE_APP_PUBLIC_PATH/stage/ ;; prod) export VUE_APP_PUBLIC_PATH/ ;; cdn) export VUE_APP_PUBLIC_PATHhttps://cdn.example.com/app/ ;; *) echo Usage: ./build.sh [test|stage|prod|cdn] exit 1 ;; esac npm run build然后执行chmod x build.sh ./build.sh test这样打包前就把公共路径注入到环境变量vue.config.js统一读取。好处是部署路径和打包脚本放在一起换环境只需要改脚本里一个变量不用每个项目都去翻配置。如果你用GitLab CI或GitHub Actions也可以把同样的逻辑搬到CI配置里在构建阶段根据分支或tag设置环境变量。这样能达到的效果是代码合并到dev分支自动构建并部署到/dev/路径合并到main分支自动部署到根路径全程不需要人工干预。4. 实战vue django打包部署场景下的publicPath与跨域问题4.1 前后端分离部署的两种模式最近社区里vuedjango打包部署的讨论很多我发现很多人的问题其实包含了两个点一个是publicPath一个是跨域。这两个问题经常被混在一起但其实一个是前端配置的事一个是后端接口的事。我先梳理一下vuedjango常见的两种部署模式。第一种完全分离部署。前端dist部署到nginxdjango用uwsgi跑在8080端口前端通过axios直接请求http://api.domain.com/api/xxx或者请求http://domain.com:8080/api/xxx。这种情况下前端和后端是两个独立的域名或端口必然存在跨域需要django配CORS。第二种统一域名部署。nginx同时接收前端页面请求和后端接口请求通过location规则把/api/转发给uwsgi后端前端页面仍然通过域名根路径或子路径访问但是接口走相对路径/api/xxx由nginx做反向代理。这种情况下因为页面和接口同源不存在跨域问题。很多人说“vuedjango打包部署后无法跨域”我猜大概率是用的第一种模式然后django侧没配CORS或者配了CORS但配置有误。4.2 后端集成模式的publicPath设置如果你选择的是第二种模式而且django不是只提供API还要负责渲染部分页面把dist目录集成到django的模板里那publicPath的设置又有讲究。假设整个服务挂在http://yourdomain.com/下django的静态文件收集功能把你的前端资源从dist目录收集到STATIC_ROOT这里的publicPath设置为/static/就比较合理。// vue.config.js module.exports { publicPath: /static/, outputDir: dist/ }打包后资源引用script src/static/js/app.3a2b4c.js/script然后django settings里配置STATIC_URL /static/ STATICFILES_DIRS [ BASE_DIR / dist, ]运行python manage.py collectstatic时因为django会把dist目录下的静态文件收集到静态目录资源路径对齐。但这里有个容易犯的错如果你不加公众路径用了相对路径./打包出来的资源引用是js/app.xxx.js当django渲染模板时页面URL如果带了一级路径比如http://yourdomain.com/some/page/浏览器会把这个相对路径解析成http://yourdomain.com/some/page/js/app.xxx.js然后404。这就是为什么后端集成方案里publicPath强烈建议使用绝对路径也就是以/开头的完整路径而不要用相对路径。4.3 跨域问题的解决思路再说跨域的坑。如果你用的是完全分离模式即前端nginx和后端django(uwsgi)分别在不同域名或端口那么后端必须启用CORS。django侧最简单的方案是安装django-cors-headerspip install django-cors-headers在settings.py里INSTALLED_APPS [ ... corsheaders, ... ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ... ] CORS_ALLOWED_ORIGINS [ http://yourdomain.com, https://yourdomain.com, ]注意CorsMiddleware的位置文档建议放在所有能生成响应的中间件之前尤其是CommonMiddleware之前否则有时候跨域头会加不上。但如果你和我一样是折腾型选手其实更建议的做法是统一域名用nginx把前后端放在同一个origin下。比如server { listen 80; server_name yourdomain.com; # 前端页面 location / { root /var/www/dist; try_files $uri $uri/ /index.html; } # 后端API location /api/ { include uwsgi_params; uwsgi_pass 127.0.0.1:8080; } }这样前端请求/api/xxx和后端接口同源axios不需要baseURL指向别的域名也就没有跨域问题了。这个方案在部署层面的复杂度比“前端后端CORS”要低也更不容易出幺蛾子我个人比较推荐。有同学可能会问那我用第一种分离模式把接口域名写在axios的baseURL里不也一样能用吗能用但CORS配置、cookie跨域、预检请求这些都会带来额外的心智负担。你如果只是个人项目或者中小型应用统一域名部署的收益是最明显的。5. 常见问题与排查技巧实录5.1 404问题速查表我盘点了一下这些年帮人排查时遇到的典型问题整理成一张速查表遇到404先对照着查一下。现象深挖检查项常见原因解决方案页面白屏控制台报js/css 404看看URL路径前缀是什么publicPath默认/但项目部署在子目录publicPath改为子目录路径页面能开但图片/字体图标404检查img、font文件引用代码里用了相对路径引用静态资源用require或import引用资源或者统一走publicPath首页正常二级路由刷新后404看nginx日志和URL路径nginx没配try_files或publicPath用了相对路径加try_files规则history模式用绝对路径publicPath部署在CDN/OSS后html里资源路径少了前缀对比html和存储桶实际路径publicPath配置错误没有带CDN前缀publicPath设成完整CDN地址双击index.html本地打开白屏看file协议下资源请求绝对路径/在file协议下解析异常本地预览用serve等http服务或者构建时用./Django模板渲染后JS能加载但接口跨域看浏览器Network里接口响应头用了分离部署但django没配CORS配置django-cors-headers或改统一域名部署5.2 部署后发现css引用的字体图标跨域这个坑比较隐蔽。页面功能和样式都正常就是字体图标加载不出来控制台提示font from origin http://yourdomain.com has been blocked from loading by Cross-Origin Resource Sharing policy。这个问题的根源在于你的字体文件是外链的或者部署后字体路径变了浏览器在加载字体文件时做了跨域校验。排查思路是打开Network看字体文件的完整请求URL和响应头的Access-Control-Allow-Origin。一般处理办法就是让nginx在字体文件所在的location里加上location ~*\.(woff2?|eot|ttf|otf)$ { add_header Access-Control-Allow-Origin *; }但你别忘了这个问题的前置条件是字体文件路径本身要正确如果字体请求本身就是404那得先解决publicPath的问题。5.3 开发环境下publicPath怎么兼顾有人问publicPath改了本地开发怎么办其实开发模式下publicPath是另一个维度的逻辑。vue-cli开发服务器的publicPath默认也是/如果你在vue.config.js里把publicPath写成/demo/开发服务器也会自动把路径映射到http://localhost:8080/demo/下浏览器访问要加后缀。所以有些项目为了方便开发环境的publicPath和生产环境是分开的。// vue.config.js module.exports { publicPath: process.env.NODE_ENV production ? /demo/ // 生产打包用这个 : / // 本地开发保持根路径 }我在多个环境同时开发时都是这么处理的。开发环境保持简单的根路径避免HMR热更新出现路径错位生产环境再用动态环境变量控制不同部署目标。5.4 排查路径问题的三个高效命令最后分享三个排查路径相关的命令行技巧。第一个构建后直接检查产物里的路径。用grep搜一下index.html里的前缀grep -o src[^]* dist/index.html | head -20一眼就能看出资源引用是/js/xxx.js还是./js/xxx.js还是https://cdn.../js/xxx.js。第二个启动一个本地静态服务模拟服务器环境。不要再双击index.html了那个file协议和线上的http(s)差异太大。npx serve -s dist这个命令会把dist目录作为一个单页应用伺服起来访问路径和线上环境接近排查路径问题比打开file协议可靠太多。第三个nginx如果没起来或者配置不对先看error.logtail -f /var/log/nginx/error.log很多404其实nginx已经在日志里写明了原因比如文件不存在、目录权限不足、rewrite规则有误这些信息往往比浏览器端的报错更有用。6. 最后再分享一个小技巧publicPath这个问题表面上看是配置项的问题实际上是对“浏览器如何根据URL解析资源路径”这件事的理解。我个人这些年踩坑下来的体会是如果你对路径没谱先在服务器上建一个最简单HTML里面写个script src/js/test.js/script用不同的访问路径去访问这个HTML看浏览器实际请求的URL是什么一下就明白了。这个排查思路适用于任何静态资源路径问题不局限于vue-cli。还有用了vue-router的history模式千万别省nginx的try_files规则。publicPath解决的是“加载入口页后入口页里引用的资源去哪找”try_files解决的是“刷新某个路由时服务器该怎么响应”。两个问题互为补充少了哪个都会出幺蛾子。如果项目后续要扩展多级目录部署或者迁移到CDN建议从一开始就把publicPath的配置抽出来用环境变量控制不要散落在业务代码里。等真正要换部署方式的时候你要做的只是改一个变量重新构建一次而不是翻遍代码去改资源引用。