Vue 3正式发布有一段时间了。我把项目从Vue 2迁移到了Vue 3。过程中踩了很多坑。有些坑让我熬了好几个夜。本文是我的踩坑记录。包括Composition API的坑、响应式系统的坑、组件库兼容的坑、构建工具的坑、TypeScript的坑等。每个坑都有原因分析和解决方法。希望能帮到正在用Vue 3的你。

一、写在前面

先说说我为什么迁移到Vue 3。

我们公司有一个中后台项目。用Vue 2开发了两年多。代码量越来越大。维护起来越来越困难。Vue 3发布之后。我们看了官方的介绍。觉得Composition API和更好的TypeScript支持能解决我们的很多痛点。就决定迁移到Vue 3。

最开始我觉得迁移应该不难。Vue 3大部分API和Vue 2是兼容的。结果真正开始迁移之后才发现。坑比想象的多得多。有些是Vue 3本身的变化。有些是生态还没跟上。有些是第三方库不兼容。

这篇文章就是我迁移过程中踩过的坑的记录。每个坑都让我熬了夜。希望能帮到大家。

二、Composition API的坑

Composition API是Vue 3最大的变化。也是坑最多的地方。

坑1:ref和reactive分不清

最开始用Composition API的时候。我分不清什么时候用ref什么时候用reactive。

ref用来包装基本类型。比如数字、字符串、布尔值。reactive用来包装对象。但是ref也可以包装对象。reactive也可以包装数组。这就容易混淆。

我最开始图省事。所有东西都用reactive。结果发现基本类型用reactive包装之后。解构会丢失响应式。而且函数返回的时候也不方便。

后来我总结了一个简单的规则:基本类型用ref。对象用reactive。但是如果对象可能被替换。比如整个对象重新赋值。那就用ref。因为reactive不能直接替换整个对象。

还有一个坑是ref在模板里会自动解包。不需要写.value。但是在script里必须写.value。这个很容易忘。忘了写.value就会出bug。而且很难发现。因为ref对象打印出来是一个对象。不是你期望的值。

坑2:解构reactive丢失响应式

这是一个经典的坑。比如你有一个reactive对象。const state = reactive({ name: '张三', age: 20 })。然后你解构它。const { name, age } = state。这时候name和age就不是响应式的了。修改它们不会触发视图更新。

我最开始不知道这个。写了一个组件。解构了props里的reactive对象。结果数据变了视图不更新。查了半天才发现是解构导致的。

解决方法是用toRefs。const { name, age } = toRefs(state)。这样解构出来的还是ref。保持响应式。或者用computed来包装。const name = computed(() => state.name)。

坑3:watch和watchEffect的区别

Vue 3有watch和watchEffect两个API。最开始我分不清什么时候用哪个。

watch是显式指定要监听的数据源。回调里可以拿到新值和旧值。watchEffect是自动收集依赖。回调里用到了哪些响应式数据就监听哪些。但是拿不到旧值。

我最开始都用watchEffect。觉得方便。结果发现有些时候不需要监听的数据也被监听了。导致回调执行次数过多。而且有些时候我需要旧值来做对比。watchEffect拿不到。

后来我就分情况用。需要精确控制监听什么。或者需要旧值的时候用watch。简单的副作用用watchEffect。

还有一个坑是watch监听reactive对象的时候。默认是深度监听的。如果你只想监听对象的某个属性。要写成() => state.name。而不是直接写state。否则对象里任何属性变了都会触发回调。

坑4:setup里没有this

Vue 2的组件里到处都是this。this.data、this.methods、this.$refs、this.$router。到了Vue 3的setup里。没有this了。

最开始我很不习惯。想访问props要从setup的参数里拿。想访问router要用useRouter。想访问store要用useStore。想拿DOM引用要用ref。

而且有些Vue 2的插件是挂在this上的。比如this.$axios、this.$message。到了Vue 3里不能这么用了。需要改成导入函数的方式。

这个坑让我熬了好几个夜。因为很多旧代码都是用this写的。迁移的时候要一个个改。而且有些第三方插件还没适配Vue 3。用不了。

三、响应式系统的坑

Vue 3用Proxy重写了响应式系统。比Vue 2的Object.defineProperty强大很多。但是也有一些坑。

坑5:reactive不能直接替换整个对象

前面提到过。reactive对象不能直接替换。比如const state = reactive({ name: '张三' })。然后state = { name: '李四' }。这样是不行的。因为state是一个const。而且替换之后新对象不是响应式的。

如果你需要替换整个对象。要么用ref包装对象。const state = ref({ name: '张三' })。然后state.value = { name: '李四' }。要么就修改对象的属性。state.name = '李四'。

这个坑我踩过好几次。每次都是数据变了视图不更新。查了半天才发现是替换了reactive对象。

坑6:数组的响应式

Vue 2里数组的响应式有局限。不能直接用索引修改数组。Vue 3用Proxy之后。数组的响应式完善了。可以直接用索引修改。也可以修改length。

