Vue-Cli 脚手架从入门到实战:环境搭建、路由、Vuex 与工程化踩坑复盘

发布时间:2026/10/15 15:12:58
Vue-Cli 脚手架从入门到实战:环境搭建、路由、Vuex 与工程化踩坑复盘
1. 从零上手 Vue-Cli 脚手架一个老前端的踩坑复盘刚入行那会儿我最怕听到的一句话就是“你先把项目跑起来”。因为那时候我连 Node 是什么版本、npm 和 yarn 有什么区别都搞不清楚更别提什么脚手架、路由、状态管理了。后来带过几个新人发现大家卡住的地方惊人地一致不是 Vue 语法难而是工程化的那一套东西没人给你讲透。Vue-Cli 就是这样一个东西它本身不复杂但它是你从“写单个 HTML 页面”跨到“做一个正经前端项目”的第一道门槛。这篇文章我打算把 Vue-Cli 从安装到项目跑起来、再到路由、状态管理、代码规范、常用组件库这一整条链路用我自己的话重新捋一遍。不管你是刚学完 Vue 基础语法想做个完整项目练手还是工作里突然被丢了一个 Vue 项目让你维护这篇内容都能让你少走至少两星期的弯路。先说清楚 Vue-Cli 到底解决了什么问题。在没有脚手架的时代你要做一个 Vue 项目得自己用 script 标签引入 Vue自己配 webpack自己装 babel自己处理热更新自己搞环境变量。光是让项目能跑起来就得折腾一整天。Vue-Cli 把这些东西全部封装好了你只需要敲一行命令选几个选项一个带热更新、带构建配置、带代码检查的完整项目就出来了。它的核心价值不是“帮你写代码”而是“帮你省掉搭环境的时间”。我个人的判断是如果你打算认真学 VueVue-Cli 是必须过的一关哪怕现在 Vite 很火但大量存量项目还在用 Vue-Cli你迟早要面对它。2. 安装脚手架之前先把 Node 环境这件事搞明白2.1 Node 版本选择与安装的坑Vue-Cli 的运行依赖 Node.js这是绕不过去的。我见过太多人卡在第一步装完 Node 之后命令行里敲vue提示“不是内部或外部命令”然后就懵了。这里面的核心问题通常出在两个地方一是 Node 版本太老二是全局安装路径没加到系统环境变量里。先说版本。Vue-Cli 4.x 要求 Node 版本至少是 8.9 以上Vue-Cli 5.x 则要求 Node 12 以上。我的建议是直接上 Node 16 或 18 的 LTS 版本别用最新版也别用太老的版本。为什么因为 Node 的奇数版本是实验性的偶数版本才是稳定版。你装个 Node 17 或者 19说不定哪个依赖就编译不过去。LTS 版本是长期支持版社区验证最充分踩坑概率最低。安装方式上Windows 用户直接去 Node 官网下载安装包一路下一步就行。Mac 用户我强烈建议用 nvm 来管理 Node 版本因为不同项目可能依赖不同的 Node 版本nvm 可以让你一键切换。安装完之后打开命令行敲node -v和npm -v能正常输出版本号就说明装好了。注意如果你之前装过 Node最好先卸载干净再装新版本。Windows 上残留的 npm 缓存目录经常导致各种诡异问题卸载后手动把C:\Users\你的用户名\AppData\Roaming\npm和npm-cache这两个文件夹删掉。2.2 npm 镜像源配置别让下载速度拖垮你的耐心Node 装好之后npm 默认用的是国外的源在国内下载依赖包的速度会让你怀疑人生。我试过不配镜像源直接npm install一个 Vue-Cli 项目初始化花了将近二十分钟中间还断了一次。后来换成国内镜像源同样的项目两分钟搞定。配置方法很简单一行命令npm config set registry https://registry.npmmirror.com敲完之后可以用npm config get registry确认一下是否生效。如果你用的是 yarn对应的命令是yarn config set registry https://registry.npmmirror.com。这里有个细节很多人不知道镜像源不是万能的有些包在镜像源上更新不及时或者压根没有。如果你发现某个包死活装不上可以临时切回官方源试试。另外公司内网环境可能有自己的私有源这个要提前问清楚别自己瞎配。2.3 Vue-Cli 的安装与版本确认环境准备好之后安装 Vue-Cli 本身npm install -g vue/cli-g表示全局安装这样你在任何目录下都能用vue命令。安装完成后敲vue --version或者vue -V能看到版本号就说明成功了。如果你之前装过旧版本的 Vue-Cli建议先卸载再装新的npm uninstall -g vue/cli npm install -g vue/cli实操心得有时候安装过程会卡住不动大概率是网络问题。可以先npm cache clean --force清一下缓存再重试。如果还是不行试试用cnpm或者yarn global add vue/cli。我个人的经验是 yarn 的全局安装比 npm 稳定一些尤其是在 Windows 上。3. 创建项目交互式选项背后的逻辑3.1 两种创建方式的选择Vue-Cli 创建项目有两种方式命令行交互式和图形化界面。图形化界面就是敲vue ui会打开一个浏览器页面你可以在页面上点来点去创建项目。这种方式适合完全的新手因为所有选项都有说明不用记命令。但我个人更推荐命令行方式原因很简单命令行更快而且你迟早要习惯在终端里干活。命令行创建项目的命令是vue create 项目名称项目名称不能用中文也不能有大写字母这是 npm 包命名规范决定的。我一般用短横线连接的小写英文比如my-vue-demo。3.2 预设选项怎么选默认 vs 手动敲完命令之后Vue-Cli 会问你选哪个预设。它给你两个选项默认预设和手动选择特性。默认预设是Babel ESLint适合快速创建一个简单项目。但如果你要做一个正经的、带路由和状态管理的项目默认预设是不够的。所以我的建议是选“手动选择特性”然后按需勾选。手动选择模式下你会看到这些选项Babel把 ES6 代码转成浏览器能识别的 ES5 代码必选。TypeScript如果你用 TS 就选不用就不选。新手建议先不选先把 Vue 本身搞熟。Progressive Web App (PWA) Support做 PWA 应用才选普通项目不用。Router路由做多页面应用必选。Vuex状态管理中大型项目必选小项目可以后加。CSS Pre-processorsCSS 预处理器选 Sass 或 Less看你团队习惯。Linter / Formatter代码规范检查强烈建议选。Unit Testing单元测试新手可以先不选。E2E Testing端到端测试同上。选完之后Vue-Cli 还会问你一些细节问题比如路由是否用 history 模式、选哪个 CSS 预处理器、选哪个 linter 配置。这些后面都能改不用太纠结。3.3 项目创建后的目录结构详解项目创建完成后你会看到这样一个目录结构my-vue-demo/ ├── node_modules/ # 依赖包目录 ├── public/ # 静态资源目录 │ ├── favicon.ico │ └── index.html # 入口 HTML 文件 ├── src/ # 源码目录 │ ├── assets/ # 静态资源会被 webpack 处理 │ ├── components/ # 公共组件 │ ├── views/ # 页面级组件 │ ├── router/ # 路由配置 │ ├── store/ # Vuex 状态管理 │ ├── App.vue # 根组件 │ └── main.js # 入口 JS 文件 ├── .eslintrc.js # ESLint 配置 ├── babel.config.js # Babel 配置 ├── package.json # 项目依赖和脚本 └── vue.config.js # Vue-Cli 配置文件可选这里面有几个地方新手容易搞混。public/index.html是整个项目的入口 HTMLVue 实例会挂载到这个文件里的某个元素上。src/main.js是入口 JS它负责创建 Vue 实例、挂载路由、挂载 store。src/App.vue是根组件所有页面都是它的子组件。assets和public的区别也经常有人问。简单说assets里的资源会被 webpack 处理比如图片会被压缩、小图会被转成 base64public里的资源是直接拷贝到打包目录的不会被处理。所以如果你有一些第三方库的静态文件或者需要保持原样的资源放public里。踩坑记录有一次我把一个很大的图片放在assets里结果打包的时候 webpack 把它转成了 base64导致 JS 文件巨大页面加载慢得要死。后来才知道超过一定大小的图片应该放public里或者配置 webpack 的url-loader阈值。这个阈值默认是 10KB超过就不会转 base64 了但如果你改过配置就要注意。4. 路由重定向与路由守卫让页面跳转听话4.1 路由重定向的三种写法路由重定向解决的是一个很实际的问题用户访问/的时候你希望他自动跳到/home或者/login。Vue Router 提供了几种重定向的写法。第一种是字符串写法const routes [ { path: /, redirect: /home }, { path: /home, component: Home } ]第二种是对象写法可以带参数{ path: /, redirect: { name: home } }第三种是函数写法可以动态判断{ path: /, redirect: to { // 根据条件返回不同的路径 return isLoggedIn() ? /dashboard : /login }}我一般用第一种简单直接。但如果你的重定向逻辑依赖用户登录状态那就得用函数写法。4.2 路由守卫的分类与执行顺序路由守卫是 Vue Router 里最容易让人迷糊的部分因为守卫有好几种执行顺序也不一样。我把它分成三类全局守卫、路由独享守卫、组件内守卫。全局守卫有三个beforeEach跳转前执行最常用一般用来做登录验证。beforeResolve跳转确认前执行在beforeEach和组件内beforeRouteEnter之后。afterEach跳转后执行没有next参数不能阻止跳转。路由独享守卫只有一个beforeEnter写在路由配置里只对当前路由生效。组件内守卫有三个beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave写在组件里。执行顺序是这样的全局beforeEach→ 路由独享beforeEnter→ 组件内beforeRouteEnter→ 全局beforeResolve→ 全局afterEach。4.3 登录验证的完整实现登录验证是路由守卫最典型的应用场景。我一般这样写router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })然后在需要登录的路由配置里加上meta: { requiresAuth: true }。这里有个细节next({ path: /login, query: { redirect: to.fullPath } })里的redirect参数是为了登录成功后能跳回用户原本想去的页面。登录页面在登录成功后读取这个参数然后router.push(redirect)就行了。注意事项beforeEach里一定要确保所有分支都调用了next()否则路由会卡住不动。我见过有人写了if分支忘了写else里的next()结果页面白屏排查了半天。5. Vuex 状态管理什么时候该用怎么用5.1 Vuex 的核心概念拆解Vuex 有五个核心概念State、Getter、Mutation、Action、Module。新手最容易搞混的是 Mutation 和 Action 的区别。State 就是全局数据相当于组件里的 data。Getter 相当于 computed用来派生状态。Mutation 是唯一能修改 State 的地方必须是同步函数。Action 用来处理异步操作它不能直接改 State而是通过提交 Mutation 来改。为什么要有这个区分因为 Vuex 想让数据变化可追踪。如果允许异步直接改 State那 devtools 里就看不到变化记录了。所以强制要求同步改数据走 Mutation异步操作走 ActionAction 里再提交 Mutation。5.2 一个完整的 Vuex 模块示例我拿一个用户模块举例// store/modules/user.js const state { userInfo: null, token: } const getters { isLoggedIn: state !!state.token, userName: state state.userInfo?.name || 未登录 } const mutations { SET_TOKEN(state, token) { state.token token }, SET_USER_INFO(state, info) { state.userInfo info } } const actions { async login({ commit }, { username, password }) { const res await api.login({ username, password }) commit(SET_TOKEN, res.token) commit(SET_USER_INFO, res.userInfo) return res }, logout({ commit }) { commit(SET_TOKEN, ) commit(SET_USER_INFO, null) } } export default { namespaced: true, state, getters, mutations, actions }namespaced: true很重要它让这个模块的 mutation 和 action 有了命名空间避免不同模块之间的命名冲突。调用的时候要写成store.dispatch(user/login, ...)。5.3 什么时候不该用 VuexVuex 不是万能的也不是所有项目都需要。我见过有人在只有两三个页面的小项目里硬上 Vuex结果代码复杂度反而增加了。我的判断标准是如果多个组件需要共享同一份数据而且这些组件之间没有直接的父子关系那就用 Vuex。如果只是父子组件通信用 props 和 emit 就够了。如果是兄弟组件通信可以用 EventBus 或者 provide/inject。另外Vuex 的数据在页面刷新后会丢失这是很多人踩过的坑。解决办法是把关键数据同步到 localStorage 或者 sessionStorage 里在 Vuex 初始化的时候从 storage 里读回来。6. ESLint 代码规范从抗拒到真香6.1 ESLint 的配置逻辑ESLint 的配置看起来复杂其实核心就三块规则集、解析器、插件。规则集决定了启用哪些规则比如eslint:recommended是官方推荐的基础规则plugin:vue/essential是 Vue 相关的必要规则。解析器决定了 ESLint 能理解哪些语法比如babel-eslint让 ESLint 能解析 ES6 语法。插件则提供了额外的规则比如eslint-plugin-vue提供了 Vue 模板的检查规则。Vue-Cli 创建项目时如果选了 ESLint它会自动生成一个.eslintrc.js文件。我一般会在这个基础上加几条自己的规则module.exports { root: true, env: { node: true }, extends: [ plugin:vue/essential, eslint:recommended ], rules: { no-console: process.env.NODE_ENV production ? warn : off, no-debugger: process.env.NODE_ENV production ? warn : off, semi: [error, never], quotes: [error, single] }, parserOptions: { parser: babel-eslint } }semi和quotes这两条规则是我个人偏好强制不用分号、强制用单引号。团队开发的话最好统一一下别一个人一个风格。6.2 自动修复与编辑器集成ESLint 最爽的功能是自动修复。命令行里加--fix参数npm run lint -- --fix它会自动修复那些能修复的问题比如缩进、分号、引号。不能自动修复的比如未使用的变量才会报错让你手动改。编辑器集成也很重要。VS Code 里装一个 ESLint 插件然后在设置里开启eslint.autoFixOnSave保存文件的时候自动修复。这样你写代码的时候基本感觉不到 ESLint 的存在但代码风格一直是统一的。实操心得ESLint 报错不要怕大部分都是格式问题。真正需要关注的是那些逻辑相关的规则比如no-unused-vars未使用的变量、no-undef使用未定义的变量。这些才是能帮你发现 bug 的规则。格式问题交给--fix就行。7. Element 与 Vue-Axios常用组件库和请求封装7.1 Element UI 的按需引入Element UI 是一个基于 Vue 的桌面端组件库提供了表格、表单、弹窗、日期选择器等常用组件。全量引入很简单但打包体积会很大。我一般用按需引入npm install babel-plugin-component -D然后在babel.config.js里配置module.exports { presets: [vue/cli-plugin-babel/preset], plugins: [ [component, { libraryName: element-ui, styleLibraryName: theme-chalk }] ] }这样你用到哪个组件就引入哪个打包的时候只打包用到的组件体积能小很多。7.2 Vue-Axios 的封装思路Axios 是一个 HTTP 请求库Vue-Axios 是它的 Vue 版本。但直接用 Axios 也行没必要非得用 Vue-Axios。我一般会封装一个 request.jsimport axios from axios import { Message } from element-ui const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 5000 }) service.interceptors.request.use( config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer ${token} } return config }, error { return Promise.reject(error) } ) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message || Error)) } return res }, error { Message.error(error.message || 网络异常) return Promise.reject(error) } ) export default service这个封装做了三件事统一 baseURL、请求拦截器里加 token、响应拦截器里统一处理错误。这样你在组件里调用接口就很简单import request from /utils/request export function getUserInfo() { return request({ url: /user/info, method: get }) }注意事项baseURL用环境变量配置不同环境用不同的值。Vue-Cli 里以VUE_APP_开头的环境变量会被自动注入到process.env里。你可以在项目根目录建.env.development和.env.production两个文件分别配置开发环境和生产环境的 baseURL。8. 常见问题与排查技巧实录8.1 项目启动报错排查表报错信息可能原因解决方法vue: command not foundVue-Cli 未全局安装或环境变量未配置重新npm install -g vue/cli检查 npm 全局路径是否在 PATH 里Module not found: Error: Cant resolve xxx依赖未安装或路径写错检查package.json里是否有该依赖路径大小写是否正确EADDRINUSE: address already in use端口被占用换端口npm run serve -- --port 8081或杀掉占用端口的进程Cannot read property xxx of undefined数据未初始化就使用在模板里用v-if判断或在 data 里给初始值ESLint: Parsing error语法错误或解析器配置不对检查babel.config.js和.eslintrc.js的 parser 配置8.2 路由跳转不生效的几种情况路由跳转不生效我遇到过至少四种原因。第一种是路由模式问题history 模式下需要服务端配置支持否则刷新页面会 404。第二种是路由守卫里没调next()路由卡住了。第三种是路由路径写错了比如多写了一个斜杠或者大小写不一致。第四种是嵌套路由的router-view没写对子路由的组件渲染不出来。排查的时候我一般先在beforeEach里console.log(to.path)看看路由有没有匹配上。如果匹配上了但页面没变那就是组件渲染的问题。如果压根没匹配上那就是路由配置的问题。8.3 Vuex 数据刷新丢失的解决方案Vuex 的数据存在内存里页面刷新就没了。解决办法是在 Vuex 初始化的时候从 localStorage 里读在 mutation 里同步写回 localStorage。我一般用 Vuex 的插件机制来做const store new Vuex.Store({ // ... plugins: [ store { const savedState localStorage.getItem(vuex-state) if (savedState) { store.replaceState(JSON.parse(savedState)) } store.subscribe((mutation, state) { localStorage.setItem(vuex-state, JSON.stringify(state)) }) } ] })但要注意不是所有数据都适合持久化。比如用户的登录 token 可以存但一些临时的表单数据就不适合存了反而会造成困扰。我一般只持久化 user 模块和 app 模块的部分数据。8.4 打包后页面空白的排查思路打包后页面空白打开控制台看到一堆 404这是最常见的问题。原因通常是publicPath配置不对。Vue-Cli 默认的publicPath是/如果你的项目部署在子目录下比如/my-app/那就得改成/my-app/。在vue.config.js里配置module.exports { publicPath: process.env.NODE_ENV production ? /my-app/ : / }另一个常见原因是路由模式。history 模式需要服务端把所有路径都指向index.html否则刷新子页面会 404。如果服务端不好配置可以改成 hash 模式就是在路由配置里把mode: history改成mode: hash。踩坑记录有一次打包后页面空白控制台没有任何报错。排查了半天发现是vue.config.js里配了productionSourceMap: false导致 source map 没生成报错信息定位不到源码。后来改成true才看到真正的报错。所以生产环境虽然建议关掉 source map 减小体积但排查问题的时候可以临时打开。9. 一些零散但重要的经验补充9.1 环境变量的使用规范Vue-Cli 支持.env文件来管理环境变量。以VUE_APP_开头的变量会被注入到客户端代码里其他的只在构建时可用。我一般建三个文件.env所有环境共用、.env.development开发环境、.env.production生产环境。.env里放一些通用的配置比如VUE_APP_TITLE我的项目。.env.development里放开发环境的 API 地址.env.production里放生产环境的。这样切换环境的时候不用改代码改配置文件就行。9.2 组件命名与目录组织组件命名我建议用大驼峰比如UserProfile.vue在模板里用短横线比如user-profile /。目录组织上页面级组件放views公共组件放components。如果组件多了可以在components下面再按功能分子目录比如components/form、components/table。9.3 性能优化的几个小点路由懒加载是最基本的优化把component: Home改成component: () import(/views/Home.vue)这样每个页面单独打包首屏加载更快。图片压缩也很重要大图尽量放public里或者用 webpack 的image-webpack-loader压缩。还有gzip压缩在vue.config.js里配compression-webpack-plugin服务端开启 gzip能显著减小传输体积。9.4 调试技巧Vue DevtoolsVue Devtools 是必装的浏览器插件它能让你看到组件树、Vuex 的 state 变化、路由信息。调试 Vuex 的时候特别有用你可以看到每次 mutation 前后的 state 快照定位数据变化非常方便。安装之后在浏览器开发者工具里会多一个 Vue 面板如果没显示检查一下是不是用了生产版本的 Vue生产版本默认不开启 devtools。9.5 关于 Vue-Cli 和 Vite 的选择最后说一个很多人纠结的问题新项目到底用 Vue-Cli 还是 Vite我的看法是如果是学习目的先用 Vue-Cli因为它把 webpack 的配置都封装好了你能更专注于 Vue 本身。如果是新起的生产项目Vite 的启动速度和热更新速度确实快很多而且配置更简单。但 Vue-Cli 的生态更成熟遇到问题更容易找到解决方案。我个人的做法是小项目和新项目用 Vite维护老项目继续用 Vue-Cli两者并不冲突。我在实际带新人的过程中发现Vue-Cli 这一套东西真正卡住人的从来不是某个 API 不会用而是对整个工程化流程没有概念。你不知道main.js里那几行代码在干什么不知道router和store是怎么挂上去的不知道打包的时候发生了什么。一旦你把这条链路走通一遍后面再学什么新工具都是触类旁通。所以我的建议是别光看文档自己动手从零创建一个项目把每个文件都打开看看把每行代码都试着改改报错了就查查不到就猜猜错了再改。这个过程比看十篇教程都管用。