15-Vue3.0迁移

image-20200723170734421

参考资料:

本文全部基于 vue3。

1. vue-cli 创建vue3项目

1
vue create my-app-vue3

image-20251229174550313

2. vue3升级调整

升级依赖后,需要对代码进行相应的调整。以下是几个关键的迁移步骤。

全局 API 迁移

Vue 3 对全局 API 进行了重构,许多全局方法现在需要通过 createApp 实例来调用。

main.js & router/index.js & store/index.js

Vue 2 示例

1
2
3
4
5
6
7
8
9
10
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
Vue.config.productionTip = false;
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app');

Vue 3 示例

1
2
3
4
5
6
7
8
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
const app = createApp(App);
app.use(router);
app.use(store);
app.mount('#app');

实例属性迁移

Vue 2 中的实例属性,如 $mount$destroy,在 Vue 3 中已经有所变化。

Vue 2 示例

1
2
3
4
const vm = new Vue({
// options
});
vm.$mount('#app');

Vue 3 示例

1
2
3
4
const app = createApp({
// options
});
app.mount('#app');

事件 API 迁移

Vue 3 删除了 $on, $off, 和 $once 方法,建议使用 mitt[2] 这样的事件库作为替代。

Vue 2 示例

1
2
3
4
const vm = new Vue();
vm.$on('event', () => {
// handle event
});

Vue 3 示例

1
2
3
4
5
import mitt from 'mitt';
const emitter = mitt();
emitter.on('event', () => {
// handle event
});

指令迁移

Vue 3 对指令的定义方式进行了调整。

Vue 2 示例

1
2
3
4
5
Vue.directive('focus', {
inserted: function (el) {
el.focus();
}
});

Vue 3 示例

1
2
3
4
5
6
7
const app = createApp(App);
app.directive('focus', {
mounted(el) {
el.focus();
}
});
app.mount('#app');

组件生命周期钩子迁移

Vue 3 对一些生命周期钩子进行了重命名,例如 beforeDestroy 改为 beforeUnmountdestroyed 改为 unmounted

Vue 2 示例

1
2
3
4
5
6
7
8
export default {
beforeDestroy() {
console.log('Component is about to be destroyed');
},
destroyed() {
console.log('Component has been destroyed');
}
};

Vue 3 示例

1
2
3
4
5
6
7
8
export default {
beforeUnmount() {
console.log('Component is about to be unmounted');
},
unmounted() {
console.log('Component has been unmounted');
}
};

3. 使用 Vue 3 的新特性

3.0 createApp

多个应用实例应用实例并不只限于一个。

  • createApp API 允许你在同一个页面中创建多个共存的 Vue 应用,而且每个应用都拥有自己的用于配置和全局资源的作用域。
1
2
3
4
5
6
7
import { createApp } from 'vue'

const app1 = createApp({...})
app1.mount('#container-1')

const app2 = createApp({...})
app2.mount('#container-2')

3.1 Composition API

Composition API(组合式API / 函数式API)是 Vue 3 中的一个重要新特性,它提供了一种更灵活、更可组合的方式来组织组件逻辑。

3.1.1 reactive

作用:创建响应式对象,非包装对象,可以认为是模版中的状态。

  • setup(){...} 组合式 API 的入口,返回的对象/方法会暴露给模板和组件实例使用。同时也没有了 this. 访问
  • <template>可以放兄弟节点
  • reactive 类似 useState,如果参数是字符串、数字会报警告”value cannot be made reactive“,所以应该设置对象,才可以数据驱动页面
  • 结合单文件组件使用的组合式 API,推荐通过 <script setup> 以获得更加简洁及符合人体工程学的语法。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<template>
<div>
hello-vue3 {{ obj1.myname }}-{{ obj1.myage }}
<button @click="handleClick">change</button>
<div>todo list-{{obj2.mytext}}</div>
<input type="text" v-model="obj2.mytext" />
<button @click="handleAdd">add</button>
<ul>
<li v-for="data in obj2.datalist" :key="data">{{ data }}</li>
</ul>
</div>
</template>

