技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

Vue 2 升级 Vue 3:不要让自己成为另一个“IE6”

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/08-Vue 2 升级 Vue 3:不要让自己成为另一个“IE6”
本文目录4 个章节
  1. 为什么需要升级到 Vue 3?
  2. 升级流程
  3. 总结
  4. 官方迁移参考

Vue 2 升级 Vue 3:不要让自己成为另一个“IE6”

Category(分类): Vue Status: 已核对

2026 迁移说明:本文保留原作者覆盖的全部迁移主题和 Vue CLI / Vue 3.0 预发布历史方案,但以当前官方流程纠错。Vue 2 已于 2023-12-31 EOL;Composition API 与 Options API 在 Vue 3 中都是正式 API,前者没有取代后者。新项目优先使用 create-vue + Vite,既有项目可评估 @vue/compat 渐进迁移。

Vue.js 在前端技术中的地位日益重要。随着 Vue 3 的推出,我们需要了解如何将 Vue 2 项目平滑地升级到 Vue 3。本文将详细介绍如何将 Vue 2 项目升级到 Vue 3,帮助同学们更好地掌握 Vue 3 的新特性和改进。

为什么需要升级到 Vue 3?

Vue 3 相较于 Vue 2 有哪些新特性和改进?这些新特性和改进又为什么值得我们去升级呢?在这里,让我们简单概括一下:

更快的渲染速度

Vue 3 中使用了 Proxy 对象,使得数据监听更加高效,提高了渲染速度。同时,Vue 3 还引入了静态 Tree-Shaking 技术,可以在编译阶段进行优化,减少运行时的代码量,从而进一步提高了性能。

更准确地说,两代 Vue 都通过虚拟 DOM 和依赖追踪避免直接重建整页;Vue 3 进一步使用编译器 patch flag、静态提升和更细粒度的更新提示,减少不必要的运行时工作。子组件是否更新仍取决于 props、插槽和优化边界,不能概括成“Vue 2 全部重渲染、Vue 3 只渲染修改部分”。

更小的体积

Vue 3 的核心库体积比 Vue 2 减少了 30% 左右。这得益于 Vue 3 的模块化设计,可以根据需求进行按需加载。同时,Vue 3 使用了更加先进的 Tree-Shaking 技术,可以在编译阶段进行优化,减少运行时的代码量。

更好的 TypeScript 支持

Vue 3 的类型定义更完善,支持更多的 TypeScript 特性。比如,Vue 3 中支持 Prop 类型推断,能够自动推断出组件 props 的类型,并进行检查。此外,Vue 3 还增加了一些新的 API,如 defineComponentFunctionalComponent 等,可以更方便地使用 TypeScript 来编写 Vue 组件。

更好的组合式 API

Vue 3 提供了更灵活、更易用的组合式 API,使得代码更易于维护和阅读。组合式 API 允许我们将逻辑相关的代码封装到一个函数中,从而提高代码的可读性和可维护性。

在 Vue 2 中,我们通常使用 mixins 或者 render props 等方式来实现复用逻辑。而在 Vue 3 中,我们可以使用组合式 API 来实现类似的功能。比如,我们可以将复用的逻辑封装到一个函数中,然后在组件内使用 setup 函数进行调用。

更好的性能监控和调试工具

Vue 3 提供了更好的性能监控和调试工具,帮助我们更好地排查问题、分析性能瓶颈。同时,Vue 3 还提供了新的警告机制,以及更友好的错误信息提示,在排查问题时非常实用。

升级流程

将 Vue 2 项目升级到 Vue 3 分为以下几个步骤:

Step1:确认升级条件

在升级之前,我们需要先进行一些准备工作:

  • 确认当前项目是否可以升级。有些第三方库可能不兼容 Vue 3,需要等待它们更新。
  • 盘点模板 ref,尤其是 v-for ref。Vue 3 仍支持 Options API 的 this.$refs、普通模板 ref 和函数形式的动态 :ref;变化是 v-for 不再自动把同名 ref 收集成数组。模板 ref 与响应式 API ref() 是两件事。
  • 确保项目中没有使用被 Vue 3 移除的 APIs 和全局属性。

Step2:备份代码

在进行升级之前,我们需要先备份代码。备份代码可以帮助我们在升级过程中出现问题时快速恢复到之前的状态。 建议将项目备份到一个新的 Git 分支或者独立的文件夹中,以便于管理和恢复。

