Vue.js: 如何将大型项目从 Vue 2 迁移到 Vue 3

7,072 阅读9分钟

原文标题:Vue.js: How to Migrate a large project from Vue 2 to Vue 3

Vue.js 团队在 2020 年 9 月发布 Vue 3.0 。这个全新的版本带来了许多新的特性、优化,同时也带来了一些突破性的变化

2021 年 6 月,Vue.js 团队发布了 Vue 3.1 版本。这个新版本发布带有 @vue/compat(也叫做“迁移构建”)。它允许通过运行 Vue 2 和 Vue 3 代码方便将大型项目从 Vue 2 迁移到 Vue 3。

迁移到 Vue 3 可能是一项重要任务(取决于你项目的大小)。在 Crisp,我们最近花了2个星期将我们的 app(25 万行代码)从 Vue 2.6 迁移到 Vue 3.2

我们通过阅读官方的迁移手册准备我们的迁移工作,但是我们在迁移项目时发现了许多不同的问题。

我们觉得分享我们的经验是非常好的,这样其他公司就可以从我们的经验中受益。这篇文章将会告诉你:

  • Vue 2 和 Vue 3 的不同之处
  • 我们迁移时所遇到的问题以及相应的解决方法
  • 我们的 Vue.js 迁移策略

Vue 3 的相关背景

Vue.js 创建于 2013 年,最初的理念是创建 AngularJS 1 的简约替代品。

当时,Angular 是一个庞大的框架,具有大量的新功能。Vue.js 的第一个版本就像一个带有模板、数据绑定、过滤器和指令的微型框架。

然后 Vue 2 在 2016 发布(几乎与 Angular 2 同时发布),并为 AngularJS 提供了一个很好的替代方案。事实上,许多使用 Angular 1 的开发人员决定迁移到 Vue 2,是因为他们不喜欢 Angular 2:Vue 2 提供了与 AngularJS 一样简单的东西,但性能更好。

Vue 3 的创建考虑了性能方面的问题。这是一种进化而不是革命。出于性能原因,大多数新功能和重大更改都已发布。

内部反应系统已经从头开始重新设计,因此,IE 11 支持已被删除(这不是一个巨大的损失😂)。

译者注:IE,老业界毒瘤了 😂

最后,这个新版本旨在更模块化并引入了 Composition API 等功能。

Vue 3 最大的改变

Vue 全局 API

Vue 全局 API 已被弃用。虽然 @vue/compat 仍然支持它,为了完全支持 Vue 3,您需要重新设计您的应用程序,移出全局 API 的使用。

这意味着无法使用像 Vue.set 或 Vue.delete 这样的 API。因为 Vue 3 带有一个新的响应系统,使用这些 API 变得毫无用处。

您可以直接使用 object[key] = value,代替 Vue.set(object, key, value)。同样,可以使用 delete object[key]来代替 Vue.delete(object, key)

译者注:

vue 2 底层是借助 Object.defineProperty API 实现响应式,但是该 API 无法拦截数组等一些对象的数据变化。因此在 Vue 2 中我们无法直接通过数组下标修改数组实现视图的更新

vue 3 则借助了 Proxy API 实现响应式。该 API 可拦截对象、数组等操作。

Filters(过滤器)

如前所述,Vue.js 是作为 Angular 1 的替代品而创建的。这就是最初支持过滤器的原因。

过滤器的最大问题是性能:每次更新数据时都必须执行过滤器函数。出于这个原因,Vue 3 取消了对过滤器的支持。

这就意味着我们无法在 Vue 3 templates 使用类似 {{ user.lastName | uppercase }}的模板语法。

相反,您将不得不使用 {{ uppercasedLastName }} 之类的计算属性或 {{uppercase(lastName)}} 之类的方法代替过滤器。

Vue&插件初始化

从 Vue 3 开始,您的应用程序和插件不再全局实例化。这意味着您可以在同一个项目中创建多个 Vue 应用程序。

Vue 2:

import Vue from "vue";

new Vue({
  router,
  render: h => h(App)
}).$mount("#app");

Vue 3:

import { createApp, h } from "vue";

const app = createApp({
  render: () => h(App)
});

app.use(router);

app.mount("#app");

v-if + v-for

v-for 列表中使用 v-if 条件在 Vue 2 中是可以使用的。出于性能原因,此行为已在 Vue 3 上禁用。