<script>
import { reactive } from "vue"
export default {
// vue3老写法或者vue写法中 beforeCreate/created 生命周期 ===> setup
setup() {
console.log("setup")
// 定义状态obj1,obj2,(同一个组件reactive支持多个定义)
const obj1 = reactive({
myname: "jerry",
myage: 18,
})
const obj2 = reactive({
mytext: "",
datalist: [],
})
const handleClick = () => {
obj1.myname = "tom"
}
const handleAdd = () => {
obj2.datalist.push(obj2.mytext)
obj2.mytext = ''
}
return {
obj1,
obj2,
handleClick,
handleAdd
}
},
}
</script>

3.1.2 ref

作用:创建一个包装式对象,含有一个响应式属性 value,可以 . 访问出来(即 .value 就是dom节点对象)。

  • ref 与 reactive 的差别就是 ref 没有包装属性 value(也会被拦截,也可以当做状态字段用在dom节点上)
  • const count = ref(0),可以接受普通数据类型, count.value++
  • .value 如果是 <input type="text" ref="mytext">文本标签,取值即为 mytext.value.value 为输入内容

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
hello-vue3 {{ myname }}
<button @click="handleClick">change</button>
</div>
</template>

<script>
import { ref } from "vue"
export default {
setup() {
const myname = ref("jerry") //.value属性拦截, dom使用 {{myname}}
const handleClick = () => {
myname.value = "tom" //修改只能对 .value 属性修改
}
return {
myname,
handleClick,
}
},
}
</script>

3.1.3 toRefs

作用:可以在dom节点省略对象名直接访问对象的属性

  • ...toRefs(obj) 实际用法要对象展开式。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div>
hello-vue3 {{ myname }}-{{ myage }}
<button @click="handleClick">change</button>
</div>
</template>

<script>
import { reactive, toRefs } from "vue"
export default {
setup() {
const obj1 = reactive({
myname: "jerry",
myage: 18,
})
const handleClick = () => {
obj1.myname = "tom"
}
return {
...toRefs(obj1), //toRefs可以让obj1省略,在dom节点上直接访问属性值
handleClick,
}
},
}
</script>

3.1.4 props

作用:状态字段通信。参考官网使用

  • props 接收参数。

示例:

navbar.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>
--{{myname}}--
</div>
</template>

<script>
export default {
props: ["myname"],
setup(props) {
console.log(props.myname)
}
}
</script>

验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>
通信
<navbar myname="home"></navbar>
</div>
</template>

<script>
import navbar from './components/Navbar.vue'
export default {
components: {
navbar
}
}
</script>

3.1.5 emit

作用:触发父组件中定义的时间,来实现不同组件之间的通信。

  • setup(props, { emit }) {emit} 作为第二个参数,结构接参
  • emit("event") 触发父组件中名字为event的事件 @event=”change”

父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div>
通信
<navbar myname="home" @showEvent="change"></navbar>
<sidebar v-show="obj.isShow"></sidebar>
</div>
</template>

<script>
import navbar from "./components/Navbar.vue"
import sidebar from "./components/Sidebar.vue"
import { reactive } from "vue"
export default {
components: {
navbar,
sidebar,
},
setup() {
const obj = reactive({
isShow: true,
})
const change = () => {
obj.isShow = !obj.isShow
}
return {
obj,
change,
}
},
}
</script>

子组件1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
--{{ myname }}--
<button @click="handleShow">show</button>
</div>
</template>

<script>
export default {
props: ["myname"],
// {emit} 作为第二个参数,结构接参
setup(props, { emit }) {
console.log(props.myname)
const handleShow = () => {
console.log("handleShow")
emit("showEvent")
}
return {
handleShow,
}
},
}
</script>

子组件2:

1
2
3
4
5
6
7
8
9
<template>
<div>
<ul>
<li>111</li>
<li>222</li>
<li>333</li>
</ul>
</div>
</template>

3.2 Teleport

Teleport 允许你将组件的 DOM 渲染到一个特定的 DOM 节点之外。

示例

1
2
3
4
5
6
7
8
9
<template>
<div>
<teleport to="#modals">
<div class="modal">
<p>This is a modal</p>
</div>
</teleport>
</div>
</template>

