Appearance
Vue 2 到 Vue 3 迁移指南
Vue 3 带来了许多激动人心的新特性和改进,但同时也引入了一些破坏性变更。本文将为你提供一个全面的迁移指南,帮助你顺利从 Vue 2 升级到 Vue 3。
1. 迁移前准备
评估项目复杂度
bash
# 使用 Vue 官方迁移工具检查项目
npm install -g @vue/compat-migration-helper
vue-migration-helper
# 或者使用 ESLint 插件
npm install eslint-plugin-vue@next
依赖兼容性检查
json
// package.json - 检查这些依赖的 Vue 3 兼容版本
{
"dependencies": {
"vue-router": "^4.0.0", // Vue 2: ^3.x
"vuex": "^4.0.0", // Vue 2: ^3.x
"element-plus": "^2.0.0", // Vue 2: element-ui
"@vue/composition-api": "remove" // Vue 3 内置
}
}
2. 主要破坏性变更
2.1 全局 API 变更
javascript
// Vue 2
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
Vue.use(router)
Vue.use(store)
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
// Vue 3
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')
2.2 组件实例变更
javascript
// Vue 2 - 全局组件注册
Vue.component('MyComponent', {
// 组件选项
})
// Vue 3 - 应用实例组件注册
app.component('MyComponent', {
// 组件选项
})
// Vue 2 - 全局指令
Vue.directive('focus', {
inserted: function (el) {
el.focus()
}
})
// Vue 3 - 应用实例指令
app.directive('focus', {
mounted: function (el) {
el.focus()
}
})
2.3 生命周期钩子变更
javascript
// Vue 2
export default {
beforeDestroy() {
console.log('组件即将销毁')
},
destroyed() {
console.log('组件已销毁')
}
}
// Vue 3
export default {
beforeUnmount() {
console.log('组件即将卸载')
},
unmounted() {
console.log('组件已卸载')
}
}
// Composition API
import { onBeforeUnmount, onUnmounted } from 'vue'
export default {
setup() {
onBeforeUnmount(() => {
console.log('组件即将卸载')
})
onUnmounted(() => {
console.log('组件已卸载')
})
}
}
3. 模板语法变更
3.1 v-model 变更
vue
<!-- Vue 2 -->
<template>
<input v-model="value" />
<my-component v-model="value" />
</template>
<!-- Vue 3 -->
<template>
<!-- 基本用法不变 -->
<input v-model="value" />
<!-- 自定义组件 v-model 变更 -->
<my-component v-model="value" />
<!-- 多个 v-model -->
<my-component
v-model:title="title"
v-model:content="content"
/>
</template>
<script>
// Vue 2 - 自定义组件 v-model
export default {
model: {
prop: 'value',
event: 'input'
},
props: ['value'],
methods: {
updateValue(newValue) {
this.$emit('input', newValue)
}
}
}
// Vue 3 - 自定义组件 v-model
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
methods: {
updateValue(newValue) {
this.$emit('update:modelValue', newValue)
}
}
}
</script>
3.2 v-for 中的 key
vue
<!-- Vue 2 -->
<template>
<div v-for="item in list" :key="item.id">
<span v-for="child in item.children" :key="child.id">
{{ child.name }}
</span>
</div>
</template>
<!-- Vue 3 - key 不再需要在 template 上 -->
<template>
<div v-for="item in list" :key="item.id">
<span v-for="child in item.children" :key="child.id">
{{ child.name }}
</span>
</div>
</template>
3.3 事件监听器变更
vue
<!-- Vue 2 -->
<template>
<div @click="handleClick">
<!-- .native 修饰符 -->
<my-component @click.native="handleNativeClick" />
</div>
</template>
<!-- Vue 3 - 移除 .native 修饰符 -->
<template>
<div @click="handleClick">
<!-- 组件需要显式声明 emits -->
<my-component @click="handleClick" />
</div>
</template>
<script>
// Vue 3 - 组件需要声明 emits
export default {
emits: ['click'],
setup(props, { emit }) {
const handleClick = () => {
emit('click')
}
return { handleClick }
}
}
</script>
4. Composition API 迁移
4.1 从 Options API 到 Composition API
javascript
// Vue 2 Options API
export default {
data() {
return {
count: 0,
user: null
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
watch: {
count(newVal, oldVal) {
console.log(`count changed from ${oldVal} to ${newVal}`)
}
},
methods: {
increment() {
this.count++
},
async fetchUser() {
this.user = await api.getUser()
}
},
mounted() {
this.fetchUser()
}
}
// Vue 3 Composition API
import { ref, computed, watch, onMounted } from 'vue'
import api from './api'
export default {
setup() {
const count = ref(0)
const user = ref(null)
const doubleCount = computed(() => count.value * 2)
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
const increment = () => {
count.value++
}
const fetchUser = async () => {
user.value = await api.getUser()
}
onMounted(() => {
fetchUser()
})
return {
count,
user,
doubleCount,
increment,
fetchUser
}
}
}
4.2 可组合函数(Composables)
javascript
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubleCount = computed(() => count.value * 2)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
return {
count,
doubleCount,
increment,
decrement,
reset
}
}
// 在组件中使用
import { useCounter } from '@/composables/useCounter'
export default {
setup() {
const { count, doubleCount, increment, decrement, reset } = useCounter(10)
return {
count,
doubleCount,
increment,
decrement,
reset
}
}
}
5. 状态管理迁移
5.1 Vuex 4 迁移
javascript
// Vue 2 + Vuex 3
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
})
// Vue 3 + Vuex 4
import { createStore } from 'vuex'
const store = createStore({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
})
// 在组件中使用
import { useStore } from 'vuex'
export default {
setup() {
const store = useStore()
const count = computed(() => store.state.count)
const increment = () => store.commit('increment')
return { count, increment }
}
}
5.2 Pinia 迁移(推荐)
javascript
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
async incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000))
this.increment()
}
}
})
// 在组件中使用
import { useCounterStore } from '@/stores/counter'
export default {
setup() {
const counterStore = useCounterStore()
return {
// 直接返回整个 store
counterStore,
// 或者解构需要的属性和方法
count: counterStore.count,
doubleCount: counterStore.doubleCount,
increment: counterStore.increment
}
}
}
6. 路由迁移
6.1 Vue Router 4 变更
javascript
// Vue 2 + Vue Router 3
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
// Vue 3 + Vue Router 4
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue')
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
// 在组件中使用
import { useRouter, useRoute } from 'vue-router'
export default {
setup() {
const router = useRouter()
const route = useRoute()
const goToAbout = () => {
router.push('/about')
}
return {
route,
goToAbout
}
}
}
7. 构建工具迁移
7.1 Vite 配置
javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
build: {
outDir: 'dist',
sourcemap: true
}
})
7.2 环境变量
bash
# .env.development
VITE_API_URL=http://localhost:3000
VITE_APP_TITLE=My Vue 3 App
javascript
// 在代码中使用
console.log(import.meta.env.VITE_API_URL)
console.log(import.meta.env.VITE_APP_TITLE)
8. 测试迁移
8.1 Vue Test Utils 更新
javascript
// Vue 2 测试
import { shallowMount } from '@vue/test-utils'
import MyComponent from '@/components/MyComponent.vue'
describe('MyComponent', () => {
it('renders correctly', () => {
const wrapper = shallowMount(MyComponent, {
propsData: {
title: 'Test Title'
}
})
expect(wrapper.text()).toContain('Test Title')
})
})
// Vue 3 测试
import { shallowMount } from '@vue/test-utils'
import MyComponent from '@/components/MyComponent.vue'
describe('MyComponent', () => {
it('renders correctly', () => {
const wrapper = shallowMount(MyComponent, {
props: {
title: 'Test Title'
}
})
expect(wrapper.text()).toContain('Test Title')
})
})
9. 迁移策略
9.1 渐进式迁移
javascript
// 使用 @vue/compat 进行渐进式迁移
import { createApp } from 'vue'
import { configureCompat } from '@vue/compat'
// 全局兼容配置
configureCompat({
// 启用 Vue 2 兼容模式
MODE: 2,
// 禁用特定的兼容特性
GLOBAL_MOUNT: false,
GLOBAL_EXTEND: false
})
const app = createApp(App)
9.2 组件级别迁移
javascript
// 在单个组件中配置兼容性
export default {
compatConfig: {
MODE: 3, // 为这个组件启用 Vue 3 模式
INSTANCE_LISTENERS: false // 禁用实例监听器兼容
},
// 组件选项...
}
10. 常见问题和解决方案
10.1 第三方库兼容性
javascript
// 检查库的 Vue 3 兼容版本
const libraryCompatibility = {
'element-ui': 'element-plus', // 替换为 element-plus
'vue-class-component': '@vue/composition-api', // 使用 Composition API
'vue-property-decorator': '移除,使用 Composition API',
'vuex-class': '移除,使用 Composition API + Pinia'
}
10.2 TypeScript 支持
typescript
// Vue 3 + TypeScript
import { defineComponent, ref, computed } from 'vue'
interface User {
id: number
name: string
email: string
}
export default defineComponent({
name: 'UserProfile',
props: {
userId: {
type: Number,
required: true
}
},
setup(props) {
const user = ref<User | null>(null)
const loading = ref(false)
const displayName = computed(() => {
return user.value ? user.value.name : 'Unknown User'
})
const fetchUser = async () => {
loading.value = true
try {
// API 调用
user.value = await api.getUser(props.userId)
} finally {
loading.value = false
}
}
return {
user,
loading,
displayName,
fetchUser
}
}
})
11. 迁移检查清单
准备阶段
- [ ] 评估项目复杂度和依赖
- [ ] 检查第三方库的 Vue 3 兼容性
- [ ] 准备测试环境
- [ ] 备份当前代码
核心迁移
- [ ] 更新 Vue 版本到 3.x
- [ ] 更新构建工具(推荐 Vite)
- [ ] 迁移全局 API 调用
- [ ] 更新生命周期钩子名称
- [ ] 修复模板语法变更
- [ ] 更新事件处理
功能迁移
- [ ] 迁移状态管理(Vuex 4 或 Pinia)
- [ ] 更新路由配置(Vue Router 4)
- [ ] 迁移到 Composition API(可选)
- [ ] 更新测试代码
优化阶段
- [ ] 性能优化
- [ ] 代码重构
- [ ] 文档更新
- [ ] 团队培训
总结
Vue 2 到 Vue 3 的迁移虽然涉及一些破坏性变更,但通过合理的规划和渐进式的迁移策略,可以平滑地完成升级。主要收益包括:
- 更好的性能:更小的包体积和更快的渲染速度
- 更强的 TypeScript 支持:原生 TypeScript 支持
- Composition API:更好的逻辑复用和代码组织
- 更好的开发体验:Vite 构建工具和更好的调试支持
建议采用渐进式迁移策略,先迁移核心功能,再逐步优化和重构代码。