Vue 3 都出来这么久了怎么还在搭 Vue 2这个问题我一年里被问了不下十次。答案很现实手上的存量大头是 Vue 2团队沉淀了两三年的公共组件库是 Vue 2甚至有些项目还跑在 vue-cli 3 的老脚手架上你不可能一夜之间全部推倒重来。所以如何搭建一个 vue2 项目这件事本质上不是怀旧而是很多前端同学每周都要面对的日常——新开一个后台管理、验证一个业务想法、给老系统做模块拆分第一件事都是先把架子立起来。这篇内容我按真能照着做一遍的标准来写从本机环境怎么配、脚手架怎么选、命令行怎么走、目录和配置文件每一项为什么要这么改到路由、状态管理、请求层三件套的接入顺序再到跑通之后的几类高频报错比如Network: unavailable、Permissions Policy violation: unload is not allowed怎么一步步排查。中间还会穿插 Vue 2 生命周期钩子的执行顺序以及 Vue 2 和 Vue 3 到底差在哪、什么时候值得升级。不管你是刚入门、第一次独立起项目还是带人干活、需要一份可复用的骨架方案看完应该都能直接上手抄作业。1. 先对齐口径你说的Vue2 项目到底是哪一种很多人一上来就问装哪个版本的脚手架其实在动手之前有一件更重要的事先确认你要搭的是哪一类 Vue 2 项目。这三种形态的搭建路径差别很大选错了后面全是返工。1.1 三种常见的 Vue 2 项目形态第一种是标准的后台管理系统SPA 单页应用vue-router vuex axios用 vue-cli 生成打包后丢到静态服务器或者塞进后端工程的 static 目录。这是绝大多数人嘴里的Vue 2 项目本文也主要围绕它展开。第二种是多页面应用MPA一个工程里有十几个 entry比如官网 活动页 帮助中心打包在一起这种就要在vue.config.js里改pages配置而不是用默认的单入口。第三种是uni-app 或 HBuilderX 模板项目虽然底层也是 Vue 2 语法但它的构建链路是 HBuilderX 自己的一套pages.json、manifest.json才是核心配置跟 webpack 那一套基本不搭界。我见过不少人把第三种的问题拿到第一种的语境里问结果在vue.config.js里折腾半天发现根本没生效——因为 HBuilderX 的 uni-app 项目压根不读这个文件。所以第一步先确认形态能省掉后面一半的沟通成本。1.2 版本组合的雷区清单Vue 2 生态最折磨人的地方在于版本必须门当户对。vue和vue-template-compiler的版本号必须完全一致差一个补丁号都可能报Vue packages version mismatch这个坑每年都有人踩。组合类型vue编译器vue-loaderwebpackNode 建议经典存量项目2.6.xvue-template-compiler 2.6.x必须一致15.x4.x14 / 16较新的 Vue2 骨架2.7.x2.7 内置编译器可不装15.10 及以上5.x16 / 18vue-cli 5 默认产出2.7.x 或 2.6.x视选项而定由脚手架管理5.x16 以上如果你的项目是 Vue 2.7官方已经在核心包里内置了编译器vue-template-compiler可以去掉vue-loader升到 15.10 以上就能直接用vue/compiler-sfc。这一步不做npm run serve起来就会在编译.vue文件时报错而且报错信息通常指向 loader新手很容易误以为是 loader 装漏了。1.3 选脚手架还是自己手搓 webpack我的建议很直接默认用 vue-cli别手搓。手搓 webpack 配置唯一的价值是我真的需要极致定制比如要跟公司已有的构建平台对接、要做特殊的产物目录结构。但从零配一套 Vue 2 的 webpack你要处理 vue-loader、babel、postcss、css 提取、热更新、publicPath、sourcemap 这一长串问题没有两天时间很难做利索而且后面加 SSR、加多页、加按需 polyfill 时还要来回改。vue-cli 的抽象层次刚刚好默认配置帮你兜住 90% 的通用场景需要定制时用configureWebpack和chainWebpack两个口子插进去就行。真要拆开看vue inspect webpack.config.js能把最终的 webpack 配置完整导出来对着改反而比手搓更快。除非你明确知道自己在做什么否则别在这上面浪费时间。2. 环境地基Node、包管理器和依赖源怎么配才不翻车搭建失败十次里面有六次是环境问题不是代码问题。这一节的内容看起来枯燥但它决定了你后面半小时是顺顺利利还是不断重装。2.1 Node 版本决定脚手架的上限vue/cli5.x 要求 Node 12 以上但能跑和好用是两码事。Node 14 和 16 是跟 vue-cli 配合最默契的两个版本尤其是那些还在用 webpack 4 的存量项目Node 18 以上经常在crypto、openssl相关的依赖上翻车报错长得像天书error:0308010C:digital envelope routines::unsupported。遇到这种报错最快的解法不是硬改代码而是切回 Node 16。所以我的习惯是每个项目锁定一个 Node 大版本。用 nvm 或者 nvm-windows 在项目根目录放一个.nvmrc里面只写一行16.20.2进目录先nvm use。团队协作时这一行能省掉无数次我这跑得起来你那跑不起来的扯皮——注意.nvmrc在一些环境下不生效所以脚本里做一次版本校验更靠谱node -e const vprocess.versions.node.split(.)[0]; if(v!16){console.error(请使用 Node 16当前为 process.version); process.exit(1)}把这段塞进package.json的preinstall或者 CI 脚本里版本不对直接拦住比等到编译一半崩掉友好得多。2.2 包管理器怎么选npm、yarn、pnpm 都能装 Vue 2 项目区别在依赖树的处理方式。npm 7 以后会自动安装 peerDependencies这在 Vue 2 生态里反而容易出问题——某些老包声明的 peer 版本跟你实际装的对不上npm 会直接报ERESOLVE让你没法继续。遇到这种情况别急着一路--force和--legacy-peer-deps糊过去那样会生成一棵畸形的依赖树跑起来各种诡异。我的实际选择是以 yarn 1.x 为主pnpm 用在确定没有幽灵依赖的新项目上。yarn 1.x 对老包的容忍度好锁文件也直观pnpm 的硬链接机制省磁盘、装得快但它严格限制未声明依赖的访问有些 Vue 2 老包偷偷引用了没写进dependencies的模块用 pnpm 就会报Cannot find module。这不是 pnpm 的错是那些包的错但你没法去改人家的源码只能换包管理器绕过去。一个关键原则一个项目只用一个包管理器并且把 lock 文件提交到仓库。不要出现package-lock.json和yarn.lock并存的情况那等于给未来的自己埋雷。2.3 依赖源与安装速度国内安装node-sass、chromedriver、puppeteer这类带二进制下载的包时卡住是常态因为它们会去往默认地址拉预编译文件跟 npm registry 的源是两套东西。常见的处理方式有两条一是把二进制包的下载地址通过环境变量指向国内的镜像二是直接绕开node-sass改用纯 JS 实现的sassdart-sass。# 示例指定部分二进制包的镜像地址 npm config set sass_binary_site https://registry.npmmirror.com/-/binary/node-sass npm config set chromedriver_cdnurl https://registry.npmmirror.com/-/binary/chromedriver我更推荐第二条路新项目直接上sass别碰node-sass。node-sass需要跟 Node 版本严格对应Node 一升级就得重装而sass是纯 JS 的装上就能用成本几乎为零。存量项目迁移时要注意sass-loader的版本也得跟着调具体对应关系在第 6 节会展开。3. 命令行一路走通从装脚手架到首屏渲染环境备好了接下来就是把项目真正生成出来。这一步看着简单但交互式选项里每一问都有讲究选错了后面还得改配置。3.1 全局安装与卸载旧版本先确认本机是不是有旧版脚手架残留。如果装过 vue-cli 2 或者 3直接装新版容易出现命令冲突。# 查一下当前版本 vue --version # 如果有旧版本先卸干净 npm uninstall -g vue-cli npm uninstall -g vue/cli # 装 5.x写死大版本避免以后团队环境漂移 npm install -g vue/cli5 # 再次确认 vue --version注意包名Vue 2 时代的vue-cli和后来的vue/cli是两个不同的包前者早就停止维护了装它必然出问题。如果vue --version输出版本号后面还跟着一长串类似路径的东西说明你装的是非常老的版本赶紧卸掉重来。3.2 交互式选项逐条选择及理由直接敲vue create my-vue2-app会进入交互流程。第一条问你选 preset一定要选Manually select features别用默认选项——默认选项在新版里会自动给你 Vue 3 的模板。后续几个关键问题的选择理由Choose a version of Vue.js这里选2.x。这是全文最关键的一次点击选错了后面所有关于 Options API 的写法都会报错。Use history mode for router看部署方式。后端能配合做 URL 重写就选Y用 hash 模式带#就选N。很多内部系统为了省事直接选N也没什么问题。Pick a linter / formatter config小团队选ESLint Prettier人多了统一风格很重要一个人写的小工具选ESLint with error prevention only就够别让格式问题打断思路。Where do you prefer placing config for Babel, ESLint选In dedicated config files把配置拆到babel.config.js、.eslintrc.js里比全塞在package.json里可读性好太多。Save this as a preset for future projects选Y起个名字下次直接vue create -p 你的预设名 项目名一步到位。3.3 用 preset 文件跳过全部交互团队里最推荐的做法是把上面的答案固化成 preset 文件提交到代码仓库所有人用同一条命令生成项目。这样能彻底消灭你装的跟我装的不一样的问题。{ useConfigFiles: true, plugins: { vue/cli-plugin-babel: {}, vue/cli-plugin-router: { historyMode: true }, vue/cli-plugin-vuex: {}, vue/cli-plugin-eslint: { config: prettier, lintOn: [save] } }, vueVersion: 2 }把它保存成preset.json然后vue create --preset ./preset.json my-vue2-app即可。重点看vueVersion: 2这一行它就是控制生成 Vue 2 还是 Vue 3 模板的开关。如果忘了写或者写成3生成出来的就是 Vue 3 骨架但目录结构跟你预期完全不一样。3.4 HBuilderX 和图形化界面的两条小路如果你只是想做移动端页面、用 HBuilderX 开发那走的是另一条路新建项目时选普通项目 - Vue 项目或者直接选 uni-app 空模板Vue 2 语法照样能用配置集中在pages.json和manifest.json。这类项目不方便用 npm 管理依赖也不太适合复杂的工程化改造适合快速验证一个页面想法。另一条路是vue ui会启动一个本地图形化界面鼠标点点就能创建项目、加插件、跑构建还能看构建分析报告。新手用它对理解项目里到底有哪些东西很有帮助但长远看还是要回到命令行因为脚本化和 CI 集成都依赖命令。3.5 生成后的目录逐项拆解脚手架跑完之后先别急着写业务花十分钟把目录看明白。路径作用常见修改点public/index.html页面模板改 title、加 CDN、加统计脚本src/main.js应用入口挂全局组件、全局样式、插件src/App.vue根组件通常只放router-view/src/router/index.js路由表路由懒加载、权限前置守卫src/store/index.js状态仓库拆模块、加持久化src/assets会被构建处理的静态资源图片、全局样式变量public/下的其他文件原样拷贝不参与构建需要保留原文件名的资源vue.config.js项目级构建配置自己新建脚手架不会生成这里有个新手最容易搞混的点assets里的图片走 webpack 处理会被哈希重命名、可以做压缩public里的文件是原样拷贝过去的路径写死。需要动态拼接路径引用的图片放public需要参与打包优化的放assets。4. 目录之外的硬功夫把默认配置改成能上生产的骨架脚手架给你的配置是能跑不是能上生产。这一节说的每一项改动我都在真实项目里被坑过所以写的是结论而不是建议。4.1 vue.config.js 的常用字段脚手架不会自动生成这个文件自己在根目录建一个。下面这份是我常用的起点// vue.config.js const path require(path) module.exports { // 部署到子目录时必须改比如部署在 /admin/ 下就写 /admin/ publicPath: process.env.NODE_ENV production ? /admin/ : /, outputDir: dist, assetsDir: static, // 生产环境关掉 sourcemap一是体积二是别把源码暴露出去 productionSourceMap: false, lintOnSave: process.env.NODE_ENV ! production, devServer: { port: 8080, open: true, // 接口转发解决开发阶段的跨域 proxy: { /api: { target: http://127.0.0.1:9000, changeOrigin: true, pathRewrite: { ^/api: } } } }, configureWebpack: { resolve: { alias: { : path.resolve(__dirname, src) } } }, css: { loaderOptions: { sass: { // 全局注入变量文件省得每个 .vue 都 import additionalData: import /assets/styles/variables.scss; } } } }三个高频坑点publicPath改错会导致白屏资源 404productionSourceMap不关会让打包体积翻好几倍additionalData注入了带实际 CSS 输出的文件会导致样式重复几十遍——所以注入的文件里只能放变量和 mixin不能放具体样式类。4.2 环境变量与多环境构建.env系列文件是区分开发、测试、生产的关键。有一点必须记住只有以VUE_APP_开头的变量才会被注入到前端代码里其他变量只存在于构建进程中前端读不到。# .env.development NODE_ENVdevelopment VUE_APP_API_BASE/api VUE_APP_TITLE运营后台开发 # .env.production NODE_ENVproduction VUE_APP_API_BASEhttps://api.example.com VUE_APP_TITLE运营后台代码里用process.env.VUE_APP_API_BASE读取。想加一套测试环境就建.env.staging然后配一条脚本build:stage: vue-cli-service build --mode staging。这里有个容易忽略的点.env里的值是编译期字符串替换不是运行时读取所以改了.env必须重启 dev server 或者重新构建才生效热更新不会带上新值。4.3 打包体积的三板斧项目做大了打包产物动辄好几 MB首屏白屏三五秒。我一般按这个顺序优化第一按需引入 UI 组件库。全量引入 Element UI 打进去接近 1MB按需引入能砍掉一大半配置好babel-plugin-component之后基本无感。第二用 externals 把不变的库挂到 CDN。vue、vue-router、vuex、axios这几个版本稳定的库直接走外链构建产物会明显变小configureWebpack: { externals: { vue: Vue, vue-router: VueRouter, vuex: Vuex, axios: axios } }同时在public/index.html里用script引入对应的 CDN 地址。注意 externals 之后本地开发环境也要能拿到这些全局变量否则一启动就Vue is not defined。我的处理方式是只在生产模式启用 externals开发环境照常走 npm 依赖。第三拆包和路由懒加载。路由表里用() import(...)写法页面级代码自动分片再用chainWebpack配一下splitChunks把node_modules里的东西按使用频率分组避免一个改动导致整个 vendor 缓存失效。5. 业务三件套路由、状态管理和请求层的接入顺序骨架搭好之后接下来是接入真正的业务基础层。顺序很重要先路由再状态最后请求。因为路由决定了页面结构状态管理要按页面模块拆请求层要先知道接口的鉴权方式。5.1 路由懒加载与权限占位Vue 2 配的是 vue-router 3.x4.x 是给 Vue 3 用的装错版本会直接报Cannot read property install of undefined。路由表我一般这样组织import Vue from vue import VueRouter from vue-router Vue.use(VueRouter) const routes [ { path: /login, component: () import(/views/login/index.vue) }, { path: /, component: () import(/layout/index.vue), children: [ { path: dashboard, name: Dashboard, component: () import(/views/dashboard/index.vue), meta: { title: 工作台, requireAuth: true } } ] }, { path: *, component: () import(/views/404.vue) } ] const router new VueRouter({ mode: history, base: process.env.BASE_URL, routes })注意base: process.env.BASE_URL这一行它跟vue.config.js里的publicPath是联动的。只改了一边history 模式下刷新页面必 404。另外mode: history需要服务端配合做重写把所有路径都指回index.html做不到就用 hash 模式别硬扛。5.2 vuex 的分模块与持久化小项目一个store/index.js就够了但只要模块超过三个就必须拆文件了。我的拆法是一模块一个目录包含state、mutations、actions、getters四块最后统一注册import Vue from vue import Vuex from vuex import user from ./modules/user import permission from ./modules/permission Vue.use(Vuex) export default new Vuex.Store({ modules: { user, permission }, getters: { token: state state.user.token } })刷新丢状态是 Vue 2 后台项目的经典问题因为 token 存在内存里F5 一下全没了。最省事的方案是配合vuex-persistedstate把需要的字段写进 localStorage但要注意别把整个 store 都持久化那些跟权限树、列表缓存相关的状态写进去会导致用户切换账号后看到上一个人的数据。我的做法是只在plugins里声明白名单明确指定哪几个字段可以落地。5.3 axios 封装与统一错误处理请求层不要在每个页面里裸写 axios统一收口到一个utils/request.js。这里的关键不是代码多优雅而是把所有后端返回格式的差异都挡在这一层。import axios from axios import { Message } from element-ui import store from /store import router from /router const service axios.create({ baseURL: process.env.VUE_APP_API_BASE, timeout: 15000 }) service.interceptors.request.use(config { if (store.getters.token) { config.headers[Authorization] Bearer store.getters.token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.message || 请求失败) if (res.code 401) { store.dispatch(user/logout).then(() router.push(/login)) } return Promise.reject(new Error(res.message || Error)) } return res.data }, error { Message.error(error.message) return Promise.reject(error) } ) export default service这里有个必须提醒的点401 的处理要防重复弹窗。并发请求同时返回 401 时会弹七八个错误提示体验极差。做法是加一个标志位跳转登录只执行一次其他请求直接静默失败。另外timeout设置要跟后端对齐设得太短会在文件导出这类慢接口上误报。5.4 顺手提一个文本文件在线预览有段时间总有人在问 Vue 2 里怎么做 txt 在线预览。其实不需要什么特殊组件核心就两步把文件内容取回来放进pre标签里渲染同时保留原始换行。async function loadText(url) { const buf await fetch(url).then(r r.arrayBuffer()) // 注意编码中文文本如果是 GBK直接当 UTF-8 解会乱码 const decoder new TextDecoder(gbk) return decoder.decode(buf) }踩过的坑有两个一是编码判断老系统导出的文本很多是 GBK用默认的 UTF-8 解出来全是问号TextDecoder指定编码就能解决二是大文件几十兆的文本直接塞进 DOM 会卡死浏览器得做虚拟滚动或者只渲染前 N 行。如果文件在跨域的不同源上还得让服务端配合放行这个比代码本身更容易卡住。6. 跑通之后才是真正的开始几类高频报错的排查链路这一节是我最想写的部分。项目搭起来之后真正的麻烦往往出现在第二天打开电脑的时候——昨天还好好的今天就不行了这类问题最耗时间因为看起来毫无规律。6.1 Network: unavailable 到底是怎么回事启动 dev server 后控制台打出这样一行App running at: - Local: http://localhost:8080/ - Network: unavailable先明确一点这个提示不影响本机开发localhost:8080照样能访问Hot reload 也正常所以如果你只是自己开发完全可以忽略。它的真实含义是脚手架在尝试获取本机局域网 IP 时没拿到结果所以没法给你打印一个手机也能访问的地址。它的取值逻辑是通过本机网络接口信息拿一个非环回的 IPv4 地址拿不到就打印 unavailable。所以排查链路是看是不是网卡全都禁用了。有些同学为了省电把 Wi-Fi 关了只插网线或者反过来结果系统只剩环回接口自然拿不到 IP。启用任意一块物理网卡再重启 dev server。看虚拟网卡是不是干扰了结果。装了虚拟机、Docker Desktop 之后系统里会多出一堆虚拟网卡某些环境下取值会落到这些不可用的接口上表现就是能打印出 IP 但手机访问不了。这种情况手动指定 host 更靠谱。手动指定 host 兜底。在vue.config.js里写devServer.host: 0.0.0.0或者启动时加参数vue-cli-service serve --host 0.0.0.0这样会监听所有接口局域网内其他设备就能访问。手机还是连不上就查防火墙。开发机上的防火墙会拦截局域网对 8080 端口的访问这时候需要给 Node 进程放行或者临时关闭对应规则。注意公司办公网经常做了终端隔离同一网段也互相访问不了这属于网络策略问题不是代码问题。以前可以突然就不行了这个描述八成对应的是第 1 或第 2 条中间装了什么软件、开了什么虚拟网卡、插拔过什么设备。所以我的排查习惯是先看网络接口列表再决定要不要动配置而不是上来就改vue.config.js。6.2 Permissions Policy violation: unload is not allowed控制台出现这样一条警告Permissions Policy violation: unload is not allowed in this document.这个不是 Vue 的错跟你写的业务代码也基本没关系。现代浏览器出于性能考虑逐步收紧了对unload事件的支持某些文档环境尤其是被嵌套的 iframe或者配置了相关权限策略的页面里注册unload监听会被拒绝浏览器就把这条提示打出来。排查步骤是这样的先在控制台点开这条警告看调用栈指向哪个文件。绝大多数情况下会落到某个第三方脚本上常见的有埋点 SDK、旧版富文本编辑器、基于 jQuery 的图表插件。定位到是谁之后处理方式分两种如果是你自己写的代码把window.addEventListener(unload, fn)换成window.addEventListener(pagehide, fn)或者document.addEventListener(visibilitychange, fn)。这两个事件在页面隐藏、切换、关闭时都会触发语义上更可靠也不会被策略拦截。如果是第三方库注册的先看有没有新版本修复了这个问题没有的话可以在入口文件里做一层兜底// main.js 最顶部拦截掉不支持的 unload 注册 const rawAdd window.addEventListener window.addEventListener function (type, listener, options) { if (type unload) { // 降级成 pagehide业务逻辑照样能跑 return rawAdd.call(this, pagehide, listener, options) } return rawAdd.call(this, type, listener, options) }这样做的好处是不用改第三方代码坏处是全局改写原生方法属于能用但有副作用的方案上线前最好在真机上多测几轮。能用官方修复就别用这招。6.3 样式预处理器和 Node 版本的连环坑再列一组我在 Vue 2 项目里遇到频率最高的报错基本都跟依赖版本有关报错关键词根本原因处理方向Vue packages version mismatchvue 与 vue-template-compiler 版本不一致两者对齐到同一版本号Node Sass version x is incompatiblenode-sass 与 Node 版本不匹配换用 dart-sass或降到匹配的 Nodeerror:0308010C:digital envelope routines::unsupportedNode 17 的 OpenSSL 变化切回 Node 16或设置旧版 OpenSSL 兼容参数Cannot find module node-sass二进制包没下载成功配置镜像地址或直接换 sassModule build failed: sass-loadersass-loader 与 webpack 大版本不匹配webpack 4 用 sass-loader 10 及以下webpack 5 用 12 以上处理这类问题的通用思路是别一个个试版本先看依赖树。npm ls sass-loader能告诉你当前实际生效的是哪个版本是不是被别的包提升上来了。很多时候你在package.json里写了 10.x实际装的是 8.x因为某个子依赖锁死了版本。这种时候用resolutions字段yarn强制指定比手动删node_modules靠谱得多。6.4 缓存、锁文件和幽灵依赖删了node_modules重装一下这句话虽然听起来敷衍但它确实能解决相当一部分玄学问题。原因有三一是缓存里的包损坏二是锁文件和你本地实际安装的不一致三是依赖被提升后产生了幽灵依赖——你的代码引用了某个没写进package.json的包平时能用一旦依赖树结构变化就崩。我的做法是准备两条命令日常小改动只想刷新依赖用npm ci严格按锁文件装怀疑依赖树坏了才用彻底清理# 彻底重置 rm -rf node_modules package-lock.json yarn.lock yarn install最后这条会重新生成锁文件所以只在确认是依赖问题时用别在正常开发中随便跑——它可能把别人锁好的版本悄悄升上去。锁文件是团队资产改了要提交并说明原因。7. 生命周期钩子与 Vue2/Vue3 差异容易含糊的几处写 Vue 2 的时候有两个知识点几乎每周都会用到但又常常被含糊带过生命周期钩子的执行顺序以及 Vue 2 和 Vue 3 到底差在哪里。7.1 八个钩子的执行顺序和实战用途Vue 2 的生命周期钩子一共八个不含 keep-alive 和错误捕获相关的执行顺序是固定的钩子触发时机典型用途beforeCreate实例初始化数据还没挂载几乎不用插件初始化偶尔用created数据观测完成$el还不存在发请求、初始化非 DOM 数据beforeMount模板编译完成还没替换到页面极少用mountedDOM 挂载完成操作 DOM、初始化图表、绑定事件beforeUpdate数据变了DOM 还没更新更新前记录滚动位置updatedDOM 更新完成注意别在这里改数据会死循环beforeDestroy实例销毁前还能访问数据清定时器、解绑事件、取消请求destroyed实例销毁后收尾清理父子组件的顺序要单独记父beforeCreate→ 父created→ 父beforeMount→ 子beforeCreate→ 子created→ 子beforeMount→ 子mounted→ 父mounted。这个顺序决定了在父组件的mounted里能安全访问到子组件的 DOM反过来在子组件的mounted里访问父组件的数据要小心可能还没准备好。另外两个容易被忽略的钩子activated和deactivated配合keep-alive使用。前者在组件被激活时触发适合刷新列表数据后者在组件被缓存休眠时触发适合暂停轮询、保存草稿。很多人用 keep-alive 之后发现页面数据不更新就是因为把刷新逻辑写在了mounted里而mounted在整个生命周期中只会执行一次。errorCaptured也值得一提它能捕获子孙组件的错误返回false可以阻止错误继续向上传播。做全局错误上报的时候很好用但要注意它不能捕获异步回调里的错误。7.2 Vue2 和 Vue3 的核心差异对照被问得最多的问题就是Vue2 和 Vue3 有什么区别这里给一张对照表都是我实际写代码时会碰到的差异点对比项Vue 2Vue 3响应式原理Object.defineProperty递归劫持Proxy代理整个对象数组/对象新增属性需要this.$set直接赋值即可组件写法Options API 为主Options API Composition API根节点必须单根支持多根Fragment卸载相关钩子beforeDestroy/destroyedbeforeUnmount/unmounted全局 APIVue.use/Vue.mixincreateApp().use()事件 API$on/$off/$once已移除需用第三方事件库$listeners独立存在合并进$attrsv-modelvalueinputmodelValueupdate:modelValue过滤器filters支持已移除改用计算属性静态资源提升无编译期标记更新粒度更细其中对日常开发影响最大的是响应式原理。Vue 2 用Object.defineProperty逐个属性劫持所以对数组下标赋值、给对象新增属性都不生效必须用this.$set。这个差异直接导致了无数我明明改了数据视图没更新的问题。Vue 3 换成Proxy之后整个对象的读写都能被拦截这类问题基本消失了。7.3 什么时候值得动手升级我的判断标准很简单新项目直接上 Vue 3存量项目看 ROI。如果只是维护状态、半年内没有大改版计划别升升级过程中踩到的坑和消耗的时间远超收益。如果项目要接新功能、要长期演进可以把升级排进规划并且用官方的兼容构建vue/compat做渐进式迁移——它允许你在 Vue 3 的运行环境里跑大部分 Vue 2 的写法逐个组件改写而不是一次性全量重写。如果项目是 uni-app 那套升级路径就更特殊要用新版本的工具链来编译 Vue 3 模式同时注意第三方组件库的兼容性很多老组件在 Vue 3 编译模式下会出现属性透传异常。这类项目我的建议是先在一个旁支页面上试点跑通别在主流程上直接切。8. 上线前我自己会过一遍的检查清单项目能跑不等于能交付。下面这份清单是我每次上线前都会逐条确认的每一条都对应过真实事故。构建产物相关。publicPath跟部署路径是否一致尤其是部署在子目录的场景productionSourceMap是否关掉静态资源有没有被 CDN 缓存住旧版本这个最坑改了代码用户还是看到老页面通常是因为index.html被缓存了要单独给它设置不缓存。打包产物大小看一眼如果单文件超过 1MB回头看第 4 节的优化三板斧。运行环境相关。环境变量是不是被打进代码里了打开产物搜一下接口地址如果搜不到说明是编译期替换掉了这是正常的生产环境的接口地址、上传地址、第三方 key 是否正确axios的baseURL有没有因为环境切错打到测试环境去。这一条我建议做成发布前的一次人工确认自动化很难覆盖。代码质量相关。console.log和调试用的debugger有没有清干净尤其是那些会打印用户信息的日志路由有没有漏配 404 兜底beforeDestroy里的定时器和事件监听有没有清这类问题在单页应用里会累积成内存泄漏跑一整天之后页面越来越卡。兼容性相关。目标浏览器范围在browserslist里写清楚browserslist变了会直接影响 babel 转译和 polyfill 的注入范围。如果还要兼容旧版浏览器core-js的引入方式和按需注入策略都要跟着调别指望默认配置能覆盖所有场景。最后分享一个我自己的习惯每搭一个新项目就把这次踩到的坑追加到一个私人的NOTES.md里记下当时的报错原文和最终解法。搭到第五第六个项目的时候你会发现绝大多数问题都是同一批翻笔记比搜索快十倍。这套骨架我也习惯直接抽成模板仓库新项目git clone之后改个名字就能开工省下来的时间足够把业务逻辑多写两遍。