但是有一个坑。如果你把一个普通数组赋值给reactive对象的属性。这个数组会变成响应式的。但是如果你用ref包装数组。然后替换整个数组。新数组也是响应式的。这个和Vue 2不一样。需要注意。

还有一个坑是用reactive包装数组的时候。数组的方法比如filter、map返回的是新数组。不是响应式的。如果你需要保持响应式。要把结果重新赋值回去。或者用computed。

坑7:shallowReactive和shallowRef

Vue 3提供了shallowReactive和shallowRef。只有第一层是响应式的。深层不是。性能更好。

我最开始不知道有这两个东西。所有对象都用reactive。结果大对象的响应式开销很大。性能不好。后来了解到shallowReactive之后。把一些不需要深层响应式的大对象改成了shallowReactive。性能提升了不少。

但是要注意。用了shallowReactive之后。修改深层属性不会触发更新。需要手动触发。或者替换整个对象。

四、组件库兼容的坑

Vue 3的生态还在完善中。很多组件库还没完全适配。这是迁移过程中最大的坑。

坑8:Element UI不兼容Vue 3

我们项目用的是Element UI。结果Element UI不支持Vue 3。官方出了Element Plus来支持Vue 3。但是API有一些变化。不是完全兼容的。

我们花了很多时间把Element UI的组件改成Element Plus。有些组件的props变了。有些事件名变了。有些插槽的用法变了。一个个改非常痛苦。

而且Element Plus当时还在开发中。有些组件还没有。有些功能不完善。bug也比较多。我们还临时自己实现了几个组件。

所以如果你的项目重度依赖某个UI库。迁移之前一定要确认这个库有没有Vue 3版本。API变化大不大。否则迁移成本会很高。

坑9:第三方组件不兼容

除了UI库。我们还用了很多第三方组件。比如富文本编辑器、图表库、日历组件等。这些组件很多都没有Vue 3版本。

有些组件我们找到了Vue 3的替代品。但是API不一样。要重写。有些组件没有替代品。我们只能自己封装。或者用Vue 3的兼容模式来用Vue 2的组件。但是兼容模式有性能损失。而且不是所有Vue 2组件都能在兼容模式下正常运行。

这个坑让我们熬了最多的夜。因为每个第三方组件都要调研。要测试。要改。有些实在用不了的还要自己写。

坑10:Vue 2的插件不能用

Vue 2有很多好用的插件。比如vue-router、vuex、vue-i18n等。这些插件在Vue 3里都有新版本。但是API变了。

比如vue-router 4。创建路由的方式从new VueRouter变成了createRouter。模式从mode变成了history。导航守卫的用法也有一些变化。

vuex 4也有变化。创建store从new Vuex.Store变成了createStore。在组件里用从this.$store变成了useStore。

这些变化虽然不大。但是代码量多的话。一个个改也很花时间。而且要仔细测试。确保没有遗漏。

五、构建工具的坑

Vue 3推荐用Vite作为构建工具。我们也从webpack迁移到了Vite。过程中也踩了一些坑。

坑11:Vite和webpack的差异

Vite用的是ES模块。开发环境下不打包。直接用浏览器的原生模块加载。这和webpack完全不一样。

最明显的差异是CommonJS模块的处理。webpack可以直接require CommonJS模块。Vite不行。需要用@rollup/plugin-commonjs来转换。有些老的npm包只有CommonJS格式。在Vite里用会出问题。

还有环境变量的处理。webpack用process.env。Vite用import.meta.env。而且变量名必须以VITE_开头。否则不会暴露给客户端。我们最开始有很多环境变量没改名。结果读不到。

还有全局变量的处理。webpack可以用ProvidePlugin自动导入。Vite需要用配置或者自己导入。有些库依赖全局变量。比如jQuery的$。在Vite里需要额外配置。

坑12:开发环境和生产环境不一致

Vite开发环境用的是esbuild。生产环境用的是rollup。两者的行为不完全一致。有些代码在开发环境正常。打包之后就出问题。

我们遇到过一次。开发环境下某个第三方库正常。打包之后报错。查了很久才发现是rollup的tree-shaking把一些代码删掉了。导致库不能正常工作。最后在配置里把这个库设为external。或者用noExternal来解决。

所以用Vite的时候。一定要定期打生产包测试。不要只在开发环境测试。否则到了上线的时候才发现问题。就来不及了。

六、TypeScript的坑

Vue 3对TypeScript的支持比Vue 2好很多。但是也有一些坑。

坑13:props的类型定义

Vue 3里定义props的类型。可以用TypeScript的类型注解。比如defineProps<{ name: string, age: number }>()。这种方式很简洁。但是不能设置默认值。

如果需要默认值。要用withDefaults。withDefaults(defineProps<{ name?: string }>(), { name: '默认值' })。这个API最开始我不会用。查了文档才知道。

