2016年那会儿我刚入行前端,第一次打开 Vue 2 的文档就被简洁的 API 吸引住了。几年过去,Vue 3 早已成了新项目的默认选项,但你要是现在去接手一个企业级项目,或者去看那些还在稳定迭代的系统,大概率碰到的还是 Vue 2。这篇不是从零教学,而是我在持续维护 Vue 2 项目时沉淀下来的一份全景笔记:安装环境、依赖管理、路由插槽、样式折叠、m3u8 播放、地图集成、SSE 对接、打包含进 Spring Boot、源码怎么交给别人、面试常问什么,全都有。目标是帮你少踩我踩过的坑。
1. 为什么还在用 Vue 2:技术选型前的冷静思考
1.1 存量项目远比想象中多
很多人一听到 Vue 2 就是“该淘汰了”,但现实是,大量长期运营的后台管理系统、中台项目、老电商站点,核心代码还是 Vue 2。改造成本不是写两行代码那么简单:老的第三方组件库、同事习惯的写法、已经稳定运行多年的业务逻辑,都成了继续使用 Vue 2 的理由。尤其是那些基于 vue-cli 3/4、Element UI、Vant 2 搭建的项目,内部系统迭代几年后,直接升 Vue 3 的风险远大于收益。
我自己手上就有这样的项目,每年都要往上叠加需求。比起一门心思迁移,更实际的做法是把 Vue 2 的机制吃透,把该修的坑修好,把老代码维护利索。对新入行的朋友,我的建议也很直白:如果你的目标是快速进入团队干活,Vue 2 绝对是绕不开的一课;如果是从零做新项目,再考虑 Vue 3 也不迟。
1.2 Vue 2 与 Vue 3 核心差异速览
面试时被问到“Vue 2 和 Vue 3 的区别”,不要只答“Vue 3 更快”。核心区别要从三个角度看:
- 响应式原理:Vue 2 用 Object.defineProperty 拦截对象的属性读写,数组通过重写方法实现;Vue 3 用 Proxy 直接拦截对象整体操作,支持新增属性、删除属性、Map/Set 等数据类型。
- API 风格:Vue 2 主推 Options API,data/methods/computed/watch 分段写;Vue 3 同时提供 Composition API,把关联逻辑聚合到一起。
- 构建与性能:Vue 3 代码基于 ES Module,利用 tree-shaking 按需打包,运行时体积更小;Vue 2.7 虽然也支持组合式 API,但底层仍是旧响应式方案。
Nuxt 和 Vue 的区别也常被混为一谈。Vue 是一个前端视图层框架,只负责页面渲染;Nuxt 是基于 Vue 的上层框架,帮你解决服务端渲染(SSR)、文件路由、SEO、中间件等工程化问题。Nuxt 2 对应 Vue 2,Nuxt 3 对应 Vue 3。简单类比:Vue 是发动机,Nuxt 是给你装好发动机、轮子、导航的整车。
1.3 一条务实的 Vue 2 学习路线
结合我带新人和面试候选人的经验,整理一条比较快的路线:先装环境跑通一个 demo,再啃模板语法、组件通信、生命周期;然后学路由和状态管理;接着看工程化配置,理解打包;最后找一个真实业务场景完整做一遍。顺序一定不要反,很多人上来就啃源码,结果连 computed 缓存特性都没搞明白。
这块就是热词里“vue 快速学习路线”的本意。我的建议是给自己定一个目标,比如两周之内做一个带登录、列表、详情、权限路由的 Vue 2 后管页面,比看十遍文档都管用。
2. 环境搭建与依赖安装:每个新手都要过的一关
2.1 初始化一个 Vue 2 项目的方式
目前初始化 Vue 2 项目有两条主流路径:用 vue-cli 4,或者用 Vite 加官方插件。vue-cli 老牌稳妥,命令很简单:
npm install -g @vue/cli vue create project-name选预设的时候,注意选择 Vue 2 版本,别顺手选了 Vue 3。vue-cli 会生成完整的 Webpack 构建配置,开箱即用,适合业务项目。
如果不想用 Webpack,也可以用 Vite 搭配官方插件:
npm create vite@latest project-name -- --template vue npm i vite@3 vue@2.7 @vitejs/plugin-vue2这里特别提醒:Vite 3 配 @vitejs/plugin-vue2 是稳定组合,Vite 4/5 不一定兼容 Vue 2 插件,这类版本错位问题排查起来很花时间,尽量按官方维护矩阵走。
2.2 安装依赖时最容易忽视的版本锁定问题
{Vue 2 项目依赖安装}有句口诀:先把 package.json 发给同事,别急着 npm install。因为很多老项目的依赖树经不起升级,npm 默认会按语义化版本拉新,如果某天一个子依赖悄悄升了主版本,整个构建就崩了。我的习惯是项目里同时维护 package-lock.json 和 .npmrc,安装命令固定为:
npm config set registry https://registry.npmmirror.com npm install --legacy-peer-deps这里解释一下 --legacy-peer-deps 的作用。老项目里经常出现 Element UI 要求 peer 依赖 vue@^2.x,但 npm 7 以上会严格检查,导致安装失败;加了这个参数就跳过 peer 依赖校验,代价是可能忽略某些依赖冲突,所以装完一定要跑一遍构建和测试。
新手常见问题:拉下来的项目,npm install 半天装不上,最后发现是全局 Node 版本太高。Vue 2 + webpack 4 项目建议 Node 14/16,别轻易上 Node 18+,不然 node-sass 编译直接报错。
2.3 failed to load tsconfig '“@vue/tsconfig/tsconfig.web.json"':这个报错怎么处理
写 TS 的 Vue 2 项目偶尔会碰上这么一句:failed to load tsconfig '@vue/tsconfig/tsconfig.web.json': tsconfig not found。这个热词本身已经说明了场景。
我遇到的情况一般是两类:一是引用 @vue/tsconfig 这个包,但 node_modules 里根本没装,或装到了错误的版本;二是 monorepo 结构下相对路径解析失败。解决步骤按顺序来:
npm i -D @vue/tsconfig npx tsc --showConfig先确认 @vue/tsconfig 的真实路径,再到 tsconfig.json 里改成可解析的相对路径。比如在 monorepo 的 apps/web 里,引用要写成 ../../packages/... 这种相对路径,不能写绝对包名。
还有一类情况是 vue-tsc 版本和 @vue/tsconfig 版本不匹配,导致 exports 字段不允许导入该子路径。解决方案是固定两边版本,或者把 include 改成更宽容的配置。
2.4 VSCode 里 .vue 标签跳转失效的解决方案
热词里有“vscode中点vue中的标签没有跳转”,这问题几乎每个 Vue 2 项目都碰到过。原因很简单:Vue 2 和 Vue 3 推荐的插件不一样,很多人装的是 Volar,但 Volar 对 Vue 2 的支持并不好。
我做 Vue 2 项目的插件组合是 Vetur,而不是 Volar。Vetur 装好之后,如果要支持在 template 里点击组件名跳到对应文件,需要手动开启一个隐藏配置:
"vetur.experimental.templateInterpolationService": true搞完这个,按住 Ctrl 点击组件标签就能跳转。如果还不生效,检查 .vue 文件是否被 Vetur 识别,命令面板里输入 Vue: Restart Language Server,重启语言服务基本就好了。
3. 路由、插槽与样式折叠:Vue 2 组件的三大核心
3.1 从静态路由到动态路由
Vue 2 的路由用 vue-router,版本对应关系是 vue-router@3.x 配 Vue 2,版本一定不能装成 4.x,否则一启动就报匹配不到组件。基础路由很简单:
import Vue from 'vue' import Router from 'vue-router' import Home from './views/Home.vue' Vue.use(Router) export default new Router({ mode: 'history', routes: [ { path: '/', name: 'home', component: Home } ] })动态路由常见的应用场景是权限系统。登录后拿到菜单数据,通过 addRoutes 动态注入。注意 addRoutes 是 Vue Router 3 的 API,Router 4 改成了 addRoute;Vue 2 项目里不要用错。动态路由的核心思路是:把权限映射成每个路由的 meta,比如 meta: { roles: ['admin'] },然后在全局前置守卫里做判断。
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { next('/login') } else { next() } })3.2 路由参数传递:query 和 params 的区别与坑
Vue 2 路由参数是面试高频题,实际开发里几乎天天用。query 和 params 的区别要记牢:
| 方式 | 写法 | URL 表现 | 刷新后 |
|---|---|---|---|
| query | this.$router.push({ path: '/detail', query: { id: 1 } }) | /detail?id=1 | 不丢 |
| params + name | this.$router.push({ name: 'detail', params: { id: 1 } }) | /detail(不显示参数) | 丢 |
| 路径参数 | this.$router.push({ path: '/detail/1' }),路由定义 /detail/:id | /detail/1 | 不丢 |
最容易踩的坑是第二种,params 配合 name 跳转,页面刷新后 this.$route.params.id 变成 undefined。如果你希望刷新后参数还在,要么用 query,要么用路径参数。本质上是因为非路径的 params 只存在于当前内存路由记录中,刷新后路由重新匹配,找不到对应参数。
实战里我倾向于把关键参数都用路径或 query 传递,params 只用来传一些非必选的辅助状态。组件里接收参数最好用 props 解耦:
props: ['id']路由配置加上 props: true,这样组件不依赖 $route,测试和复用都方便。
3.3 插槽的三种形态:基础组件封装的关键
插槽是 Vue 2 组件封装里最顺手也最容易被忽略的能力。默认插槽最直白,父组件往子组件标签里塞内容,子组件用 接住。具名插槽解决一个组件多处需要自定义区域的场景,比如 dialog 的头部、底部按钮,用 name 区分。
作用域插槽才是重点,它让父组件能拿到子组件内部的数据。Vue 2.6 之后统一了 v-slot 写法:
<!-- 子组件 --> <slot name="item" :data="itemData"></slot> <!-- 父组件 --> <template v-slot:item="{ data }"> <span>{{ data.name }}</span> </template>开发表格、下拉、列表这类通用组件时,作用域插槽几乎是刚需。它可以让你在不改子组件源码的前提下,把每一行的渲染逻辑完全交给业务方。面试里提到的“slot vue”基本都是这个知识点。
3.4 内容折叠展开:一个容易做丑的交互
Vue 2 里做折叠展开,最基础的方案是 v-show 或 v-if。v-show 适合频繁切换但内容不重的场景,v-if 适合内容里包含大量组件、不想初始化的场景。但用户想要的很可能是带动画的流畅折叠,这里就要用 transition 和 height 动画。
我的做法是给容器设置 max-height,展开时从 0 过渡到内容高度。这个方案的缺点是内容高度不固定时,需要 JS 动态测量 scrollHeight:
this.isOpen ? (box.style.height = box.scrollHeight + 'px') : (box.style.height = 0)过渡结束之后再把 height 改成 auto,避免窗口缩放时内容被截断。另一个更现代的方案是用 CSS grid 的 fr 技巧:
.fold { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .3s; } .fold.open { grid-template-rows: 1fr; } .fold > .inner { overflow: hidden; }这个方案不需要 JS 测量高度,动画还平滑,兼容性也可以接受。Vue 2 里给 .fold 动态绑定 open class 就行。
注意“vue div超出隐藏”也是同一类样式问题。单行超出隐藏是 text-overflow: ellipsis,多行要用 -webkit-line-clamp;但如果这个 div 是 flex 布局的 item,记得给 min-width: 0,不然依旧会撑破布局。这个坑我替你们踩过了。
4. 业务集成实战:视频、图表、地图、上传
4.1 Vue 2 中播放 m3u8 流媒体
“vue播放m3u8免安装”这词儿点出了一个常见需求:网页直接播 HLS 格式视频。原生 video 标签默认不解析 .m3u8,必须要借助 hls.js 来做。安装方式:
npm install hls.js组件里的核心逻辑是:拿到视频元素引用,创建 Hls 实例,监听 URL 并绑定:
if (Hls.isSupported()) { const hls = new Hls() hls.loadSource(videoSrc) hls.attachMedia(this.$refs.video) this.hls = hls }注意两点:一是 Hls.isSupported() 在 Safari 里是 false,因为 Safari 原生支持 HLS,这种情况直接走原生 video.src 就行;二是 beforeDestroy 里一定要 hls.destroy(),否则播放器实例残留会导致内存泄漏和音频继续播放。
如果不想用 npm 安装,也可以直接在 index.html 里通过 script 标签引入 hls.min.js 的 CDN,Vue 2 里通过 window.Hls 使用,功能完全一致。这是“免安装”的常见做法。还有一个容易混淆的点,热词里“vue .acc播放器”其实大概率说的是音频播放器,音频用原生 audio 标签直接 src 就能播,不涉及 m3u8。
4.2 ECharts 双柱状统计图:从初始化到销毁
“vue中用echarts画两个柱状统计图”是后台项目最高频的可视化需求。ECharts 接入 Vue 2 的思路不要搞复杂,直接在 mounted 里初始化:
this.chart = echarts.init(this.$refs.chart) this.chart.setOption({ legend: { data: ['本月', '上月'] }, xAxis: { data: ['1月', '2月', '3月'] }, yAxis: {}, series: [ { name: '本月', type: 'bar', data: [120, 200, 150] }, { name: '上月', type: 'bar', data: [80, 140, 110] } ] })双柱状图的关键配置:series 数组里放两个 bar 对象,xAxis 要共用,否则会出现两组坐标。样式上可以控制 barWidth 和 barGap,两个柱子之间留一点间距更美观。
真正要注意的是生命周期。图表所在的容器如果初始是隐藏状态,初始化时宽度会是 0,画出来就是一张废图。解决办法:容器显示后再调用 this.chart.resize(),或者用 nextTick 延迟初始化。还有,beforeDestroy 里一定要 this.chart.dispose(),卸载组件时清掉 echarts 实例,不然切页多了,页面卡到怀疑人生。窗口 resize 事件也要在 mounted 里绑定、beforeDestroy 里解绑,并且 resize 时加个防抖。
4.3 Canvas 2D 在 Vue 2 中的正确用法
Canvas 2D 跟 Vue 的“数据驱动视图”思路天然冲突,因为 Canvas 本质是命令式绘制,我每次用的时候都会先跟团队说清楚:不要指望 Vue 帮我们画 Canvas,我们要在合适的时机自己调用 Canvas API。
Vue 2 项目里最常见的玩法是给 canvas 元素加 ref,然后在 mounted 里拿 DOM 画:
const canvas = this.$refs.canvas const ctx = canvas.getContext('2d') ctx.fillStyle = '#409EFF' ctx.fillRect(20, 20, 100, 100)如果是画动态图形,比如跑一个粒子动画,需要 requestAnimationFrame 循环,同时要记住每帧清屏。组件销毁时 cancelAnimationFrame,否则后台页面挂着,几十个画布还在疯狂重绘,CPU 直接拉满。
Canvas 在 Vue 2 里另一个常见结合是图表填echarts;但有些轻量场景,比如签名板、图片水印、验证码,纯 Canvas 更灵活,不用引入大库。不过要注意,canvas 的绘制坐标不受 Vue 响应式驱动,一旦业务数据变化,必须手动重新绘制,所以最好把绘制逻辑封装成一个方法,数据变化后用 watch 触发重绘。
4.4 地图集成:Mapbox 和腾讯地图在 Vue 2 中的落地
热词里有两个地图相关:mapbox vue、用在 vue 里的腾讯地图。先说 Mapbox,安装 mapbox-gl 之后,在组件里初始化:
import mapboxgl from 'mapbox-gl' mapboxgl.accessToken = 'YOUR_TOKEN' this.map = new mapboxgl.Map({ container: this.$refs.map, style: 'mapbox://styles/mapbox/streets-v11', center: [116.397428, 39.90923], zoom: 12 })Mapbox 的坑在于 token 和样式 URL,免费额度有限,超过请求量会黑屏。腾讯地图相对省心一些,直接用 TMap 和它的 key:
this.map = new TMap.Map(this.$refs.map, { center: new TMap.LatLng(39.90923, 116.397428), zoom: 12 })不管哪家地图,共同注意点:容器一定要有明确的高度,否则地图初始化为 0 高度,只有滚动条没有图;初始化不要放在隐藏的 tab 页里,否则地图渲染错乱,需要调用 resize 重新计算;组件销毁前调用 map.remove() 或 destroy 释放资源。
4.5 多文件上传:从原生 Input 到组件化
Vue 2 的多文件上传,最简单的实现是 input 加 multiple:
<input type="file" multiple @change="handleUpload" />选中多个文件后,遍历 files 列表,逐个转 FormData 后调用 axios:
for (const file of this.fileList) { const formData = new FormData() formData.append('file', file) await api.upload(formData) }如果项目里用了 Element UI,el-upload 组件本身就是封装好的多文件上传统一方案,给 action 指定接口地址,on-success 里处理返回结果。进阶玩法是大文件切片:把文件拆成若干片并发上传,最后调一个合并接口。这一块面试常被追问,我会在最后总结里提一下思路,实现上核心就是 Blob.slice 和并发控制。
5. 前后端对接与部署:SSE、Spring Boot 和源码交付
5.1 Vue 2 + Python SSE:流式数据的正确接法
SSE(Server-Sent Events)在聊天、进度条、实时通知场景里很常用。“vue python sse”这个热词说明了很多人是在 Python 后端项目里碰到 SSE。前端常规用法是 EventSource:
const source = new EventSource('/api/stream') source.onmessage = (event) => { console.log(JSON.parse(event.data)) }EventSource 的优点是自带断线重连,缺点是只能用 GET 请求,没法加自定义请求头。如果你的 Python 后端接口是 POST,或者需要带 token 认证,就得绕到 fetch 流式读取:
const response = await fetch('/api/stream', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }, body: JSON.stringify({ params: 1 }) }) const reader = response.body.getReader() const decoder = new TextDecoder() while (true) { const { value, done } = await reader.read() if (done) break const text = decoder.decode(value) // 按行解析 data: 前缀,再 JSON.parse console.log(text) }SSE 的格式是连续的 text/event-stream,服务端通常发data: {...}\n\n。前端拿到的是原始字节流,需要自己按行切割和解析。我这里踩过最大的坑是忘了处理拆包和粘包,因为网络传输过程中一个 JSON 可能被拆成多次传输,也可能多个 data 合在一次返回里。稳妥做法是维护一个缓冲区,把 chunk 先拼接,再按换行符切出完整事件。
5.2 Vue 2 打包放进 Spring Boot:两个致命细节
“vue打包放进springboot中”是典型的单体部署方式。思路很简单:Vue 2 项目 npm run build 生成 dist,把 dist 复制到 Spring Boot 的 src/main/resources/static 下,启动后端就能访问。但有两个细节能让你一夜白发。
第一个是 publicPath。Vue 2 的 vue.config.js 里默认 publicPath 是 '/',如果你的后端在根路径部署没毛病,但只要 Spring Boot 配了 context-path,比如 /admin,静态资源请求就会 404。正确配置:
module.exports = { publicPath: process.env.NODE_ENV === 'production' ? '/admin/' : '/' }第二个是路由的 history 模式和刷新 404。你 router 用了 history 模式,在 Spring Boot 里直接刷新 /admin/user 页面时会 404,因为后端没这个静态文件。解决方案是提供一个 fallback:让 Spring Boot 对所有不匹配 /api 的路径都转发到 index.html。这个用 Java Config 写一个 WebMvcConfigurer 就能搞定。如果不想动后端,也可以把 mode 改成 hash,URL 会带个 #,刷新不会 404,体验稍差但省事。
5.3 项目源码怎么发给别人:交接不是甩包裹
“vue项目源码怎么发给别人”这个热词背后,其实是交接协助的问题。我先说结论:不要把整个项目文件夹压缩发过去,因为 node_modules 动辄几百 MB,对方电脑环境跟你不一样,大概率跑不起来。
标准做法是把源码(不含 node_modules)打成压缩包,保留 package-lock.json 或 yarn.lock,再写一个 README,记录 Node 版本、依赖安装命令、启动命令、环境变量、后端接口地址。如果项目用了 .env.development 之类的环境配置,记得把示例文件也带上,把敏感字段脱敏。
如果双方都懂 Git,最好直接推一个仓库分支,让对方 clone。另外,有一些“老项目病”是交接专用问题:比如依赖里带 node-sass,换电脑就装不上。这种依赖在安装前要优先处理,要么换成 sass(dart-sass),要么把 Node 版本固定成能编译的那一个。源码交接这件事,真正做到位,是让对方 clone 下来执行两条命令就能跑起来。
6. 高频面试题速查:Vue 2 背这些就够
6.1 响应式原理:Object.defineProperty 的局限
Vue 2 面试必问响应式原理。Vue 2 初始化时遍历 data,通过 Object.defineProperty 给每个属性添加 getter/setter,同时为每个对象创建一个依赖收集器 dep。render 过程中读取属性会触发 getter,把当前 watcher 收集进 dep;属性改变时触发 setter,通知 watcher 重新渲染。
这个机制的硬伤是:直接给对象添加一个新属性 obj.newKey = 1,Vue 检测不到,因为没有 getter/setter。数组也是,只有 7 个会改变数组自身的方法被重写过,直接 arr[0] = 'xx' 不会触发更新。解决办法就是大家熟悉的 this.$set(obj, 'newKey', value)。这个原理解释清楚了,后面一系列坑都能想通。
6.2 生命周期顺序:父子组件谁先跑
Vue 2 生命周期顺序也是热门题。单个组件的顺序是 beforeCreate -> created -> beforeMount -> mounted -> beforeDestroy -> destroyed,配合 keep-alive 还有 activated/deactivated。面试更喜欢问父子组件嵌套时的顺序:父 beforeCreate、父 created、父 beforeMount、子 beforeCreate、子 created、子 beforeMount、子 mounted、父 mounted。销毁顺序反过来,先父 beforeDestroy,再子 beforeDestroy,最后子 destroyed 和父 destroyed。
理解这个顺序对实际开发很重要。在 created 里拿接口数据,在 mounted 里操作 DOM,在 beforeDestroy 里清除定时器、取消事件监听、销毁图表实例,这些都是 Vue 2 项目的日常。我在项目里见过有人在 mounted 里拿不到 DOM 高度,基本就是没理解异步渲染和 nextTick 的关系。
6.3 组件通信方式全盘点
组件通信的完整答案包括:
- props / $emit:父子组件单向数据流。
- $refs / $parent / $children:直接访问实例,简单但侵入性强。
- $attrs / $listeners:祖先组件给孙组件传属性和事件,常用于二次封装第三方组件。
- provide / inject:跨层级注入,适合封装高阶组件,但不具备响应式,使用时注意。
- EventBus:平级通信或跨组件通信,用 new Vue() 做事件中心,适合简单场景但容易混乱。
- Vuex:全局状态管理,适合多组件共享、异步流程复杂的场景。
我个人建议:业务状态尽量走 Vuex,UI 联动多级组件时优先用 $attrs/$listeners,不要一上来就搞 EventBus,后期维护成本太高。
6.4 v-if 和 v-show 的区别、computed 和 watch 的分工
v-if 是真正的条件渲染,表达式为 false 时组件不渲染,用到时才创建;v-show 是 CSS display 控制,元素会一直存在。高频且频繁切换的场景用 v-show,内容重且不需要初始化的场景用 v-if。computed 有缓存,依赖不变不会重新计算;watch 用于观察数据变化然后执行副作用,适合异步操作。一句经验:能用 computed 解决的问题不要用 watch,watch 写多了,数据流会变得难以追踪。
7. 常见问题排查与实践心得
7.1 高频问题排查一览表
| 问题场景 | 大概率原因 | 处理建议 |
|---|---|---|
| Vue 2 项目启动白屏 | 路由 mode 是 history,且后端没配 fallback | 改 hash 模式,或后端转发 index.html |
| npm install 报 peer 依赖冲突 | npm 7 严格校验 | 加 --legacy-peer-deps 安装 |
| node-sass 编译报错 | Node 版本或 python 环境不一致 | 换 Node 16 或改 dart-sass |
| 页面刷新后 params 丢失 | 用了 name + params 且非路径参数 | 改用 query 或路径参数 |
| tap 事件在移动端 300ms 延迟 | 未引入 fastclick 或 viewport 不合适 | 检查 meta viewport,必要时引入 fastclick |
| img 标签无法显示 PDF | img 本身不支持 PDF 渲染 | 用 iframe 或 vue-pdf 展示 |
| VSCode 点击组件标签不跳转 | Vue2 项目装了 Volar | 换 Vetur,并开启 experimental.templateInterpolationService |
| .vue 文件 tsconfig 报错找不到 @vue/tsconfig | 依赖缺失或版本不匹配 | 重新安装对应版本,路径改相对路径 |
| 后台页面切换后图表空白 | echarts 实例销毁或 resize 未处理 | beforeDestroy 里 dispose,窗口 resize 绑定后再解绑 |
| 折叠动画不平滑 | height 从 0 到 auto 无法过渡 | 用 scrollHeight 动态赋值,或 grid-template-rows |
| div 多行省略失效 | flex 子项挤压 | 加 min-width: 0,配合 overflow: hidden |
| m3u8 在 Safari 播不了 | Safari 原生支持 HLS,hls.js 不启用 | 判断 Hls.isSupported() 后走原生 video.src |
| Spring Boot 打包后静态资源 404 | publicPath 未适配 context-path | 配置 publicPath 为绝对路径前缀 |
7.2 几条压箱底的经验
说实话,我把这些写下来的时候,脑子里飘过的都是一个个加班排查的场景。做 Vue 2 项目,我最深切的体会是:它不是一个完美的框架,但它足够成熟,成熟到你遇到的所有问题,几乎都有人在网上记录过。关键是你能不能搜对关键词,能不能用最小代价找到答案。
有一句话我必须多说一遍:所有涉及第三方实例的东西,图表、地图、播放器、Canvas 动画,在组件销毁前必须做清理。Vue 2 的页面跳转非常频繁,destroyed 里不清理,内存就像漏水一样一点点漏干,等用户反馈页面越来越卡,你根本查不出是哪一屏的锅。
另一个经验是:老项目升级依赖,一定要小步慢跑。先锁版本,再逐个升级,每次升级都跑一遍构建和核心流程。我见过同事把 Element UI 从 2.13 升到 2.15,结果一个 table 的默认行为变了,十几个页面出问题,比迁移 Vue 3 还痛苦。稳定压倒一切,这大概就是 Vue 2 项目教会我的第一课。