1. 为什么 config 目录值得单独拎出来讲
很多人做 Vue 项目,src目录翻得滚瓜烂熟,组件、路由、状态管理写得飞起,但一提到根目录下的config文件夹,脑子里就三个字:没动过。这其实特别正常,因为脚手架生成的项目里,config目录往往就那么两三个文件,平时开发也用不着碰它。但问题在于,一旦项目要区分开发、测试、预发布、生产多套环境,或者要调整构建产物的路径、端口、代理规则,你就绕不开这个目录了。
我见过不少项目,环境变量直接硬编码在代码里,切换环境靠手动改axios的baseURL,上线前还得挨个文件搜一遍有没有漏改的地址。这种做法在小项目里勉强能跑,但只要团队超过三个人、环境超过两套,出事故几乎是必然的。config目录存在的意义,就是把这些跟"环境"和"构建"相关的东西从业务代码里抽出来,集中管理。
这篇文章我打算把config目录彻底拆开讲一遍。从它到底管什么、每个文件的作用、配置项背后的原理,到多环境怎么落地、代理怎么配、踩过哪些坑,都会覆盖到。不管你是刚接触 Vue 项目的新手,还是做了几年但一直没系统梳理过构建配置的老手,应该都能从里面找到点有用的东西。需要说明的是,不同脚手架版本对config目录的组织方式不完全一样,下面讲的内容以常见的 Vue CLI 2.x 风格项目结构为主,同时会说明在 Vue CLI 3+ 里这些配置迁移到了哪里,方便你对照自己手上的项目。
2. config 目录到底管什么:整体设计与思路拆解
2.1 config 目录的定位与边界
先给config目录一个清晰的定位:它是构建工具和开发服务器的配置中心,不是业务代码的存放地。这里面放的东西,基本都跟"项目怎么跑起来、怎么打包出去"有关,跟"页面上显示什么"没关系。
具体来说,config目录通常承担这几类职责:
- 环境区分:开发环境用哪个接口地址、生产环境用哪个,通过配置文件区分开。
- 开发服务器配置:本地起服务时的端口号、是否自动打开浏览器、热更新行为等。
- 代理配置:解决本地开发时的跨域问题,把请求转发到后端服务。
- 构建输出配置:打包产物放哪个目录、静态资源路径怎么拼、要不要生成 source map。
- 构建行为微调:比如是否压缩、是否启用 gzip、打包分析报告的开关等。
理解了这个边界,你就知道什么该往config里放,什么不该。比如"用户登录后跳转到哪个页面"这种业务逻辑,绝对不能塞进config;而"打包后资源引用的公共路径"这种跟部署环境强相关的东西,就应该放在这里。
2.2 为什么要把配置独立成目录
有人会问,这些配置直接写在webpack.config.js或者vue.config.js里不行吗,为什么非要单独搞个config目录?
核心原因是可维护性和环境隔离。设想一个场景:你的项目要部署到三个环境,开发环境接口是http://localhost:8080,测试环境是http://test.xxx.com,生产环境是https://api.xxx.com。如果这些地址散落在各个文件里,每次切换环境就是一场灾难。而把它们集中到config目录下的不同文件里,通过一个入口文件根据当前环境变量去加载对应的配置,切换环境只需要改一个启动命令,这就是独立目录带来的价值。
另一个原因是团队协作。当配置集中且结构清晰时,新加入的同事能快速看懂项目在不同环境下是怎么跑的,而不是靠口口相传"记得把那个地址改一下"。配置即文档,这句话在config目录上体现得特别明显。
2.3 典型目录结构长什么样
一个典型的 Vue CLI 2.x 项目的config目录,结构大致是这样的:
config/ ├── index.js # 配置入口,根据环境变量合并配置 ├── dev.env.js # 开发环境变量 ├── prod.env.js # 生产环境变量 └── test.env.js # 测试环境变量(可选)而在 Vue CLI 3+ 的项目里,这套结构被大幅简化了,config目录基本消失,取而代之的是根目录下的.env系列文件和vue.config.js。.env文件负责环境变量,vue.config.js负责构建和开发服务器配置。所以如果你用的是较新的脚手架,看到没有config目录不要慌,它的职责被拆分到了别的地方。
下面这张表可以帮你快速对照两种组织方式:
| 职责 | Vue CLI 2.x 位置 | Vue CLI 3+ 位置 |
|---|---|---|
| 环境变量定义 | config/dev.env.js等 | .env.development等 |
| 配置合并入口 | config/index.js | vue.config.js |
| 开发服务器配置 | config/index.js中dev段 | vue.config.js中devServer段 |
| 构建输出配置 | config/index.js中build段 | vue.config.js中outputDir等 |
搞清楚这个对应关系,你就能在两种项目结构之间自由切换,不会因为换了脚手架就手足无措。
3. 核心文件逐个拆解与实操要点
3.1 index.js:配置的总调度台
config/index.js是整个config目录的核心,它做两件事:一是定义开发和构建两套配置,二是根据当前环境把环境变量注入进去。
一个简化后的index.js结构大概是这样:
const path = require('path') module.exports = { dev: { assetsSubDirectory: 'static', assetsPublicPath: '/', host: 'localhost', port: 8080, autoOpenBrowser: false, proxyTable: {}, cssSourceMap: false }, build: { index: path.resolve(__dirname, '../dist/index.html'), assetsRoot: path.resolve(__dirname, '../dist'), assetsSubDirectory: 'static', assetsPublicPath: '/', productionSourceMap: true, productionGzip: false, bundleAnalyzerReport: false } }这里有几个关键点值得展开说。
assetsSubDirectory和assetsPublicPath的区别。前者是打包后静态资源放在哪个子目录里,默认是static,也就是说 JS、CSS、图片会进到dist/static/下面。后者是资源引用的公共路径,决定了 HTML 里引用资源时前面拼什么前缀。如果你的项目部署在域名根目录下,assetsPublicPath用/就行;如果部署在子路径下,比如https://xxx.com/myapp/,那就要改成/myapp/,否则资源会 404。这个坑我踩过不止一次,打包出来本地打开正常,一部署就白屏,十有八九是这里没配对。
productionSourceMap的取舍。开启后打包会生成.map文件,方便线上报错时定位到源码。但代价是打包体积变大,而且源码有暴露风险。我的建议是:内部系统可以开着,对外产品关掉,或者用错误监控平台的上报功能替代。
productionGzip和bundleAnalyzerReport。前者开启后会对打包产物做 gzip 压缩,需要配合服务端的 gzip 支持;后者开启后打包完会自动打开一个可视化报告,告诉你哪个包体积最大,排查体积问题时特别好用。
3.2 dev.env.js 与 prod.env.js:环境变量的来源
这两个文件的作用是定义不同环境下的变量,最终会通过webpack.DefinePlugin注入到代码里。典型内容如下:
// dev.env.js module.exports = { NODE_ENV: '"development"', API_BASE_URL: '"http://localhost:8080"' }注意这里的值为什么要用两层引号。因为DefinePlugin是直接做字符串替换的,它会把代码里的process.env.API_BASE_URL替换成你写的值。如果你写API_BASE_URL: 'http://localhost:8080',替换后代码变成process.env.API_BASE_URL = http://localhost:8080,这就成了语法错误。所以必须写成'"http://localhost:8080"',替换后是"http://localhost:8080",才是合法的字符串。
这个细节新手特别容易搞错,报错信息又不够直观,往往要排查半天。记住一句话:环境变量的值,字符串要套两层引号,数字和布尔值不用。
3.3 环境变量的注入原理
理解注入原理,能帮你排查很多"为什么变量读不到"的问题。整个链路是这样的:
config/index.js根据process.env.NODE_ENV判断当前是什么环境。- 加载对应的
dev.env.js或prod.env.js。 - 把加载到的对象传给
webpack.DefinePlugin。 DefinePlugin在编译阶段做全局文本替换,把process.env.XXX替换成实际值。
关键点在于,这是编译期替换,不是运行期读取。也就是说,打包完成后,代码里的process.env.API_BASE_URL已经变成了写死的字符串。这也解释了为什么改了环境变量必须重新打包才生效,光刷新页面是没用的。
提示:正因为是编译期替换,所以不要试图在运行时动态修改这些变量,那样不会生效。需要运行时配置的场景,得用别的方式,比如单独请求一个配置文件。
3.4 多环境扩展的实操方法
默认只有开发和生产两套环境,但实际项目往往需要更多。扩展的方法很直接:新增一个test.env.js,然后在index.js里根据环境变量加载它。
// config/test.env.js module.exports = { NODE_ENV: '"testing"', API_BASE_URL: '"http://test.xxx.com"' }然后在package.json里加一条脚本:
"scripts": { "build:test": "cross-env NODE_ENV=testing node build/build.js" }这里用cross-env是为了兼容不同操作系统的环境变量写法。Windows 下直接写NODE_ENV=testing是不生效的,必须借助这个工具。
配置加载逻辑也要相应调整,从原来的二选一变成多选一:
const env = process.env.NODE_ENV === 'production' ? require('./prod.env') : process.env.NODE_ENV === 'testing' ? require('./test.env') : require('./dev.env')环境一多,这种三元表达式会越来越难看,可以改写成映射表的形式:
const envMap = { development: require('./dev.env'), testing: require('./test.env'), production: require('./prod.env') } const env = envMap[process.env.NODE_ENV] || envMap.development这样以后再加环境,只需要往映射表里加一行,清爽很多。
4. 代理配置与构建配置的实战细节
4.1 proxyTable 代理配置怎么写
本地开发最头疼的就是跨域。浏览器同源策略拦着,前端跑在localhost:8080,后端跑在localhost:3000,直接请求就是跨域。proxyTable就是用来解决这个问题的。
proxyTable: { '/api': { target: 'http://localhost:3000', changeOrigin: true, pathRewrite: { '^/api': '' } } }逐项解释一下。target是后端服务地址。changeOrigin: true表示把请求头里的 Host 改成目标地址的 Host,很多后端框架会校验这个,不开可能被拒。pathRewrite是路径重写,把请求路径里的/api前缀去掉再转发给后端。
举个例子,前端请求/api/user/list,经过代理后实际请求的是http://localhost:3000/user/list。如果后端接口本身就带/api前缀,那pathRewrite就可以不写,或者写成{'^/api': '/api'}。
这里有个容易忽略的点:代理只在开发环境生效。打包上线后,proxyTable是不起作用的,线上跨域得靠后端配置 CORS 或者用 Nginx 反向代理。所以不要把代理当成跨域的终极方案,它只是开发阶段的便利工具。
4.2 构建输出路径的配置逻辑
build段里的路径配置,直接决定了打包产物长什么样、放哪里。几个关键项:
index:生成的 HTML 文件路径,通常是dist/index.html。assetsRoot:所有产物的根目录,通常是dist。assetsSubDirectory:静态资源子目录,默认static。assetsPublicPath:资源引用前缀。
打包后的目录结构大致是:
dist/ ├── index.html └── static/ ├── js/ ├── css/ └── img/assetsPublicPath这个值需要根据部署方式仔细设置。我整理了一个对照表:
| 部署方式 | assetsPublicPath 取值 |
|---|---|
| 部署在域名根目录 | / |
| 部署在子路径 /myapp/ | /myapp/ |
| 用 CDN 托管静态资源 | https://cdn.xxx.com/myapp/ |
| 相对路径引用 | ./ |
用相对路径./的好处是打包产物可以放在任意目录下都能跑,适合那种不确定部署路径的场景。但相对路径在某些路由模式下会有问题,比如 history 模式下刷新页面,相对路径可能解析错误,这时候还是得用绝对路径。
4.3 打包体积优化的几个开关
config目录里能控制的体积优化手段主要有两个:gzip 和 source map。
开启 gzip 需要两步。第一步在config/index.js里把productionGzip设为true,第二步确保安装了compression-webpack-plugin依赖。开启后会生成.gz文件,服务端配置好 gzip 支持后,浏览器请求时直接拿压缩包,传输体积能小一大截。
不过要注意,gzip 压缩率对不同类型的文件差别很大。JS 和 CSS 这类文本文件压缩率通常能到 70% 以上,而图片本身已经是压缩格式,再 gzip 意义不大。所以有些团队会选择只对 JS 和 CSS 做 gzip,图片走别的优化路径。
source map 的取舍前面提过,这里补充一个折中方案:用hidden-source-map模式,生成 map 文件但不在 JS 末尾加引用注释。这样浏览器不会主动去加载 map,但你可以手动把 map 上传到错误监控平台,线上报错时依然能还原源码。这个方案兼顾了安全和可调试性,我个人比较推荐。
5. 常见问题与排查技巧实录
5.1 环境变量读不到怎么办
这是最高频的问题。排查顺序建议这样走:
- 确认变量名拼写一致。
process.env.API_BASE_URL和配置文件里的API_BASE_URL必须完全一致,大小写敏感。 - 确认值的引号层数正确。字符串必须两层引号,这是最容易出错的地方。
- 确认重新打包了。编译期替换意味着改了配置必须重新构建。
- 确认加载逻辑走对了分支。在
index.js里打个console.log(process.env.NODE_ENV)看看实际值是什么。
我整理了一个速查表:
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 变量值为 undefined | 变量名拼写错误 | 核对大小写和拼写 |
| 报语法错误 | 字符串引号层数不对 | 字符串套两层引号 |
| 改了不生效 | 没重新打包 | 重新执行构建命令 |
| 加载了错误的环境 | NODE_ENV 判断逻辑有误 | 检查环境判断分支 |
5.2 打包后资源 404 的排查思路
白屏加控制台一堆 404,基本就是资源路径问题。先看assetsPublicPath设置得对不对,再看部署目录和这个值是否匹配。有个快速验证方法:打开dist/index.html,看里面引用 JS 的路径是什么,然后对照实际部署的目录结构,一眼就能看出问题。
还有一种情况是 CSS 里的图片路径不对。这是因为 CSS 中引用的图片路径是相对于 CSS 文件本身的,如果assetsPublicPath配错,CSS 里的图片会先挂。排查时优先看 CSS 里的背景图能不能加载出来。
5.3 代理不生效的几种情况
代理配了但请求还是跨域,常见原因有这几个:
- 代理路径没匹配上。
proxyTable的 key 是/api,但你请求的是/user/list,那自然不会走代理。请求路径必须以配置的前缀开头。 - changeOrigin 没开。后端校验 Host 头时会拒绝,加上
changeOrigin: true试试。 - pathRewrite 写反了。本来该去掉前缀,结果写成了保留,导致后端收到错误路径。
- 改完没重启服务。
proxyTable的改动需要重启开发服务器才生效,热更新不管这个。
注意:代理配置改完一定要重启,这个坑我见过太多人踩,改完发现没效果,折腾半天才发现是没重启。
5.4 多环境配置的维护心得
环境一多,配置文件容易乱。我的经验是遵循两个原则:一是命名规范统一,dev.env.js、test.env.js、prod.env.js这种命名一眼就能看懂;二是公共变量抽出来,比如所有环境都一样的变量,不要在每个文件里重复写,抽到一个base.env.js里,各环境文件去合并它。
// base.env.js module.exports = { APP_NAME: '"MyApp"', VERSION: '"1.0.0"' } // dev.env.js const base = require('./base.env') module.exports = Object.assign({}, base, { NODE_ENV: '"development"', API_BASE_URL: '"http://localhost:8080"' })这样改公共变量只需要改一处,减少遗漏。
6. 从 config 目录看构建配置的演进
6.1 Vue CLI 3+ 的变化与迁移
如果你从 Vue CLI 2.x 升级到 3+,会发现config目录没了,build目录也没了,取而代之的是根目录下的vue.config.js和.env文件。这不是功能缩水,而是配置方式变了。
环境变量从 JS 文件变成了.env文件,写法是KEY=VALUE的键值对形式,不需要再套两层引号。变量名必须以VUE_APP_开头才会被注入到客户端代码里,这是为了防止误把服务端的敏感变量暴露出去。
# .env.development VUE_APP_API_BASE_URL=http://localhost:8080代码里通过process.env.VUE_APP_API_BASE_URL读取。开发服务器配置和构建配置都挪到了vue.config.js里,通过devServer和outputDir等字段设置。
迁移的时候,把原来config/index.js里的dev段内容搬到vue.config.js的devServer里,build段内容搬到对应的顶层字段里,环境变量搬到.env文件里,基本就能对上。
6.2 配置管理的通用思路
不管用什么脚手架,配置管理的核心思路是不变的:环境隔离、集中管理、编译期注入。理解了这三点,换任何工具都能快速上手。
环境隔离意味着不同环境用不同配置,互不干扰。集中管理意味着配置有统一的入口和结构,不散落各处。编译期注入意味着环境变量在构建时确定,运行时不可变。这三条是构建配置的底层逻辑,比记住某个具体文件怎么写更重要。
6.3 我个人的一点经验
做了这么多项目,我在config目录上最大的体会是:配置要尽量简单,能少一个环境就少一个。有些团队一上来就搞五套环境,开发、测试、预发布、灰度、生产,结果每套环境都要维护一份配置,改个接口地址要改五个地方,出错概率反而更高。
我的建议是,环境数量控制在三套以内,开发、测试、生产,够用就行。预发布环境如果确实需要,可以通过在测试环境配置里加一个开关来模拟,不一定非要独立成一套。配置越少,维护成本越低,出错的概率也越小。
另外,环境变量里不要放敏感信息。因为编译期注入意味着这些值最终会出现在打包产物里,任何人都能看到。密钥、token 这类东西必须放在服务端,前端通过接口获取,这是安全底线。
最后分享一个小技巧:在config/index.js里加一个启动时的日志输出,把当前加载的环境和关键配置打印出来。这样每次启动服务,你都能一眼确认自己跑在哪个环境、接口地址是什么,避免"以为在测试环境结果连了生产库"这种低级但致命的错误。
console.log(`当前环境: ${process.env.NODE_ENV}`) console.log(`接口地址: ${env.API_BASE_URL}`)这几行代码花不了几分钟,但能帮你省下无数次排查环境问题的时间。