还有一个坑是props的类型是运行时校验和编译时类型的结合。如果你用了类型注解。就不能再用数组或者对象的方式定义props了。两者不能混用。

坑14:ref的类型推断

ref的类型推断有时候会出问题。比如const count = ref(0)。类型会被推断为Ref<number>。这个没问题。但是如果你写const data = ref(null)。类型就是Ref<null>。后面赋值对象的时候会报类型错误。

这时候需要显式指定类型。const data = ref<Data | null>(null)。或者用reactive。

还有一个坑是ref在模板里自动解包。但是TypeScript不知道。所以在模板里用ref的时候。类型可能会报错。需要用一些技巧来处理。或者用Volar插件来获得更好的类型提示。

坑15:emit的类型定义

Vue 3里emit也可以定义类型。defineEmits<{ (e: 'change', value: string): void }>()。这样在调用emit的时候会有类型检查。

但是这个语法比较复杂。最开始我不会写。还是用的数组方式。defineEmits(['change'])。这样没有类型检查。调用emit的时候参数错了也不会报错。

后来学会了类型定义的方式。代码的健壮性提升了不少。但是写起来确实比数组方式麻烦。

七、其他坑

坑16:生命周期钩子的变化

Vue 3的生命周期钩子变了。beforeDestroy变成了beforeUnmount。destroyed变成了unmounted。而且在setup里要用函数式的钩子。比如onMounted、onUpdated、onUnmounted。

最开始我习惯性地写beforeDestroy。结果不执行。查了半天才发现改名了。而且在setup里不能用选项式的生命周期钩子。必须用函数式的。

还有一个坑是setup里的生命周期钩子可以调用多次。比如写两个onMounted。两个都会执行。这个和选项式不一样。选项式的生命周期只能写一个。

坑17:v-model的变化

Vue 3里v-model的用法变了。组件上的v-model默认对应的prop是modelValue。事件是update:modelValue。而不是Vue 2里的value和input。

而且Vue 3支持多个v-model。比如v-model:name和v-model:age。分别对应不同的prop和事件。这个功能很强大。但是和Vue 2不兼容。

我们迁移的时候。很多自定义组件的v-model都要改。prop名从value改成modelValue。事件从input改成update:modelValue。一个个改很麻烦。

坑18:$listeners被移除了

Vue 2里有$listeners。可以把父组件的事件监听器传给子组件。Vue 3里$listeners被移除了。因为事件监听器现在是attrs的一部分。

如果你在Vue 2里用了v-on="$listeners"。到了Vue 3里要改成v-bind="$attrs"。因为attrs里包含了属性和事件监听器。

这个变化最开始我不知道。导致一些封装的组件事件传不下去。查了很久才发现是$listeners被移除了。

八、给准备迁移的人的建议

如果你准备从Vue 2迁移到Vue 3。我有几个建议。

1. 先评估生态兼容性

迁移之前先评估你用的所有第三方库和组件。看看有没有Vue 3版本。API变化大不大。如果有很多库没有Vue 3版本。或者API变化很大。就要慎重考虑迁移的成本。

2. 用兼容模式逐步迁移

Vue 3提供了兼容模式。可以在Vue 3里运行Vue 2的代码。这样可以逐步迁移。不用一次性全部改完。

先把项目升级到Vue 3的兼容模式。然后逐个组件迁移。这样风险比较小。出了问题也容易定位。

3. 先学Composition API再动手

Composition API是Vue 3的核心。如果不熟悉Composition API。迁移的时候会很痛苦。建议先花时间学一下Composition API。写几个小demo练练手。然后再开始迁移项目。

4. 做好测试

迁移过程中很容易出bug。一定要有完善的测试。单元测试、集成测试、手动测试都要有。每迁移一个组件就测试一个。不要等全部迁移完了再测试。那时候出了问题都不知道是哪里改坏的。

5. 不要急于用新特性

Vue 3有很多新特性。比如Teleport、Suspense、Fragment等。迁移的时候先保证功能正常。不要急于用新特性。等迁移完成稳定了。再慢慢引入新特性。

九、写在最后

从Vue 2迁移到Vue 3。我们花了一个多月的时间。踩了无数的坑。熬了很多夜。但是迁移完成之后。确实感受到了Vue 3的好处。

Composition API让代码复用更方便。逻辑可以抽离成composable。比Vue 2的mixin清晰多了。更好的TypeScript支持让代码更健壮。Proxy响应式系统让一些Vue 2的局限消失了。性能也有提升。

虽然迁移过程很痛苦。但是我觉得是值得的。Vue 3代表了前端的未来。早迁移早受益。

这篇文章记录了我迁移过程中踩过的主要的坑。希望能帮到正在迁移或者准备迁移的你。让你少熬几个夜。

最后用一句话结束本文:"Vue 3很好。但是迁移有风险。动手需谨慎。"愿每一个前端开发者都能顺利用上Vue 3。享受它带来的便利。