3.3 Fragments

在 Vue 3 中,组件可以返回多个根节点,从而摆脱了 Vue 2 中必须有单一根节点的限制。

示例

1
2
3
4
5
6
7
<template>
<div>
<header>Header Content</header>
<main>Main Content</main>
<footer>Footer Content</footer>
</div>
</template>

3.4 生命周期

Vue 2 & Vue 3 生命周期对比

vue2 vue3
beforeCreate setup(()=>{})
created setup(()=>{})
beforeMount onBeforeMount(()=>{})
mounted onMounted(()=>{})
beforeUpdate onBeforeUpdate(()=>{})
updated onUpdated(()=>{})
beforeDestroy onBeforeUnmount(()=>{})
destroyed onUnmounted(()=>{})
—分割线— —分割线—
activated onActivated(()=>{})
deactivated onDeactivated(()=>{})
errorCaptured onErrorCaptured(()=>{})

总结: Vue2和Vue3钩子变化不大,beforeCreate 、created 两个钩子被 setup() 钩子来替代。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div>
生命周期
<ul>
<li v-for="data in obj.list" :key="data">{{data}}</li>
</ul>
</div>
</template>

<script>
import axios from 'axios'
import {reactive, onBeforeMount, onMounted} from 'vue'
export default {
setup() {
const obj = reactive({
list: []
})
onBeforeMount(() => {
console.log("onBeforeMounted")
})
onMounted(() => {
console.log("dom上树、axios、事件监听、setInterval......")
setTimeout(() => {
obj.list = ["111", "222", "333"]
}, 2000)
})
return {
obj
}
}
}
</script>

3.5 计算属性

作用:与vue2一样,值没有变化时只会计算一次。注重结果,必须有返回值。

  • computed(() => { return xxx } 计算属性新的写法。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<div>
搜索:<input type="text" v-model="obj.mytext" />{{obj.mytext}}
<ul>
<li v-for="data in computedList" :key="data">
{{ data }}
</li>
</ul>
</div>
</template>

<script>
import { reactive, computed } from "vue"
export default {
setup() {
const obj = reactive({
mytext: '',
datalist: ["aaa", "abb", "abc", "bac", "caa", "cba"],
})
//计算属性写法
const computedList = computed(() => {
return obj.datalist.filter(item => item.includes(obj.mytext))
})
return {
obj,
computedList
}
},
}
</script>

3.6 watch

作用:监听状态字段值的改变,触发回调函数执行。注重过程。

  • watch(监听函数, 回调函数) 监听函数监听状态字段,回调函数在监听字段发生改变时触发执行。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>
<div>
搜索:<input type="text" v-model="obj.mytext" @input="handleInput" />
{{ obj.mytext }}
<ul>
<li v-for="data in obj.datalist" :key="data">
{{ data }}
</li>
</ul>
</div>
</template>

<script>
import { reactive, watch } from "vue"
export default {
setup() {
const obj = reactive({
mytext: "",
datalist: ["aaa", "abb", "abc", "bac", "caa", "cba"],
oldlist: ["aaa", "abb", "abc", "bac", "caa", "cba"], //保留原列表不变
})
//监听状态字段值的改变,都会触发回调函数调用(第二个参数)
watch(
() => obj.mytext,
() => {
obj.datalist = obj.oldlist.filter(item => item.includes(obj.mytext))
}
)
return {
obj,
}
},
}
</script>

3.7 自定义hooks

作用:自定义hooks可以实现函数编程的复用,更加简洁高效。可以理解为函数封装

示例:

1
2
3
4
5
6
7
8
9
10
11
import { ref } from 'vue'
// 封装 useCount 函数供外部调用
function useCount() {
const count = ref(1)
const addCount = (num = 1) => count.value += num
return {
count,
addCount
}
}
export { useCount }

hooks 封装使用示例:

image-20251230122310013

3.8 路由

1
2
3
4
5
6
7
8
import { useRoute, useRouter } from 'vue'

export default {
setup() {
const route = useRoute() //等价于 vue2 的 this.$route 用于接参:route.params.xx
const router = useRouter() //等价于 vue2 的 this.$router 用于跳转:router.push('/path')
}
}

3.9 vuex 公共状态管理 store

1
2
3
4
5
6
7
import { useStore } from 'vuex'

export default {
setup() {
const store = useStore() //等价于 vue2 的 this.$store 用于公共状态管理:store.state.xxx
}
}

超级好用的替代方案:provide, injectvue-composition-api 的一个新功能,依赖注入功能。(provide-提供, inject-注入)

1
2
3
4
5
6
7
8
9
10
11
import { provide, inject} from 'vue'

//根组件:共享自己的状态
const showStatus = ref(true)
provide("showStatus", showStatus)

//使用的组件
onMounted(() => {
const showStatus = inject("showStatus")
showStatus.value = false
})

4. 测试和调试

在完成代码迁移后,确保对整个项目进行全面的测试和调试。以下是一些推荐的测试和调试步骤。

单元测试(使用 Jest)

使用 Jest 或 Mocha 等测试框架,编写和运行单元测试,确保所有功能正常工作。

示例

1
2
3
4
5
6
7
8
9
10
11
import { mount } from '@vue/test-utils';
import HelloWorld from '@/components/HelloWorld.vue';
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message';
const wrapper = mount(HelloWorld, {
props: { msg }
});
expect(wrapper.text()).toMatch(msg);
});
});

