迁移到vue3
新特性
开始之前,先介绍一下vue3的新特性:
- Composition API。一种更好的逻辑复用和代码组织方式
- Teleport。提供一种简洁的方式可以指定它里面内容的父元素
- Fragments。vue3中组件支持拥有多个根元素
- Global API。vue3中将全局API抽取成为独立函数,已支持tree shaking
- 全面的TypeScript支持
- ...
更多特性以及使用方法请参考官方文档。
本文介绍的迁移方案是基于vue的单文件组件(SFC),如果是jsx/tsx的迁移,请参考Jsx for vue3
改动点
我们之前的项目都是基于vue2.x,如果要迁移到vue3中,有一些需要注意的地方
-
template和style几乎不需要改动,vue3的style也有一些新的变化,详情可查看Style Features。
-
vue3仍然向前兼容,如果是保留Options API方式的,组件中的js代码几乎不需要改动,有改动的地方如下(不完整,更多细节请参考向Vue2迁移):
-
使用createApp()代替了new Vue()
import { createApp } from 'vue'; import App from './index.vue'; const app = createApp(App); app.mount("#app");
-
vue3没有Vue全局变量,原先挂载到Vue.prototype上的属性,可以用app.config.globalProperties替代
import { createApp } from 'vue'; import axios from 'axios'; import App from './index.vue'; const app = createApp(App); app.config.globalProperties.$axios = axios; // Vue.prototype.$axios = axios; app.mount("#app");
-
vue3没有Vue全局变量,一些全局api的用法发生了改变
import { createApp } from 'vue'; import App from './index.vue'; const app = createApp(App); app.component('component-a', ComponentA); // Vue.component app.directive('directive-a', directiveA); // Vue.directive app.mount("#app");
-
处理静态资源的时候,vite不支持require,如果require引入了图片,可以使用new URL('xxx', import.meta.url).href,详情参考Static Asset Handling
<template> <!-- vue2.x <img :src="require('xxx')" /> --> <img :src="getImageUrl('xxx')" /> </template> <script> export default { methods: { getImageUrl(name) { return new URL(`@/assets/${name}.png`, import.meta.url).href; } } } </script>
-
异步组件引用方式改变
<script> import { defineAsyncComponent } from 'vue' export default { components: { ComponentA: defineAsyncComponent(() => import('xxxx')); // vue2.x () => import('xxxx') } } </script>
-
v-for中的 Ref数组
<template> <div v-for="item in list" :ref="setItemRef"></div> </template> <script> export default { data() { return { itemRefs: [] } }, methods: { setItemRef(el) { if (el) { this.itemRefs.push(el) } } }, beforeUpdate() { this.itemRefs = [] }, updated() { console.log(this.itemRefs) } } </script>
-
不再有$children这个属性,请使用$refs替换
-
Vue.prototype.$set已经被剔除,可以直接修改对象或者数组
// vue2.x let obj = {}; let arr = []; this.$set('name', 'jack'); this.$set(arr, 0, 'hello'); //vue3.x obj['name'] = 'jack'; arr[0] = 'hello';
-
使用defineComponent方式声明组件,最重要的是给组件正确的参数类型推断,类似于vite.config.js中的defineConfig。可选
<script> import { defineComponent } from 'vue'; export default defineComponent({ data() {}, props: {}, methods: {}, computed: {} watch: {} }) </script>
-
Composition API
<template>
<div class="count">{{count}}</div>
<div class="double-count">{{doubleCount}}</div>
<button class="add-count" @click="addCount">add count</button>
</template>
<script>
import { ref, computed, watch, watchEffect, onMounted, onUnMount } from 'vue';
export default defineComponent({
props: {} // 保留,同vue2
components: {} // 保留,同vue2
// vue3提供了setup方法,作为composition api 的入口
// setup是在befroeCreated之前被调用的,this不能找到组件实例,所以避免在setup中使用this
setup(props, {emit, ...args}) {
// 声明响应式对象的方法有ref 或者 reactive,在注意事项中会详细说明
let count = ref(0);
const addCount = () => {
count.value += 1;
emit('fn', count.value);
emit('gn', doubleCount.value);
}
// computed和watch,在vue3中提供了computed和watch等api,可以单独使用
let doubleCount = computed(() => count.value * 2);
watch(count, (newCount) => {
console.log('count changed', newCount);
})
watchEffect(() => {
console.log('count is watched', count.value);
})
// 生命周期,详情请见注意事项
onMounted(() => {});
onUnMount(() => {});
// 注意,template用到的属性和方法一定要return出去
return {
count,
doubleCount,
addCount
}
}
})
</script>
-
composition api是可以和options api混用的,但是只能在options api中引用composition api。因为this的行为不同,不建议这种写法
-
vue3提供了单独的生命周期api,但是没有beforeCreated和created,如果有需要使用这两个生命周期,直接写在setup中即可
-
ref、reactive、toRefs的区别和用法
-
reactive:
- 参数必须是对象或者数组
- 底层的本质是将数据包装成Proxy
// reactive const obj = reactive({ count: 0 }); obj.count += 1; console.log(obj.count); // 1
-
ref:
- 参数可以是基础类型也可以是对象类型,如果参数是对象类型,本质还是reactive;
- ref只能操作浅层次的数据,深层次的数据依赖于reactive;
- 在template中访问,系统会自动添加.value;在js中需要手动.value;
- 因为ref响应式原理是依赖于Object.defineProperty()的get 和 set,所以返回是一个对象,这个对象上只有一个value属性,指向该内部值
// ref let count = ref(0); console.log(count.value); // 0 count.value += 1; console.log(count.value); // 1
-
toRefs:
- 接收一个响应式对象作为参数,返回的结果对象上的每个属性都是指向原始对象中相应属性的ref对象
- 传递响应式而不是创建响应式(数据修改会影响到原始数据)
// toRefs let user = reactive({ name: 'jack', age: 18 }) let { name, age } = toRefs(user); console.log(name.value, age.value); // jack 18 age.value += 1; console.log(user.age); // 19
-
-
ref引用子组件或者dom的方式
<template> <> <div ref="root">This is a root element</div> <div v-for="i of 5" :key="i" :ref="el => refList[i] = el"> {{i}} </div> <> </template> <script> import { ref, onMounted } from 'vue' export default { setup() { // 常规引用 const root = ref(null) // 变量名与ref一致 // 循环引用 const refList = ref([]); onMounted(() => { // DOM 元素将在初始渲染后分配给 ref console.log(root.value) // <div>This is a root element</div> console.log(refList.value[0]); // }) return { root, refList } } } </script> // 循环引用
-
template中用到的方法和属性,一定要在setup方法的最后return出去
-
解构props不能直接用ES6的解构赋值,这样会丢失数据的响应式。需要...toRefs(props)的方式进行解构
-
2.1 基本结构
<template> <div class="count">{{count}}</div> <div class="double-count">{{doubleCount}}</div> <button class="add-count" @click="addCount">add count</button> <Children /> </template> <script setup lang="ts"> import { ref, computed, watch, defineProps, defineEmits } from 'vue'; // 组件引入,引入的组件可以直接在template中使用,不需要在components中进行注册 import Children from 'xxx'; // props和emit必须要用defineProps和defineEmits来声明。props不能解构赋值,不然会破坏响应式,props中的属性在template中使用不需要props.xx,直接使用xx const props = defineProps({ a: {}, b: {} }) const emit = defineEmits(['fn', 'gn']); // 变量,函数声明,以及 import 引入的内容都能在模板中直接使用,不需要return let count = ref(0); const addCount = () => { count.value += 1; emit('fn', count.value); emit('gn', doubleCount.value); } let doubleCount = computed(() => count.value * 2); // 生命周期和defineComponent没有变化 </script>
-
2.2 注意事项
-
通过ref或者$parent的方式访问组件中的属性,需要用defineExpose暴露出去
-
-
3.1 搭配typescript,更好的类型推断与代码检查
<script setup lang="ts"> import { ref, computed, defineProps, defineEmits, withDefaults } from 'vue'; import { Ref } from '@vue/runtime-dom'; let count: Ref<number> = ref(0); const props = withDefaults(defineProps<{ a: boolean }>(), { a: false, }); const emit = defineEmits<{ (e: 'fn', num: number): void, (e: 'gn', num: number): void, }>(['fn', 'gn']); const addCount = (count: Ref<number>): void => { count.value += 1; emit('fn', count.value); emit('gn', doubleCount.value); } </script>
-
3.2 通用逻辑的抽离与组合
// 新建一个useScroll.ts import { onMounted, onUnmounted } from 'vue'; const useScroll = (callback: () => {}): void => { onMounted(() => { window.addEventListener('scroll', callback()); }); onUnmounted(() => { window.removeEventListener('scroll', callback()); }) }; export default useScroll;
// 在组件中使用 <script setup lang="ts"> import useScroll from 'useScroll.ts'; useScroll(() => { console.log('页面滚动') }); </script>
-
3.3 优雅的组件逻辑分解,不依赖组件的情况去维护数据
// 新建一个useCount.ts import { ref, compunted } from 'vue'; const useCount = () => { let count = ref(0); const addCount = () => { count.value += 1; } let doubleCount = computed(() => count.value * 2); return { count, addCount, doubleCount } } export default useCount;
// 在组件中使用 <script setup> import useCount from 'useCount.ts'; let { count, doubleCount, addCount } = useCount(); // 可以直接在template中使用 </script>