从 Vue 3 开始,你将不得不使用计算列表属性。

v-model

v-model API API 在 Vue 3 上发生了一些变化。

首先,属性 value 已重命名为 modelValue

<ChildComponent v-model="pageTitle" />

ChildComponent 需要像这样重写:

props: {
  modelValue: String // previously was `value: String`
},

emits: ['update:modelValue'],

methods: {
  changePageTitle(title) {
    this.$emit('update:modelValue', title)
  }
}

很酷的事情是现在可以有多个 v-model 自定义值。例如 v-model:valueA, v-model:valueB......

$emit

在 Vue 2 中,可以使用 Vue 实例通过 vm.onvm.on 和 vm.off 创建全局 EventBus。从 Vue 3 开始,不可能再这样做了,因为 vm.onvm.on 和 vm.off 已从 Vue 实例中删除。

我们建议使用 Mitt 库作为替代:

mounted() {
  this.eventbus = mitt();

  eventbus.on("ready", () = {
    console.log("Event received");
  });

  eventbus.emit("ready");
}

使用 Vue 2 的相同代码是:

mounted() {
  this.$on("ready", () = {
    console.log("Event received");
  });

  this.$emit("ready");
}

Vue 3 仍然可以从组件向其父级发出事件,但是,所有事件都必须通过新的选项emit声明(它与现有的 props 选项非常相似)。

举个例子,如果你的组件有一个 @click 属性,使用 this.$emit('click') 触发,那么你需要在你的组件选项中声明 click 事件,如下所示:

props: {
  name: {
    type: String,
    default: ""
  },
},

emits: ["click"], // events have to be declared here

data() {
  return {
    value: ""
  }
}

迁移策略

我们的 app 叫做 Crisp。它是一个大型商业消息传递应用程序,全世界有 30 万家公司每天都在使用它。公司使用 Crisp 使用集中聊天、电子邮件和许多其他渠道的团队收件箱回复客户。

由于我们当前的应用程序被许多不同的用户使用,对我们来说,不破坏任何东西显然很重要。因此,我们还需要每天改进我们的软件,以便我们可以迭代错误和新功能。

所以我们需要有两个不同的分支:

  • 我们的主要分支,用于 Vue 2.6,具有我们当前的发布生命周期
  • vue3 分支,用于将 Vue 2 迁移到 Vue 3

此外,我们每天都会发布一个运行 Vue 3 代码库的测试版,并将几乎每天在 Vue 2 分支上所做的更改反向移植到 Vue 3,防止在合并 Vue 3 代码库后头疼。

最后,我们选择不依赖新的 Vue API,例如 Composition API,只迁移必要的代码。这背后的原因是为了降低引入回归的风险。

更新 Vue Build 工具

我们的应用依赖于 Vue Cli (webpack)。我们选择不迁移到 Vite,以防止我们引入新问题,因为我们的构建系统非常复杂。

迁移 Vue Cli 以支持 Vue 3 非常容易。

您必须首先编辑 package.json 文件以更新 Vue.js 及其依赖项。

所以我们将 "vue": "^2.6.12" 替换为 "vue": "^3.2.6"

此外,我们将不得不使用 "@vue/compat": "^3.2.6" ,以便将代码库从 Vue 2 平滑地迁移到 Vue 3。

同时我们更新 "vue-template-compiler": "^2.6.12""@vue/compiler-sfc": "^3.2.6"

现在,我们将不得不更新我们的 vue.config.js 文件并编辑 chainWebpack 函数。它将强制您所有现有的库使用 @vue/compat 包。

// Vue 2 > Vue 3 compatibility mode
  config.resolve.alias.set("vue", "@vue/compat");

  config.module
    .rule("vue")
    .use("vue-loader")
    .loader("vue-loader")
    .tap(options => {
      // Vue 2 > Vue 3 compatibility mode
      return {
        ...options,
        compilerOptions: {
          compatConfig: {
            // default everything to Vue 2 behavior
            MODE: 2
          }
        }
      };
    });

更新我们的 main.js 文件

我们现在需要像这样实例化 Vue:

import { createApp, h, configureCompat } from "vue";

const app = createApp({
  render: () => h(App)
});

app.use(router);
app.use(store);
// Initiate other plugins here

configureCompat({
  // default everything to Vue 2 behavior
  MODE: 2
});


app.mount("#app");

更新 Vue Router

我们将不得不使用 Vue Router 最新版本“vue-router”:“4.0.11”