Step3:更新依赖

在升级 Vue 2 到 Vue 3 时,我们需要先更新项目中使用的依赖。具体操作如下:

更新 Vue 依赖

package.json 文件中,将 Vue 的版本改为 ^3.0.0,然后运行 npm install 命令进行安装。

{
  "dependencies": {
    "vue": "^3.0.0"
  }
}

还可以使用 Vue CLI 来创建全新的 Vue.js 项目。 Vue CLI 是 Vue.js 官方提供的一个脚手架工具,可以帮助我们快速创建 Vue.js 项目,并自动配置 Webpack 等相关工具。在创建 Vue.js 项目时,可以使用以下命令:


vue create my-project

接着,会出现一个提示,询问我们要使用哪个预设选项。在这里,我们需要选择“Manually select features”手动选择需要的功能,然后勾选“Babel”、“Router”、“Vuex”等插件,最后选择“Vue 3.x (Preview)”作为 Vue.js 的版本。

通过这种方式,我们就可以快速创建一个基于 Vue.js 3.0 的项目,并进行开发。

新项目或者 Vuex 逻辑简单,建议直接使用 PiniaVite 更是值得一试,在打包方面带来极致的开发体验。

更新其他相关依赖

package.json 文件中,将其他相关依赖的版本号更新至最新稳定版本。

同时,还要注意一些常用的第三方库是否支持 Vue 3,并对其进行更新。比如,vue-router 和 vuex 都有对应的 Vue 3 版本。

{
  "dependencies": {
    "vue-router": "^4.0.0-0",
    "vuex": "^4.0.0-0"
  }
}

新项目或者 Vuex 逻辑简单,建议直接使用 Pinia

当前推荐:create-vue、Vite 与 @vue/compat

上面的 vue create、“Vue 3.x (Preview)”以及 ^4.0.0-0 是 Vue 3.0 初期的历史流程,不应作为当前推荐。新项目使用:

pnpm create vue@latest

它由官方 create-vue 创建 Vite 项目。老项目无法一次迁完时,可使用迁移构建:vue@vue/compat 必须保持同一精确版本,以 @vue/compiler-sfc 替换 vue-template-compiler,并把 vue alias 到 @vue/compat

// vite.config.js:仅用于渐进迁移
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  resolve: { alias: { vue: '@vue/compat' } },
  plugins: [vue({
    template: { compilerOptions: { compatConfig: { MODE: 2 } } }
  })]
})

逐项处理兼容警告,完成后移除 alias 和 @vue/compat,不要长期把兼容构建当最终架构。依赖升级还应逐个核对 Vue Router 4、状态库、UI 库、测试工具和私有插件,而不是盲目改成“latest”。

Step4:更新代码

在更新依赖后,我们需要对项目中的代码进行更新。具体操作如下:

更新组件注册方式

Vue 2 通过全局构造器注册组件:

import Vue from 'vue'
import MyComponent from './MyComponent.vue'

Vue.component('my-component', MyComponent)

迁移到 Vue 3 后,全局构造器 API 改为应用实例 API:

import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './MyComponent.vue'

const app = createApp(App)
app.component('my-component', MyComponent)
app.mount('#app')

不同 createApp() 实例的组件、插件和配置彼此隔离。

更新钩子函数

Options API 没有被删除。Vue 3 仍支持:

export default {
  beforeCreate() {},
  created() {},
  beforeMount() {},
  mounted() {},
  beforeUpdate() {},
  updated() {},
  beforeUnmount() {},
  unmounted() {}
}

真正改名的是 Vue 2 beforeDestroy → Vue 3 beforeUnmount,以及 destroyedunmounted。Composition API 没有 onBeforeCreate / onCreated,初始化逻辑写在更早执行的 setup();其他对应 API 为 onBeforeMountonMountedonBeforeUpdateonUpdatedonBeforeUnmountonUnmountedonActivatedonDeactivated。这不是删除 Options API,而是另一种组织逻辑的方式。

更新过滤器

在 Vue 3 中,过滤器已经被移除,推荐使用计算属性或者方法替代。比如原先的代码:


Vue.filter('reverse', function (value) {
  return value.split('').reverse().join('')
})

可以改为:


computed: {
  reversedValue() {
    return this.value.split('').reverse().join('')
  }
}

更新 v-for 中的 Ref 数组

在 Vue 2 中,使用 v-forref 属性会在相应的 $refs 属性中填充 ref 数组。然而,当使用嵌套的 v-for 时,这种行为会变得不明确且效率低下。 而在 Vue 3 中,这样的用法将不再在 $refs 中自动创建数组。如果要从单个绑定中获取多个 ref,请将模板 ref 绑定到函数:


<div v-for="item in list" :ref="setItemRef"></div>

export default {
  data() {
    return {
      itemRefs: []
    }
  },
  methods: {
    setItemRef(el) {
      if (el) this.itemRefs.push(el)
    }
  },
  beforeUpdate() {
    this.itemRefs = []
  },
  updated() {
    console.log(this.itemRefs)
  }
}
import { onBeforeUpdate, onUpdated } from 'vue'

export default {
  setup() {
    let itemRefs = []

    const setItemRef = (el) => {
      if (el) itemRefs.push(el)
    }

    onBeforeUpdate(() => {
      itemRefs = []
    })

    onUpdated(() => {
      console.log(itemRefs)
    })

    return { itemRefs, setItemRef }
  }
}

itemRefs 可以是对象,其 ref 会通过迭代的 key 被设置。 如果需要,itemRef 也可以是响应式的且可以被监听。

更新函数式组件

Vue 3 的函数式组件就是接收 propscontext 的普通函数,不要求 defineComponent + setup

import { h } from 'vue'

const DynamicHeading = (props, context) =>
  h(`h${props.level}`, context.attrs, context.slots.default?.())

DynamicHeading.props = ['level']
export default DynamicHeading

context 包含 attrsslotsemit。Vue 2 SFC 的 <template functional>functional: true 已移除,通常删除标记后按普通 SFC 迁移。defineComponent({ setup() { return render } }) 当然有效,但它定义的是有状态组件,不是函数式组件的必需格式。

更新异步组件

Vue 2 可以把返回 Promise 的工厂函数直接注册为异步组件:

Vue.component('my-component', () => import('./MyComponent.vue'))

动态 import() 没有废弃。迁移到 Vue 3 后,组件异步工厂要显式交给 defineAsyncComponent()

import { defineAsyncComponent } from 'vue'

const MyComponent = defineAsyncComponent(() => import('./MyComponent.vue'))
const WithOptions = defineAsyncComponent({
  loader: () => import('./MyComponent.vue'),
  delay: 200,
  timeout: 3000,
  loadingComponent: LoadingComponent,
  errorComponent: ErrorComponent
})

全局注册仍可写 app.component('my-component', MyComponent)。路由懒加载是另一套机制,在 routes 中直接写 component: () => import('./Page.vue'),不要包 defineAsyncComponent

更新组件通讯

在 Vue.js 2.x 版本中,组件通讯主要通过propsemitparent/$childrenprovide/inject 等方式实现。但是,在 Vue.js 3.x 版本中,组件通讯方式有了一些变化。

首先,Vue 3 增加了 Composition API,但 Options API 仍然完整支持。Composition API 是一种全新的 API,它允许开发者将功能逻辑封装在组合函数中,这些组合函数可以被多个组件共享。在 Vue.js 3.x 版本中,setup() 函数是组件中的入口函数,它返回一个对象,其中包括组件所需的所有响应式状态和方法。

其次,在 Vue.js 3.x 版本中引入了 Teleport 组件,它可以帮助我们更好地处理跨组件的元素传送问题。Teleport 组件可以将一个组件的内容传送到 DOM 树的另一个位置。例如,在某些情况下,我们需要在组件外部渲染一个菜单,并且需要将菜单与组件进行绑定。此时,我们可以使用 Teleport 组件来实现这个功能。

Vue.js 3.x 版本还引入了 emits 选项,它允许开发者声明组件发出的事件,有利于事件校验、文档化,并避免未声明的监听器作为透传属性落到组件根元素上。声明事件是推荐做法,但并非强制要求;未声明的事件仍可通过 $emit 发出。

provide/inject 在 Vue 2 中就支持祖先向任意后代注入,并不局限于直接父子组件。Vue 3 为它提供了 Composition API 形式的 provide() / inject(),应用实例也可以通过 app.provide() 向整棵组件树提供依赖;配合 TypeScript 的 InjectionKey 可获得更好的类型支持。