端到端测试(使用 Cypress)

使用 Cypress 或 Nightwatch 等工具进行端到端测试,模拟用户操作,确保应用在真实使用场景中表现正常。

示例

1
2
3
4
5
6
describe('My First Test', () => {
it('Visits the app root url', () => {
cy.visit('/');
cy.contains('h1', 'Welcome to Your Vue.js App');
});
});

调试

使用 Vue Devtools 来调试 Vue 3 应用。确保你安装了最新版本的 Vue Devtools,并在开发者工具中启用了 Vue 3 支持。

5. vue3组件定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<div id="box">
{{myname}}
<navbar myname="aaa">
<div>111111111111111</div>
</navbar>
<sidebar></sidebar>
</div>

<script>
var obj = {
data() {
return {
myname: "jerry"
}
},
methods: {}
computed: {}
}
var app = Vue.createApp(obj)
app.component("navbar", {
props: ["myname"],
template: `
<div>
navbar-{{myname}}
<slot></slot>
</div>
`
})
app.component("sidebar", {
template: `
<div>123123123</div>
`
})
app.mount("#box")
</script>

6. vue3自定义指令

6.1 轮播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<div id="box">
<header>导航</header>
<div class="swiper">
<div class="swiper-wrapper">
<!-- 绑一个自定义指令 v-swiper,传参为对象解构 -->
<div class="swiper-slide" v-for="(item, index) in datalist" :key="item"
v-swiper="{index: index, length: datalist.length}">
{{item}}
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>

<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
<footer>底部内容</footer>
</div>


<script>
// 自定义指令 v-swiper
var obj = {
data() {
return {
datalist: []
}
},
mounted() {
setTimeout(() => {
this.datalist = ["aaa", "bbb", "ccc"]
//过早
}, 2000)
},
}

var app = Vue.createApp(obj)
app.directive("swiper", {
mounted(el, binding) {
console.log("inserted", el, binding.value)
// 如果最后一个节点插入到父节点中了,就可以 new Swiper 初始化了
let { index, length } = binding.value
if (binding.value = length) {
console.log("new Swiper")
new Swiper(".swiper", {
// direction: 'vertical', // 垂直切换选项
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 自动轮播
autoplay: {
delay: 2500,
disableOnInteraction: false,
},
})
}
}
})
app.mount("#box")
</script>

6.2 路由

router/index.js - vue2可以用 * 做默认匹配,vue3中需要使用 \

1
2
3
4
5
6
7
8
const routes = [
...
// 重定向:访问根目录时,自动重定向到 /films
{
path: '/',
redirect: '/films'
}
]

15-Vue3.0迁移
https://janycode.github.io/2022/05/22/04_大前端/04_Vue/15-Vue3.0迁移/
作者
Jerry(姜源)
发布于
2022年5月22日
许可协议