Skip to content

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 的迁移虽然涉及一些破坏性变更,但通过合理的规划和渐进式的迁移策略,可以平滑地完成升级。主要收益包括:

  1. 更好的性能:更小的包体积和更快的渲染速度
  2. 更强的 TypeScript 支持:原生 TypeScript 支持
  3. Composition API:更好的逻辑复用和代码组织
  4. 更好的开发体验:Vite 构建工具和更好的调试支持

建议采用渐进式迁移策略,先迁移核心功能,再逐步优化和重构代码。

相关文章

用代码构建未来,让技术改变世界 🚀 | 专注前端技术,分享开发经验