使用最新的 Vue.js Router 没有太大区别。主要区别在于您必须使用以下方法手动启用历史记录模式:

import { createWebHistory, createRouter } from "vue-router";

var router = createRouter({
  history: createWebHistory(),
  routes: [
    // all your routes
  ]
});

Migrating Filters(迁移过滤器)

我们现有的应用程序在很大程度上依赖于过滤器(大约 200 次使用)。第一件事是找出我们使用了多少过滤器。由于现代 IDE(VSCode、SublimeText)支持正则表达式搜索,我们制作了一个正则表达式,以便我们可以在我们的模板中找出所有 Vue 过滤器的用法:{{ (.*?)\|(.*?) }}

Sublime Text 的正则搜索结果

由于 Vue 3 完全放弃了过滤器支持,我们试图找到一种优雅的方式来仍然使用过滤器。解决方案是将 Vue 过滤器迁移到自定义的 Singletons Helpers。

例如在 Vue 2 上:

Vue.filter("uppercase", function(string) {
  return string.toUpperCase();
});

在 Vue 3 上变成:

uppercase(string) {
  return string.toUpperCase();
}

export { uppercase };

然后我们全局实例化了 StringsFilter 类:

import { uppercase } from "@/filters/strings";

app.config.globalProperties.$filters = {
  uppercase: uppercase
};

最后,我们可以使用我们的过滤器

{{ firstName | uppercase }} 变为{{ $filters.uppercase(firstName) }}

修复错误

随着迁移过程的进行,您会在浏览器的控制台中发现报错。Vue 3 兼容模式带有不同的日志,可帮助您将应用程序迁移到 Vue 3。

Vue 3 compat mode comes with explicit warnings

与其尝试逐个功能或逐个模板迁移您的应用功能,我们建议您按 API 迁移 API。

例如,如果您在日志中看到 WATCH_ARRAY 弃用通知,我们建议您查看日志中提供的迁移指南

然后,逐步迁移一步一步观察。

完成所有的迁移后,然后您可以关闭此功能的兼容模式:

import { createApp, h, configureCompat } from "vue";

const app = createApp({
  render: () => h(App)
});

// Initiate all plugins here

configureCompat({
  // default everything to Vue 2 behavior
  MODE: 2,

  // opt-in to Vue 3 behavior for non-compiler features
  WATCH_ARRAY: false
});


app.mount("#app");

更新依赖

Vue 3 带有 @vue/compat 包,支持 Vue 2 和 Vue 3 库。但是,@vue/compat 模式也会带来性能下降。

只有在将您的应用程序转换为 Vue 3 及其所有库时才能使用兼容模式。转换所有项目和库后,您可以摆脱兼容模式。

所以我们为了实现这一点,我们必须将所有库迁移到 Vue 3 兼容的库。

所有官方 Vue 库(Vuex、Vue Router)都已移植到 Vue 3,而大多数流行的包已经有 Vue 3 兼容版本。

我们使用的大约 20% 的社区库没有 Vue 3 版本,所以我们选择 fork 所有尚未移植到 Vue 3 的库,例如 vue-router-prefetch.

移植 Vue 3 库通常非常简单,需要 30 分钟到半天时间,取决于库的复杂性。

一旦我们完成了一个库的迁移以支持 Vue 3,我们为原始库创建了一个“拉取请求”,以便其他人可以从我们的工作中受益。

关于性能

由于新的响应反应系统,Vue 3 带来了许多不同的性能改进。在大多数情况下,对于一些复杂的列表,Javascript 堆大小已从 20-30% 减少到 50%。对于我们的消息传递用例,我们看到了 CPU 的重大改进。

我们新的 Crisp 应用程序使用起来更快、更轻。

结论

在撰写本文时,我们正在生产中部署新的 Crisp 应用程序。迁移这个应用程序花了我们大约 2 周的时间,我们是 2 个开发人员,几乎是全职的。

相比之下,从 Angular 1 迁移到 Vue 2 花了我们大约 9 个月的时间

从 Vue 2 切换到 Vue 3 是一项重要但并非不可能的任务。这个新的 Vue 版本提供了一些很棒的性能改进。

为了感谢 Vue.js 社区,我们已经开始作为金牌赞助商为该项目做出贡献。作为一家公司,我们觉得我们必须授权开源项目来构建一个软件和环境可以相互交流的更美好的世界。