另外,Vue 3 移除了 $children,不要再依赖它遍历直接子组件;应改用 props / emits、模板 ref 或 provide/inject 等显式通信方式。$parent 仍然存在,但会造成组件强耦合,也不建议作为常规通信方案。

更新过渡效果的 class 名


/* Vue 2 */.v-enter,
.v-leave-to {
  opacity: 0;
}

.v-leave,
.v-enter-to {
  opacity: 1;
}

/* Vue 3 */.v-enter-from,
.v-leave-to {
  opacity: 0;
}

.v-leave-from,
.v-enter-to {
  opacity: 1;
}

<transition> 组件相关属性名也发生了变化:

  • leave-class 已经被重命名为leave-from-class (在渲染函数或 JSX 中可以写为:leaveFromClass)
  • enter-class 已经被重命名为 enter-from-class (在渲染函数或 JSX 中可以写为:enterFromClass)

更新/升级 Watch

Vue 2 的 watch 选项在 Vue 3 仍可使用:

export default {
  data: () => ({ message: 'Hello, World!' }),
  watch: {
    message(newValue, oldValue) {
      console.log(newValue, oldValue)
    }
  }
}

Composition API 提供 watchwatchEffect

import { ref, watch, watchEffect } from 'vue'

const message = ref('Hello')
const name = ref('Vue 3')

watch([message, name], ([messageValue, nameValue], oldValues) => {
  console.log(messageValue, nameValue, oldValues)
}) // 默认惰性:source 变化后才执行

const stop = watchEffect(() => {
  console.log(message.value)
}) // 注册时立即执行,并自动收集同步读取的依赖

stop()

watchEffect 没有 { lazy: true } 选项,也不是 watch 与 computed 的合并:它不提供缓存派生值和新旧值。派生状态使用 computed;需要显式 source、旧值或默认惰性执行使用 watch;需要自动追踪的立即副作用使用 watchEffect

更新全局属性挂载

Vue 2 可以通过 Vue.prototype 挂载全局属性:

import Vue from 'vue'

Vue.prototype.$appName = 'MyApp'

迁移到 Vue 3 后,Vue.prototype 改为应用实例的 app.config.globalProperties

const app = createApp(App)
app.config.globalProperties.$appName = 'MyApp'
app.mount('#app')

Options API 和模板仍可用 this.$appName / {{ $appName }};只有 setup() 本身没有组件 this。对于显式依赖注入,也可以使用:

app.provide('appName', 'MyApp')
// 后代 setup:const appName = inject('appName')

provide/inject 是更明确的替代方案,但不是使用全局属性时的强制要求。

更新/升级 Slot

Vue 3 的“插槽统一”指 $scopedSlots 被移除,所有 $slots 成员在渲染函数中都作为函数调用。模板的默认/具名插槽、fallback 与作用域插槽仍然存在;默认作用域插槽不要求具名。

<!-- Child.vue:子组件向插槽内容提供 slot props -->
<template>
  <slot :msg="'Hello from Child'">Fallback</slot>
</template>
<!-- Parent.vue:消费默认作用域插槽 -->
<Child v-slot="{ msg }">
  <h2>{{ msg }}</h2>
</Child>

渲染函数中可调用 slots.default?.({ msg });普通模板不应把 $slots.default(...) 直接放进插值。slot props 从子组件传给父组件提供的内容,不是双方自动共享的普通 prop。

更新/升级 v-model

Vue 3 无参数的组件 v-model 对应 modelValue prop 与 update:modelValue 事件,子组件仍必须声明并发出更新,并非“自动凭空得到 prop/event”。

<!-- Parent.vue:name、age 是 v-model 参数,不是修饰符 -->
<Child v-model:name="form.name" v-model:age="form.age" />
// Child.vue,Vue 3.0+
export default {
  props: { name: String, age: Number },
  emits: ['update:name', 'update:age'],
  methods: {
    changeName(value) { this.$emit('update:name', value) }
  }
}

Vue 3.4+ 可以在 <script setup> 中使用 const name = defineModel('name')const age = defineModel('age')。一个组件可以有多个 v-model 参数。.trim 是内置修饰符;自定义组件修饰符通过 modelModifiers(或命名模型对应属性)处理,不要把 v-model:name 叫作修饰符,也不要把多模型写在原生 <input> 上。

