最近接了几个活,全是维护好几年前用 Vue2 写的系统,加上群里隔三差五就有新人问“怎么用脚手架开一个 Vue2 项目”,我突然意识到,这玩意儿其实还是很多人的刚需。虽然 Vue3 出来这么久,但存量项目、老系统的迭代需求,依然大量停留在 Vue2 上。从零搭一个规范、能维护、能部署的 Vue2 工程,不夸张地讲,是很多初级前端面对的第一道坎。这篇文章我就把整套流程、包括我踩过的坑和日常总结的小技巧,一次性讲透,希望能成为你“照抄就能用”的参考资料。
1. 脚手架选型:为什么现在的 Vue2 项目还在用这套方案
很多人一提到创建 Vue2 工程,第一反应就是“用 vue-cli 呗”。但严格讲,我们平时说的“脚手架”已经不单指一个工具,而是一整套工程化基础。你要先搞清楚自己站在哪条路上,再动手,不然很容易在后面配置 webpack、研究构建产物时一头雾水。
1.1 Vue CLI 与 Vite 两条技术路线的取舍
在 Vue2 时代,最主流的脚手架就是 Vue CLI。它基于 webpack,封装了 dev server、热更新、构建优化、插件机制等一整套方案。你只需要回答几个命令行问题,就能生成一个可运行、可部署、带转译和代码规范检查的基础工程,后续所有复杂的打包细节全部交给它处理。
这几年又兴起了另一种方案:用 Vite 创建 Vue2 项目。Vite 在开发环境基于原生 ESM,冷启动速度和热更新都明显更快,所以很多新项目开始尝试。但不是所有老项目都适合迁过去,尤其是依赖了大量 webpack 专属插件和特殊配置的工程,迁移成本不低。如果是从零开始、团队又愿意尝鲜,可以选 Vite;如果是为了接手老系统、跟团队现有技术栈保持一致,稳妥起见,我建议你还是优先用 Vue CLI。
1.2 为什么要保留 Vue2 而不是直接升 Vue3
聊到创建 Vue2 工程,免不了被问一句:“为什么不直接上 Vue3?”
我的观点很直接:技术升级要考虑的是成本和收益,而不是单纯追新。很多企业内部系统依赖的组件库、周边生态,并没有完全兼容 Vue3;还有一些前辈留下的代码,全是 Options API 风格,直接混用 Vue3 的重写成本比新建一个项目还高。现实情况是,Vue2 的生命周期和维护阵营还在持续提供安全补丁,尤其对于需要长期维护的系统,沿用 Vue2 并不是技术落后,而是一种理性的工程决策。
所以这篇文章里的方案,重点面向以下这几类人:需要接手或重建 Vue2 系统、刚学完 Vue2 基础知识准备做实战项目、团队内部有历史包袱需要延续技术栈的同学。核心目标很简单,就是快速得到一个“跑得起来、看得懂、能上线”的工程骨架。
2. 从零到一:基于 Vue CLI 创建 Vue2 工程的完整实操
创建工程本身并不难,难的是每一个选项背后的意义。如果你只是机械地一路回车,后面遇到问题还是会懵。所以我先把每一步操作拆开,同时把每一步为什么这么做讲清楚。
2.1 环境准备:Node.js 版本与 npm 源切换
首先建议你确认 Node.js 版本,这一点极其重要,却常常被忽略。Vue CLI 4.x 及早期 Vue2 工程对 Node 版本比较敏感,我在维护老项目时遇到过不少怪问题,最后都定位到 Node 版本过新或过旧。稳妥起见,推荐使用 14.x 或 16.x 的 LTS 版本,这两个版本对于绝大多数 Vue2 工具链都很友好。
如果你本机装了多个 Node 版本,推荐用 nvm 管理,随时切换。为什么我强调这个?因为团队协作时,如果有人 Node 是 12,有人是 18,装依赖、跑构建的结果都可能不一样,轻则警告,重则直接启动失败。
然后就是 npm 源。国内开发者经常会遇到 npm install 卡住、超时的问题,原因不用多说,建议把镜像源切换成 npmmirror 的地址。我多年实践下来的习惯是在用户目录下的 .npmrc 文件里设置:
registry=https://registry.npmmirror.com sass_binary_site=https://npmmirror.com/mirrors/node-sass phantomjs_cdnurl=https://npmmirror.com/mirrors/phantomjs ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/这段配置里重点在于第二行sass_binary_site。因为很多 Vue2 项目会用 node-sass,而 node-sass 的二进制文件默认从外网拉,国内网络环境下特别容易失败。提前在 .npmrc 里配上镜像,能省掉非常多不必要的折腾。
2.2 全局安装 Vue CLI 与创建项目命令
环境就绪后,先全局安装 Vue CLI:
npm install -g @vue/cli注意包名是@vue/cli,不是老版本的vue-cli。安装完成后用vue --version检查版本,出现数字就说明 OK 了。然后执行:
vue create vue2-demo这里的vue2-demo是项目目录名,按你自己的规范来起名,我习惯用小写字母加连字符的格式,比如customer-system或>? Check the features needed for your project: ( ) Babel ( ) TypeScript ( ) Progressive Web App (PWA) Support ( ) Router ( ) Vuex ( ) CSS Pre-processors ( ) Linter / Formatter ( ) Unit Testing ( ) E2E Testing
2.3 关键交互选项的逐个选择逻辑
这里我帮你把每个选项过一遍,并给出我的实践中比较推荐的组合:
- Babel:必选。Vue2 工程基本都是 ES6+ 的源码,需要 Babel 转译成目标浏览器能识别的 ES5。
- TypeScript:视团队情况。如果你们组都是 JS 老手,没有 TS 强制要求,可以不选,减少心智负担。如果要选,建议从一开始就统一规范,不要写到中间再加入,会非常痛苦。用 Vue2 写 TS,还要额外处理
vue-class-component这种方案,复杂度明显提升。 - PWA Support:一般项目用不到,不选。
- Router:如果你做的是单页应用,需要页面跳转,建议选上。如果只是个简单的单页组件展示,不选也无妨。我平时做后台管理系统,肯定会选。
- Vuex:多页面、多组件共享状态,建议直接选上。哪怕你一开始用不到,这种基础工程结构里预留的状态管理位置,后面有需求时不用返工。
- CSS Pre-processors:选上。我几乎都用 Sass,工程化项目里变量、嵌套、mixin 太常用了。后面的环节我会专门讲 Sass 和 node-sass 的坑。
- Linter / Formatter:建议选。哪怕你个人开发,也能保证代码风格统一。后面如果团队协作,这一步更重要。我一般会选择 ESLint + Prettier 组合。
- Unit Testing / E2E Testing:看项目需要,一般管理系统一开始不上测试框架的话,可以先不选,后续需要再补。
接下来它会问你要不要用 history 模式的路由,我建议选 no,也就是默认的 hash 模式。为什么?因为 history 模式需要服务端配合做 fallback,否则部署到静态服务器上,刷新非根路径页面很容易出现 404。hash 模式虽然丑一点,但省心,部署时容忍度极高。
CSS 预处理器选择方面,我建议选Sass/SCSS (with dart-sass)。现在的新工程已经不推荐 node-sass 了,原因之后细说。Linter 我推荐选ESLint + Prettier,语法检查能力兼顾代码格式美化,自动化格式化会帮你省出大量时间。
最后它会问你要把这次的配置方案保存成一个 preset 吗?如果你经常创建 Vue2 项目,我建议你存一下,比如起个名叫vue2-base。下次创建时直接选这个预设,所有选项都不用再重复填写,非常省时间。
2.4 目录结构说明与初始化验证
依赖安装完毕后,打开工程目录,你会看到这样一个基础结构:
├── node_modules ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── assets │ ├── components │ ├── router │ ├── store │ ├── views │ ├── App.vue │ └── main.js ├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── babel.config.js ├── package.json └── vue.config.js有些版本会直接生成vue.config.js,有些早期版本没有则需手动创建。我用一个极其实际的方法验证工程是否正常:先跑一下开发服务器:
cd vue2-demo npm run serve接着浏览器打开http://localhost:8080,如果能看到 Vue 的欢迎界面,说明脚手架已经正常工作了。第一次启动可能有点慢,因为 webpack 要预构建整个依赖图,这种卡顿在 Vue CLI 时代非常正常。不要紧张,等编译进度条走到 100% 就好。
3. 工程化进阶:vue.config.js 里的常用配置你该这样写
脚手架帮你解决了基础框架,但真正决定一个工程能不能顺利上线、好不好维护的,往往是vue.config.js里的细节配置。接下来这部分,我根据真实项目里的高频需求,给你梳理几个最常用也最容易踩坑的配置项。
3.1 publicPath 与静态资源路径问题
很多新手部署后遇到白屏或静态资源 404,非常多时候是publicPath配置不对。
默认情况下,Vue CLI 构建出来的资源路径是绝对路径/。如果你的项目是部署在域名根路径下,那没问题;如果是部署在某个子路径下,比如www.example.com/xxx/,那么不修改 publicPath,构建后的 index.html 会引用/js/app.js,服务器上根本没有这个路径,所以白屏。
我一般会根据实际部署场景来做:
module.exports = { // 如果你的项目部署在根路径,用 '/';如果部署在子路径,用相对路径 './' 或直接写死子路径 publicPath: './' }./这种相对路径写法,适合纯前端静态文件部署,不依赖服务端特殊配置。但请注意,如果你启用了 history 路由模式,相对路径会出问题,此时还是要明确写子路径并且配合服务端配置。最简单判断标准:你要么都用 hash 路由,那 publicPath 随便配;要么用 history 路由,那 publicPath 必须和实际部署路径严格一致。
3.2 devServer 代理配置解决跨域问题
联调接口时最烦的就是跨域。虽然可以要求后端开 CORS,但很多情况下你不一定接触得到服务端配置。最立竿见影的方案,就是在本地开发服务器上配代理。
我日常使用的是这样的写法:
module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://192.168.1.100:3000', changeOrigin: true, pathRewrite: { '^/api': '/api' } } } } }这样在项目里发请求时,直接请求/api/xxx,本地服务会把请求转发到http://192.168.1.100:3000/api/xxx。加上changeOrigin: true是因为很多后端服务会校验请求来源,不设置它容易被拒绝。pathRewrite则根据你后端接口的真实前缀灵活调整,如果你后端接口本身就是/api开头,其实可以不用重写。
还有一种情况是后端接口地址里带了复杂的前缀,比如/api/v1/,那你只需要把 target 改成后端地址,并且重写规则写好即可。代理配置的核心逻辑是:让前端代码里始终用一个相对路径发请求,实际转发交给 devServer 处理。这样你从开发到测试环境,只需要改vue.config.js里的 target,而不需要改业务代码。
3.3 构建输出优化:sourceMap、gzip 与自定义构建规则
构建层面,我一般会关注这几个配置:
module.exports = { productionSourceMap: false, chainWebpack: config => { // 如果传统浏览器不兼容某些语法,可以在这里加转译 for 第三方库 config.module .rule('js') .include .add(/node_modules\/(some-module)\//) } }productionSourceMap: false意味着生产环境不会生成 source map 文件,这样可以避免源码暴露,也让构建产物体积减小不少。排错时如果确实需要定位线上问题,临时再开 true 构建一版用于排查即可,没必要长期开启。
gzip 压缩一般不放到 webpack 层面,更多是在 Nginx 上配置gzip on。如果你用的是静态服务器 CDN,一般也会自动压缩。如果确实需要在构建期生成 gzip 文件,可以引入compression-webpack-plugin,如:
const CompressionWebpackPlugin = require('compression-webpack-plugin') module.exports = { chainWebpack: config => { config.plugin('compression').use(CompressionWebpackPlugin, [{ test: /\.(js|css|html)$/, threshold: 10240, minRatio: 0.8 }]) } }这里threshold: 10240表示只有超过 10KB 的文件才会被压缩,小文件压不压意义不大,没必要徒增打包时间。
另外还有自定义 loader 规则、多环境变量注入等需求,这些都可以基于chainWebpack的 API 来实现。我提醒各位:webpack 的 chain 配置改动前,最好先跑一次构建看看结果,改完再跑一次对比。不然你看着配置写对了,实际产物可能早就不是预期的那样了。
4. 深入配置细节:Babel、环境变量与样式的工程化处理
创建完工程只是第一步。真正进入业务开发前,我建议先把这几个细节处理好,否则半路改配置的成本远高于开头就弄好。
4.1 Babel 配置与浏览器兼容范围
脚手架生成的babel.config.js通常是:
module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ] }这套预设是非常合理的 Vue2 默认方案,它结合了@babel/preset-env和 Vue 官方需要的插件,会依据你项目根目录的.browserslistrc来决定对哪些语法做转译。举个例子,如果.browserslistrc里写的是:
> 1% last 2 versions not dead那么 Babel 会针对全球使用率大于 1% 的浏览器做兼容转换。反之,如果你写的是:
Chrome >= 70这表示只需要兼容 Chrome 70 以上版本,Babel 就会少做很多转译,构建产物也相对更小、更快。日常项目里我建议先参考你业务用户的真实浏览器环境来定,而不是一味追求全部转译。
4.2 环境变量模式:区分开发、测试、生产环境
Vue CLI 环境下,你可以用.env.development、.env.production这类文件来管理不同环境下的变量。我在实战中经常这样配置:
# .env.development VUE_APP_BASE_URL=/api VUE_APP_TITLE=开发环境# .env.production VUE_APP_BASE_URL=https://api.example.com VUE_APP_TITLE=生产环境在代码里,只需要通过process.env.VUE_APP_BASE_URL就可以获取到当前环境的变量值。这里有个关键约束:只有以VUE_APP_开头的变量才会被 webpack 注入到客户端代码里,其他名称的变量不会生效。另外我提醒一句:不要把密钥、密码这类敏感信息写进前端环境变量。前端代码无论如何都是公开的,把敏感信息放在这里等于直接裸奔。
4.3 Dart Sass 与全局样式变量的注入
前面构建选项里我选了 dart-sass,这是现在推荐的预处理方案。node-sass 虽然老项目里常见,但它基于 LibSass,已经停止维护很久了,而且每次换 Node 版本,经常面临重新编译的问题。dart-sass 则更活跃、更稳定,官方也在推进兼容。
在vue.config.js里,可以注入全局样式变量:
module.exports = { css: { loaderOptions: { sass: { additionalData: `@import "@/styles/variables.scss";` } } } }这样在每个组件的<style lang="scss">里,你都无需手动@import变量文件,就能直接使用里面的变量和 mixin。需要注意的是,如果你注入的内容里定义了多个文件,不要重复引入同名的变量,否则容易出现样式冲突。稍有疏漏,就是一场变量覆盖的灾难。
5. 实战改造:给 Vue2 工程集成 UI 组件库与移动端适配
当基础工程稳定后,下一步往往是引入 UI 框架。这里我以常用的桌面端管理组件库为例,说一下集成流程,以及集成过程中我自己遇到的典型问题。
5.1 按需引入组件库的正确姿势
全局引入确实是新手最常用的办法,优点是简单,但缺点是它会让你打包体积瞬间膨胀不少,尤其是包含大量组件的库,哪怕你只用其中几个组件,也会全量打包。大部分组件库都支持按需引入。以某个组件库为例,安装依赖后,在src/main.js里这样配置:
import Vue from 'vue' import { Button, Table, Dialog, Form } from 'some-ui' import 'some-ui/lib/theme-chalk/index.css' Vue.use(Button) Vue.use(Table) Vue.use(Dialog) Vue.use(Form)同时配合babel-plugin-import插件,在babel.config.js里添加:
module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ], plugins: [ ['import', { libraryName: 'some-ui', style: true }] ] }这套组合拳可以在一定程度上减少无用代码进入构建产物。我更推荐的做法是,做一个单独的文件,比如src/plugins/ui.js,把 UI 组件的注册流程统一放进去,这样main.js里就非常干净,后期更换或升级组件库时也只动一个文件。
5.2 移动端项目:postcss-px-to-viewport 适配方案
如果你建的是移动端 H5 工程,那么常常要处理不同屏幕宽度下的样式适配。业界常见的方案: rem 适配、vw/vh 适配。vw 方案更简单直接,也是我近两年用得最多的。
安装工具后,在项目根目录的postcss.config.js里配置:
module.exports = { plugins: { 'postcss-px-to-viewport': { viewportWidth: 750, unitPrecision: 3, viewportUnit: 'vw', selectorBlackList: ['.ignore'], minPixelValue: 1, mediaQuery: false } } }viewportWidth: 750表示设计稿按 750 宽度设计,这样 750px 的元素在屏幕上也刚好是 100vw。但有几点必须注意:如果你集成了一些第三方组件库,它们的样式通常按照自己设定的基准来写,强行转换也会破坏布局。所以selectorBlackList里就很有必要把第三方类名前缀加进去,比如['.van-', '.ignore']。另外,对于 1px 的边框类样式,minPixelValue: 1可以防止这种单位被转成不可预期的值。
5.3 路由与状态管理在工程里的组织方式
使用脚手架生成的路由和 Vuex 结构可能比较基础,真正遇到业务场景时我会做这些调整:
路由层面,我会把路由拆成模块,比如router/modules/order.js、router/modules/user.js,然后在router/index.js里合并。这样多人协作时各写各的业务路由,不容易产生大量冲突,也让路由表的层级保持清晰。
Vuex 层面同理,按业务模块划分 store module。如果状态量特别大,建议在 mutation、action 之前统一约定命名规范。比如 actions 用模块/动作格式,mutations 用SET_XXX格式。别小看这种约定,它在排查复杂状态问题时,能让你少掉很多头发。
6. 高频问题排查:我在 Vue2 工程创建与使用中踩过的坑
最后我整理几个典型问题,这些问题在社区里被反复问过,也和脚手架创建 Vue2 工程高度相关。我把排查思路一并写出来,帮你少走弯路。
6.1 node-sass 安装失败与版本不兼容
node-sass 的问题,堪称 Vue2 工程的头号杀手。它的本质是:使用一个针对特定 Node ABI 版本编译好的二进制文件,Node 版本一变,二进制文件也需要重新下载编译。网络不稳时,安装大概率会挂。
我推荐的方案是:
- 新项目直接使用
sass(dart-sass),不再用 node-sass。 - 老项目必须用 node-sass 时,先确认 Node 版本是否匹配,再看
.npmrc里有没有设置sass_binary_site。 - 如果实在装不上,可以先单独设置环境变量然后重装一次:
npm config set sass_binary_site https://npmmirror.com/mirrors/node-sass npm rebuild node-sass这套操作我修复过很多次同事的本地环境,基本能解决问题。
6.2 端口占用与 dev server 起不来
开发服务器默认端口是 8080,但经常被其他程序占用。通过vue create生成的配置里,默认端口不一定写在vue.config.js里,所以当你发现无法启动时,可能不是代码问题,而是端口续期问题。我的处理方式是直接在vue.config.js里写死端口:
module.exports = { devServer: { port: 8080, host: '0.0.0.0' } }同时设置host: '0.0.0.0',这样局域网内其他设备也可以通过你的 IP 访问页面,方便在真机或手机上预览调试。如果不生效,再检查是不是 setstrictPort: true,它会让 webpack-dev-server 在端口冲突时报错退出,而不是自动换端口,这反而有助于快速发现问题。
6.3 ESLint 编译报错或格式问题导致启动失败
很多新人创建完项目后,一跑npm run serve就发现满屏 ESLint 报错。这不是项目有问题,而是代码风格没有符合默认规则。很多团队早期在这上面吃过亏,代码里加个空行都被拦截,非常烦人。
我建议把 ESLint 定位成“辅助”而非“卡点”。如果你不希望它在 dev server 启动时直接报错阻断,可以修改vue.config.js:
module.exports = { devServer: { overlay: false } }这样编译错误不会全屏遮挡页面,你可以在终端里看具体报错信息。同时,我在实际团队里更强烈的建议:提交代码时做一个 lint 钩子来把关,而不是在 dev server 上打断开发节奏。最终目标是保证代码规范,而不是开发时被规范折磨得烦躁不已。
6.4 proxy 代理不生效的排查思路
代理不生效的情况,大多数不是配置写错,而是请求路径和后端实际路径对不上。我提供一个我常用的排查路径:
- 先打开浏览器 Network 面板,看真实发出的请求地址到底是什么。
- 确认前端请求的是相对路径(以
/api开头),而不是绝对地址。 - 检查 Nginx 或后端有没有额外的路径前缀。
- 用 curl 直接找一个代理出来的地址测试,比如:
curl -X GET http://localhost:8080/api/user/list如果 curl 返回结果正常,说明 devServer 代理本身没问题,问题多半出在浏览器里拿着旧 IP 或者前端代码里用了绝对地址。如果 curl 返回 404,就要回到target和pathRewrite的组合上再排查一遍。
7. 从一个可运行的项目到可维护的工程:我的一些体会
把 Vue2 工程从“能跑”做到“好维护”,我最大的体会是:目录结构只是表面功夫,真正决定项目上限的是你对工具链的理解和团队约定的执行。
项目创建初期,花一点时间把目录模块划分清楚、把环境变量和代码规范定好,后期几乎每一天都在受益。反之,如果这些一开始不做,后面边写边乱,代码和配置的腐化速度都会非常快。我在项目里带的几个新人,最喜欢问的就是“这个文件是干嘛的”“为什么配置在这里”,而答案基本都是:因为工程在创建时选的方案决定了它的形态。
最后再分享一个小技巧:如果一个团队会长期维护多个 Vue2 项目,强烈建议把一次完整配置保存成 Vue CLI 的 preset,统一分发到团队各个成员的机器上。这样任何人新增项目,几秒钟就能得到完全一致的基础配置。不要小看这一点,它直接决定了你团队的“项目统一性”和“协作顺畅度”——在我维护过的一堆老项目里,最痛苦的事情不是代码难写,而是每个项目的配置长得都不一样,新来的人每接手一个项目都要重新适应一次。