先说个很常见的场景。手头已经有一个跑了好几年的 Vue2 老工程,或者团队明确说了新项目继续用 Vue2,那你迟早会被“基于脚手架创建 Vue2 工程”这件事卡住。网上教程刷到不少,可跟着敲完vue create,下一秒就是 Node 版本不兼容、依赖装不上、路由版本对不上、组件库装成了 Vue3 专用版,折腾一晚上还在原地打转。这篇文章把我实际创建 Vue2 工程的完整过程写出来,包括选哪套脚手架、每一步在做什么、创建完以后哪些配置文件必须动、后续要补哪些依赖,以及我反复踩过的一些坑。适合正在摸脚手架的新手,也适合有 Vue3 经验但需要回头维护 Vue2 工程的同学。
1. 为什么到了现在还要新建 Vue2 工程
1.1 老项目的技术现实决定新模块的选型
我参与维护过一个内部后台系统,代码从一开始就是基于某套 Vue2 模板仓库搭建的,业务组件、权限指令、报表模块全都长在 Vue2 的生态上。业务方后来要在里面加一个新模块,最稳妥的方式不是开一个全新的 Vue3 工程,而是在同一个仓库、同一套技术栈下面继续扩展页面。这么做的理由很直接:登录态、路由守卫、权限指令、布局组件都能直接复用,新写的代码也能被老同事照常 review。
很多团队面临的问题不是“哪个框架好”,而是“存量代码怎么办”。既然没人愿意花两个月把老系统重写一遍,那新模块继续用 Vue2 就是最务实的答案。这种情况下,基于脚手架快速生成一个结构清晰的 Vue2 工程,再逐步迁移或扩展功能,比从零手写 webpack 配置要高效得多。
1.2 动手之前,先想清楚这三件事
第一,团队里还有没有人熟悉 Vue2 的写法。如果团队全是 Vue3 出身,写 Options API 和this.$router这类语法时会很不习惯,得提前约定写法规范。第二,新工程是独立部署还是要嵌入老应用。独立部署可以放开手脚配置路由模式,嵌入老应用则要尽量对齐原有依赖版本。第三,是否存在历史包袱。如果是从零起步、没有兼容压力,当然可以认真评估 Vue3;但如果要复用的组件库、工具函数和参考代码都锁定在 Vue2 上,硬上新框架只会加大成本。
有一个很容易被忽略的点:新建 Vue2 工程通常是“为了维护存量系统”而不是“逆着潮流选型”。想清楚这个前提,后面所有技术决策都会顺畅很多。
1.3 官方演进放缓,不代表工程不能继续生长
不少新手担心 Vue2 是不是已经彻底不能用了。准确地说,官方不再围绕 Vue2 做新功能演进,维护动作也按既定计划执行,但存量项目的数量仍然非常大,相关文档、组件库、历史问答都还在。这并不等于你该无脑选 Vue2,而是要在面对一个已经存在的 Vue2 体系时,能判断出“新建一个 Vue2 工程”依然是一个合理选项。
工程能不能继续生长,取决于团队愿不愿意持续维护。只要代码规范、依赖版本、构建链路都是清楚的,Vue2 项目的迭代体验不会因为框架版本变老而突然崩塌。
2. 脚手架与构建工具怎么选,不能只看热度
2.1 三条主要路线:Vue CLI、Vite 插件方案、手动拼 webpack
网上讨论 Vue2 工程创建,主要会提到三条路线。第一条是官方 Vue CLI,无论 4.x 还是 5.x,都能通过交互式命令生成 Vue2 或 Vue3 工程,内置 webpack、devServer、ESLint、多环境等能力,属于最主流的选择。第二条是 Vite 配合支持 Vue2 的社区插件,开发服务器启动速度和热更新确实快,但插件的成熟度、兼容范围都不如 webpack 生态。第三条是手动搭 webpack 5 加 vue-loader,适合对构建产物有特殊要求、或者想彻底掌控配置的极少数场景。
对普通业务工程来说,手动拼配置的维护成本很高。你要同时处理 babel 转译、样式预处理器、静态资源处理、开发服务器代理、多环境变量,稍有不慎就漏掉一块。除非团队里有专门负责工程化的同学,否则真不建议从零写。
2.2 我为什么最后留在 Vue CLI
我自己的选择比较朴素:团队大多数人已经接触过 Vue CLI 生成的工程结构,出问题后在社区里能搜到大量现成答案。Vue CLI 5 内部用的是 webpack 5,相比老版本的构建性能有明显提升,同时保留了良好的配置覆盖能力,需要改 devServer 代理、处理资源路径时,可以借助vue.config.js完成操作。
创建工程这件事,稳定性永远比新鲜感重要。Vite 的方案我也试过,小项目里确实很爽,启动速度快到有点不真实,可一旦遇到老的 CJS 依赖、需要特殊 babel 插件或者想复用某个基于 webpack 的定制 loader,排查成本一下就上来了。所以最终给团队的推荐还是 Vue CLI,跑通闭环最快,也好交接。
2.3 工具选型对比与适用场景
| 对比项 | Vue CLI | Vite 插件方案 | 手动 webpack |
|---|---|---|---|
| 上手成本 | 低 | 中 | 高 |
| Vue2 编译链路成熟度 | 高 | 中 | 中 |
| 开发服务器速度 | 中等 | 快 | 取决于配置 |
| 社区资料和插件丰富度 | 高 | 中低 | 低 |
| 适合场景 | 常规业务工程、团队协作 | 个人原型、项目对构建速度要求极高 | 特殊构建需求、深度定制 |
如果非要选 Vite,一定要先确认你要用的第三方库都兼容这套编译链路。实测下来,部分老组件库、样式处理方案和动态导入方式都需要额外适配。不要只因为“快”就换工具,开发体验的收益如果抵不上排查兼容性浪费的时间,那就是亏的。
3. 从零跑通一个 Vue2 工程的完整过程
3.1 环境准备:Node 和 npm 先过一遍
动手前先打开终端确认环境:
node -v npm -vVue CLI 5 对 Node 版本有要求,建议使用 14、16 或 18 这样比较稳的 LTS 版本。我实测在 Node 18 下运行 Vue CLI 5 很稳定,Node 20 大部分场景也没问题,但早期 Node 22 版本可能会遇到部分 webpack loader 的兼容问题。因此,老实的做法是安装 Node 18 LTS,不要盲目追求最新版本。
如果 npm 下载依赖很慢,可以先把镜像源换到国内公共源:
npm config set registry https://registry.npmmirror.com这里要留意一点:如果团队有内网私有源,建议直接使用内网源,公共镜像和私有源的同步速度、包版本可能不一样,会造成同一份锁文件在两个人手里安装出不同结果。
3.2 安装脚手架并确认版本
全局安装 Vue CLI:
npm install -g @vue/cli@5 vue --version我习惯固定到 5.x 大版本,避免未来 CLI 行为变化影响团队一致性。安装完成后,vue --version能正常输出版本号,就说明脚手架已经就绪。这里有个小提醒:如果之前装过老版本,最好先卸载再装:
npm uninstall -g @vue/cli否则可能残留旧命令,导致生成工程时出现预期外的模板行为。
3.3 用 vue create 完成交互式创建
创建工程:
vue create my-vue2-project交互界面里常见的选择如下:
- 选择
Manually select features,不要直接回车选 Default,因为默认预设通常是 Vue 3。 - 在功能列表里勾选:Babel、Router、Vuex、CSS Pre-processors、Linter / Formatter。用方向键移动,空格键勾选。
- 选择 Vue 版本时,明确选
2.x。 - CSS 预处理器建议选
Sass/SCSS (with dart-sass),尽量避免老旧的 node-sass。 - Linter / Formatter 建议选
ESLint + Prettier。 - 触发检查时机选
Lint on save。 - 配置文件位置选
In dedicated config files,生成的配置集中在独立文件里,后续好改。
确认选项后,CLI 会自动安装依赖。整个过程会根据网络速度从几十秒到几分钟不等,耐心等它跑完即可。
3.4 打开工程,理清目录和入口
依赖装完后:
cd my-vue2-project npm run serve浏览器访问http://localhost:8080,看到默认欢迎页就说明工程已经能跑了。生成的核心目录结构大致如下:
src/ main.js App.vue router/index.js store/index.js views/ public/ index.html vue.config.js babel.config.js package.jsonmain.js是整个应用入口,它在里面创建 Vue 实例,挂载路由和状态管理。App.vue是根组件,router/index.js和store/index.js分别负责路由和全局状态。public/index.html是最终承载构建产物的 HTML 模板。建议把这个目录结构烂熟于心,因为后面所有工具链配置都是在这些文件之间打转。
4. 建完工程后,先把这些配置文件理顺
4.1 package.json 里的依赖搭配逻辑
刚生成的package.json只能保证工程能跑,真正决定它稳不稳定的,是依赖版本之间的匹配关系。写 Vue2 工程时最重要的规则是:如果用的是 Vue 2.6 及以下版本,vue和vue-template-compiler版本必须严格一致,比如"vue": "^2.6.14","vue-template-compiler": "^2.6.14"。很多莫名其妙的编译报错都是这两个包版本不匹配导致的。
如果使用的是 Vue 2.7,情况会有所变化,模板编译器的处理方式不再需要单独遵循老规则。团队里我建议统一约定:新工程尽量使用 Vue 2.7 的最新补丁版本,同时写业务代码时仍以 Options API 为主,降低学习成本和排查负担。
另外,core-js承担了大部分 polyfill 职责,@vue/cli-service是构建核心,不要随意调整它们的版本,除非你明确知道改动的影响范围。
4.2 vue.config.js 一份最常用的底稿
Vue CLI 项目里,很多工程化配置都集中在vue.config.js。下面这份是我常用的底稿:
const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, publicPath: './', outputDir: 'dist', assetsDir: 'static', devServer: { port: 8080, proxy: { '/api': { target: 'http://your-api-host', changeOrigin: true, pathRewrite: { '^/api': '' } } } } })publicPath很关键,如果工程要部署在服务器子目录,直接把publicPath从'/'改成实际子路径,比如'/admin/';如果是纯静态文件部署且路由使用 hash 模式,写成'./'也能省事。outputDir和assetsDir控制构建输出的目录名和静态资源目录。devServer.proxy是开发环境代理,前端页面里写/api/xxx,本地开发时就会被转发到后端服务,后面我会专门讲这个配置的坑。
4.3 目标浏览器、Babel 与 polyfill
再看.browserslistrc文件,它决定了 Babel 转译和样式兼容的作用范围。默认常见配置是:
> 1% last 2 versions not dead含义是兼容全球使用率超过 1% 的浏览器、最近两个版本的浏览器、还在维护的浏览器。如果项目只给内部团队用 Chrome,可以把它精简成:
last 2 Chrome versions这样构建产物体积会明显变小,因为不再需要为大量老旧浏览器准备 polyfill。babel.config.js里通常就是@vue/cli-plugin-babel/preset,它会结合.browserslistrc自动决定转译目标。很多新手只关注业务代码,却忽略了这份配置,结果就是构建产物大、兼容范围完全没按需求设计。
4.4 环境变量与多环境构建
工程根目录下可以新建.env.development和.env.production两个文件。文件里定义的变量,只有以VUE_APP_开头的会暴露给前端代码:
VUE_APP_API_BASE_URL=/api VUE_APP_TITLE=后台管理系统业务代码里通过process.env.VUE_APP_API_BASE_URL读取。执行npm run serve时加载.env.development,执行npm run build时加载.env.production。如果后续要增加测试环境,可以添加.env.test,再用vue-cli-service build --mode test触发对应模式。
这个机制的力量在于:不同环境使用不同接口域名,命令行一句就能切换,不用每次改代码。
5. 新工程从“能跑”到“能开发”的落地清单
5.1 先把路由和状态管理的版本钉死
Vue2 对应的路由库是vue-router@3,状态管理库是vuex@3。很多人图方便直接执行npm install vue-router vuex,结果装成了 Vue3 专用版本,一运行各种报错。创建工程后接着执行:
npm install vue-router@3 vuex@3 --savepackage.json里应该是:
"vue-router": "^3.6.5", "vuex": "^3.6.2"路由写法也要区分。Vue2 里创建路由对象用的是new VueRouter({ mode: 'history', routes }),而不是 Vue3 的createRouter。如果从老项目复制代码,这个差异是最常见的问题来源。
5.2 UI 组件库选择前先看 peerDependencies
Vue2 生态里常见的组件库是 Element UI,而 Element Plus 是给 Vue3 用的,二者千万别装混。实际上不光是组件库,任何第三方库都要先确认它声明的peerDependencies里是不是vue: ^2.x。打开node_modules/库名/package.json,一眼就能看到支持范围。
我在一个项目里见过同事直接安装了 Element Plus,结果导入组件时控制台大量警告,排查了半天才发现是版本错位。建议在安装任何依赖之前,先到 npm 页面看一眼版本下的说明,或者看依赖声明里的 peerDependencies,这种检查习惯能避免大量浪费时间的错误。
5.3 axios 封装与基础请求层
业务开发前,先把网络请求层搭好。axios本身不挑 Vue 版本,但封装逻辑要提前规划。我习惯在src/api目录下统一维护请求实例:
import axios from 'axios' const request = axios.create({ baseURL: process.env.VUE_APP_API_BASE_URL, timeout: 10000 }) request.interceptors.request.use((config) => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( (response) => response.data, (error) => { // 统一处理 401、接口异常等 return Promise.reject(error) } ) export default request这样做的好处是,以后所有页面请求都走同一个实例,统一处理鉴权、错误提示、请求超时,不用在每个组件里重复写。等工程规模变大以后,你会庆幸一开始就留好了这层抽象。
5.4 代码规范与提交前检查
脚手架已经生成了 ESLint 配置,接下来可以补充 Prettier 和提交前检查。安装 lint-staged 和 husky:
npm install -D lint-staged husky然后在package.json里增加:
"lint-staged": { "*.{js,vue}": ["eslint --fix", "prettier --write"] }husky 的初始化方式在不同版本里差别很大,建议按你安装的主版本官方说明执行。只要设置了提交前检查,团队成员在提交代码时就会自动做格式修正,避免“本地缩进风格各不相同”的混乱场面。注意不要为了减少干扰而关闭 lint,不然代码规范形同虚设。
6. 我踩过的最多的坑
6.1 Node 版本过高或过低,依赖直接罢工
我在新电脑上第一次跑老 Vue2 工程,遇到的是node-sass编译失败。原因很简单,Node 版本太高,老的原生模块编译不过去。解决办法是优先使用dart-sass替代node-sass。Vue CLI 交互式创建时选 Sass 预处理器,记得选带dart-sass的那一项,不要接受默认的 node-sass。
另一个问题是 Node 版本过低时,@vue/cli-service可能直接提示不支持。遇到这类问题,最省心的方式不是改代码,而是让全团队统一到 Node 18 LTS,在项目 README 里写清楚版本要求。很多所谓“环境问题”根本不是代码问题,而是版本矩阵不一致。
6.2 混用 npm 与 yarn 带来的依赖漂移
团队里如果有人用 npm 更新过依赖、有人坚持用 yarn,package-lock.json和yarn.lock会各自生长,最终 node_modules 结构完全不一致,出现“我本地没问题、你那边装不上”的经典矛盾。我在内部项目里吃过一次亏后,强制整个仓库统一使用 npm,并且只提交package-lock.json,相关人员一律删除node_modules后重新安装。
新的成员加入时,也要求先确认包管理器配置再执行安装命令。工程根目录可以放一个.npmrc,把 registry 固定下来,同时设置engine-strict=true,让包管理器在 Node 版本不符合要求时直接给出明显错误,而不是一路硬装到最后才崩溃。
6.3 代理配置写了却不起作用的几类原因
开发时要接入后端接口,最容易见到的情况是改了devServer.proxy但请求仍然不能转发。常见原因有三类:改了配置没有重启 devServer;target写了完整路径而不是域名根地址;pathRewrite规则写错导致接口路径拼错。
再看一次配置:
devServer: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, pathRewrite: { '^/api': '' } } } }假设页面请求/api/user,上面配置会把它转发到http://localhost:3000/user,其中pathRewrite的'^/api': ''负责去掉前缀,让后端不需要额外处理/api路径。changeOrigin必须为true,否则部分后端会校验 Host 字段并直接拒绝请求。改完配置后,记得重启 devServer,它是不会自动感知vue.config.js变化的。
6.4 history 路由部署子目录后的空白页
使用 Vue Router 的 history 模式时,如果工程部署在子目录,比如http://example.com/admin/,访问首页大概率是空白页。这是因为静态资源路径没有对齐。处理方法是把vue.config.js里的publicPath设置为'/admin/',同时给路由对象配置base:
new VueRouter({ mode: 'history', base: '/admin/', routes })如果后端没有把未知路径回退到index.html,刷新二三级页面还会出现 404。这种情况要么让部署服务器配置 fallback 规则,要么在路由方案上改成 hash 模式图省心。我的建议是:开发前先问清楚最终部署方式,再决定路由模式,能少走很多弯路。
6.5 Vue 2.7 带来的组合式 API 兼容问题
如果新工程直接用 Vue 2.7,理论上可以写组合式 API,甚至用<script setup>。实际情况是,很多第三方组件、混入工具和参考代码还停留在 2.6 时代的 API 习惯,直接照搬容易踩兼容问题。我更推荐的团队约定是:使用 Vue 2.7 作为基础版本,但业务代码继续沿用 Options API,新语法只在小范围试验,不当作团队主流写法。
这种选择并不是保守,而是为了降低协作成本。Vue2 工程的最大价值是稳定承接既有业务,而不是充当新语法的试验场。等整个仓库真的有条件迁移到 Vue3 时,再统一升级反而比在 Vue2 里混着两种写法更容易收拾。
最后再补一条很实用的小技巧:在 README 里写清楚四个约定——Node 版本、包管理器、Vue 版本、UI 组件库版本。团队协作时这四行字能解决大量“为什么你那边能跑”的问题。无论你是基于脚手架创建 Vue2 工程来维护老系统,还是想补一下历史技术栈,先把能跑通的闭环打通,再把配置逐个理顺,后续的开发体验就会顺畅得多。