更新 Vue Router

Router 4 使用 history 工厂,并在入口安装:

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

export default createRouter({
  history: createWebHistory(),
  routes: []
})

// main.js
const app = createApp(App)
app.use(router)
app.mount('#app')

安装 Router 4 后,Options API 和模板仍支持 this.$routethis.$router

export default {
  computed: { id() { return this.$route.params.id } },
  methods: { go() { this.$router.push('/about') } }
}

Composition API 在 setup() / composable 中使用 useRoute()useRouter()。这些函数依赖注入上下文,不能在任意路由配置模块顶层调用。

更新 Vuex

Vuex 4 中使用 createStore() 函数来创建 Store 实例,同时在 setup 函数中使用 useStore() 函数获取 Store 对象。因此,在 store 文件中需要做出如下修改:

import { createStore } from 'vuex'

const store = createStore({
  state: {
    // 状态数据
  },
  mutations: {
    // 修改状态的方法
  },
  actions: {
    // 异步操作
  },
  modules: {
    // 子模块
  }
})

export default store

还需要在应用入口安装 Store:

import { createApp } from 'vue'
import App from './App.vue'
import store from './store'

createApp(App).use(store).mount('#app')

在组件中使用 Vuex 时,需要使用 useStore() 函数获取 Store 对象。例如:

import { useStore } from 'vuex'

export default {
  setup() {
    const store = useStore()

    // 使用 store 对象
    return { store }
  }
}

更新/升级 Pinia

Vuex 4 仍可用于 Vue 3;Pinia 是当前默认推荐的状态管理库,但是否迁移应结合模块复杂度评估。

pnpm add pinia
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
// 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++ } }
})
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()
const { count, doubleCount } = storeToRefs(counter)
const { increment } = counter
</script>

Pinia 没有通用 useStore(),也不是 store.state.count / store.actions.increment() / store.getters.doubleCount。应使用具体的 useCounterStore(),直接访问 counter.countcounter.doubleCountcounter.increment();状态和 getter 解构时用 storeToRefs() 保持响应性。无需把 Pinia 手动挂到 globalProperties.$store

Step5:测试和调试

升级完成后,我们需要对项目进行测试和调试,以确保项目在新版本的 Vue 中能够正常运行 。具体操作如下:

  1. 运行项目,查看是否有报错信息。

在运行项目时,需要关注控制台中是否有报错信息。如果有报错信息,需要逐一排查并解决问题。

  1. 针对性测试项目中的组件和功能。

在升级完成后,可以对项目中常用的功能进行针对性测试,确保能够正常运行。比如,我们可以测试路由、Vuex 状态管理、表单验证等功能。

  1. 使用 Vue Devtools 进行调试。

Vue Devtools 是一款非常实用的调试工具,可以帮助我们更好地理解项目中的数据流动和组件结构。在升级完成后,我们可以使用 Vue Devtools 对项目进行调试,以便于发现问题和优化代码。

Step6:更新文档和注释

在升级完成后,我们还需要更新项目中的文档和注释,以确保其他开发人员能够快速了解项目的变化和特性。

建议在代码中加入详细的注释,包括组件的用途、函数的参数和返回值等信息。同时,也要及时更新项目中的文档,以便于其他人员可以快速上手。

总结

Vue 3 的推出为前端开发者带来了更多的可能性。通过本文的介绍,我们可以清晰地了解到 Vue 3 相较于 Vue 2 的新特性和改进,以及如何将 Vue 2 项目升级到 Vue 3。

在升级过程中,我们需要先确认升级条件、备份代码并更新依赖。然后,对项目中的代码进行更新,并进行测试和调试。最后,我们还需要更新文档和注释,以便于其他开发人员能够快速了解项目的变化和特性。

Vue 3 的升级需要谨慎对待,但也是值得尝试的。希望本文的介绍可以帮助大家更好地掌握升级方法和技巧,从而在项目早日迁移到 Vue 3 上享受更多的优势。

文中列举的更新栗子都是目前在升级项目中接触到的,若对 Vue 2 升级 Vue 3 其它升级方面感兴趣的可以分享交流,我也会同步更新此文档。

官方迁移参考

作者:昨宵月光似练链接:https://juejin.cn/post/7221425945240141879来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS