Help us learn about your current experience with the documentation. Take the survey.

迁移到 Vue 3

从 Vue 2 到 Vue 3 的迁移在 epic &6252 中进行跟踪。

为了简化向 Vue 3.x 的迁移,我们添加了 ESLint 规则, 防止我们在代码库中使用以下已弃用的功能。

Vue 过滤器

为什么?

过滤器已从 Vue 3 API 中完全 移除

改用什么

组件的计算属性 / 方法或外部辅助函数。

事件中心

为什么?

$on$once$off 方法已从 Vue 实例中 移除,因此在 Vue 3 中无法用于创建事件中心。

何时使用

如果你的 Vue 应用不使用任何事件中心,尽量避免添加新的事件中心,除非绝对必要。例如,如果你需要子组件响应父组件的事件,最好向下传递 prop。然后在子组件中使用该 prop 的 watch 属性来创建所需的副作用。

如果你需要跨组件通信(在不同的 Vue 应用之间),那么引入事件中心可能是正确的决定。

改用什么

我们创建了一个工厂,你可以用它来实例化一个新的类似 mitt 的事件中心。

这使得将现有事件中心迁移到新的推荐方法或创建新的事件中心变得更加容易。

import createEventHub from '~/helpers/event_hub_factory';

export default createEventHub();

使用工厂创建的事件中心暴露与 Vue 2 事件中心相同的方法($on$once$off$emit),使它们与我们之前的方法向后兼容。

功能性模板

为什么?

在 Vue 3 中,{ functional: true } 选项已被 移除,并且 <template functional> 不再被支持。

改用什么

功能性组件必须写成普通函数:

import { h } from 'vue'

const FunctionalComp = (props, slots) => {
  return h('div', `Hello! ${props.name}`)
}

除非你绝对需要立即提升性能,否则不建议用功能性组件替换有状态的组件。在 Vue 3 中,功能性组件的性能提升微乎其微。

使用 slot 属性的旧插槽语法

为什么?

在 Vue 2.6 中,slot 属性已被弃用,转而使用 v-slot 指令。slot 属性的使用仍然被允许,有时我们更喜欢使用它,因为它简化了单元测试(使用旧语法时,插槽在 shallowMount 上渲染)。然而,在 Vue 3 中我们不能再使用旧语法。

改用什么

使用 v-slot 指令的语法。要在 shallowMount 中修复插槽渲染,我们需要显式地存根一个带有插槽的子组件。

<!-- MyAwesomeComponent.vue -->
<script>
import SomeChildComponent from './some_child_component.vue'

export default {
  components: {
    SomeChildComponent
  }
}

</script>

<template>
  <div>
    <h1>Hello GitLab!</h1>
    <some-child-component>
      <template #header>
        Header content
      </template>
    </some-child-component>
  </div>
</template>
// MyAwesomeComponent.spec.js

import SomeChildComponent from '~/some_child_component.vue'

shallowMount(MyAwesomeComponent, {
  stubs: {
    SomeChildComponent
  }
})

Props 默认函数的 this 访问

为什么?

在 Vue 3 中,props 默认值工厂函数不再能够访问 this(组件实例)。

改用什么

编写一个计算属性,从其他 props 中解析所需的值。这将在 Vue 2 和 3 中都有效。

<script>
export default {
  props: {
    metric: {
      type: String,
      required: true,
    },
    title: {
      type: String,
      required: false,
      default: null,
    },
  },
  computed: {
    actualTitle() {
      return this.title ?? this.metric;
    },
  },
}

</script>

<template>
  <div>{{ actualTitle }}</div>
</template>

在 Vue 3 中, props 默认值工厂函数接收原始 props 作为参数,并且可以访问注入。

处理不兼容 @vue/compat 的库

问题

某些库依赖于 Vue.js 2 的内部实现。它们可能无法与 @vue/compat 一起工作,因此我们需要一种策略,在使用 Vue.js 3 的更新版本的同时保持与当前代码库的兼容性。

目标

  • 我们应该尽可能少地更改现有代码来支持新库。相反,我们应该 添加 新代码,这些代码将充当 facade(外观),使新版本与旧版本兼容
  • 新旧版本之间的切换应该隐藏在工具(webpack / jest)内部,不应暴露给代码
  • 所有与迁移相关的 facade 应该放在同一个目录中,以简化未来的迁移步骤

逐步迁移

在逐步指南中,我们将迁移 VueApollo Demo 项目。这将使我们能够专注于迁移的具体细节,同时避免 GitLab 项目中复杂工具设置的细微差别。该项目有意使用与 GitLab 相同的工具:

  • webpack
  • yarn
  • Vue.js + VueApollo

初始状态

克隆后,你可以使用 yarn serve 运行 Vue.js 2 版本的 VueApollo Demo,或使用 yarn serve:vue3 运行 Vue.js 3(compat 构建)版本。然而后者立即崩溃:

Uncaught TypeError: Cannot read properties of undefined (reading 'loading')

VueApollo v3(用于 Vue.js 2)在 Vue.js compat 中初始化失败

While stubbing Vue.version will solve VueApollo-related issues in the demo project, it will still lose reactivity on specific scenarios, so an upgrade is still needed

第 1 步:根据库文档执行升级

根据 VueApollo v4 安装指南,我们需要安装 @vue/apollo-option(这个包为 Options API 提供 VueApollo 支持)并更改我们的应用程序:

--- a/src/index.js
+++ b/src/index.js
@@ -1,19 +1,17 @@
-import Vue from "vue";
-import VueApollo from "vue-apollo";
+import { createApp, h } from "vue";
+import { createApolloProvider } from "@vue/apollo-option";

 import Demo from "./components/Demo.vue";
 import createDefaultClient from "./lib/graphql";

-Vue.use(VueApollo);
-
-const apolloProvider = new VueApollo({
+const apolloProvider = createApolloProvider({
   defaultClient: createDefaultClient(),
 });

-new Vue({
-  el: "#app",
-  apolloProvider,
-  render(h) {
+const app = createApp({
+  render() {
     return h(Demo);
   },
 });
+app.use(apolloProvider);
+app.mount("#app");

你可以在演示项目的 01-upgrade-vue-apollo 分支中查看这些更改

第 2 步:解决 Vue.js 2 和 3 中增强应用程序的差异

在 Vue.js 2 中,像 VueApollo 这样的工具是"延迟"初始化的:

// We are registering VueApollo "handler" to handle some data LATER
Vue.use(VueApollo)
// ...
// apolloProvider is provided at app instantiation,
// previously registered VueApollo will handle that
new Vue({ /- ... */, apolloProvider })

在 Vue.js 3 中,这两个步骤被合并为一个 - 我们立即注册处理器并传递配置:

app.use(apolloProvider)

为了向后移植这种行为,我们需要以下知识:

  • 我们可以通过 $options 访问提供给 Vue 实例的额外选项,因此额外的 apolloProvider 将显示为 this.$options.apolloProvider
  • 我们可以通过 this.$.appContext.app 在 Vue 实例上访问当前的 app(Vue.js 3 的含义)

We’re relying on non-public Vue.js 3 API in this case. However, since @vue/compat builds are expected to be available only for 3.2.x branch, we have reduced risks that this API will be changed

有了这些知识,我们可以在 Vue2 中尽可能早地移动工具的初始化 - 在 beforeCreate() 生命周期钩子中:

--- a/src/index.js
+++ b/src/index.js
@@ -1,4 +1,4 @@
-import { createApp, h } from "vue";
+import Vue from "vue";
 import { createApolloProvider } from "@vue/apollo-option";

 import Demo from "./components/Demo.vue";
@@ -8,10 +8,13 @@ const apolloProvider = createApolloProvider({
   defaultClient: createDefaultClient(),
 });

-const app = createApp({
-  render() {
+new Vue({
+  el: "#app",
+  apolloProvider,
+  render(h) {
     return h(Demo);
   },
+  beforeCreate() {
+    this.$.appContext.app.use(this.$options.apolloProvider);
+  },
 });
-app.use(apolloProvider);
-app.mount("#app");

你可以在演示项目的 02-bring-back-new-vue 分支中查看这些更改

第 3 步:重新创建 VueApollo

Vue.js 3 库(以及 Vue.js 本身)倾向于使用像 createApp 这样的工厂而不是类(之前的 new Vue

VueApollo 类有两个用途:

  • 用于创建 apolloProvider 的构造函数
  • 在组件中安装 apollo 相关逻辑

我们可以利用我们代码库中已有的 Vue.use(VueApollo) 代码,在其中隐藏我们的 mixin,避免修改我们的应用程序代码:

--- a/src/index.js
+++ b/src/index.js
@@ -4,7 +4,26 @@ import { createApolloProvider } from "@vue/apollo-option";
 import Demo from "./components/Demo.vue";
 import createDefaultClient from "./lib/graphql";

-const apolloProvider = createApolloProvider({
+class VueApollo {
+  constructor(...args) {
+    return createApolloProvider(...args);
+  }
+
+  // called by Vue.use
+  static install() {
+    Vue.mixin({
+      beforeCreate() {
+        if (this.$options.apolloProvider) {
+          this.$.appContext.app.use(this.$options.apolloProvider);
+        }
+      },
+    });
+  }
+}
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
   defaultClient: createDefaultClient(),
 });

@@ -14,7 +33,4 @@ new Vue({
   render(h) {
     return h(Demo);
   },
-  beforeCreate() {
-    this.$.appContext.app.use(this.$options.apolloProvider);
-  },
 });

你可以在演示项目的 03-recreate-vue-apollo 分支中查看这些更改

第 4 步:将 VueApollo 类移动到单独的文件并设置别名

现在,我们拥有与 Vue.js 2 版本几乎相同的代码(不包括导入)。我们将我们的 facade 移动到单独的文件,并设置 webpack 在使用 Vue.js 3 时,如果导入 vue-apollo,则有条件地执行它:

--- a/src/index.js
+++ b/src/index.js
@@ -1,5 +1,5 @@
 import Vue from "vue";
-import { createApolloProvider } from "@vue/apollo-option";
+import VueApollo from "vue-apollo";

 import Demo from "./components/Demo.vue";
 import createDefaultClient from "./lib/graphql";
diff --git a/webpack.config.js b/webpack.config.js
index 6160d3f..b8b955f 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -12,6 +12,7 @@ if (USE_VUE3) {

   VUE3_ALIASES = {
     vue: "@vue/compat",
+    "vue-apollo": path.resolve("src/vue3compat/vue-apollo"),
   };
 }

(为清晰起见,省略了将 VueApollo 类从 index.js 移动到 vue3compat/vue-apollo.js 作为默认导出的过程)

你可以在演示项目的 04-add-webpack-alias 分支中查看这些更改

第 5 步:观察结果

此时,你应该能够再次使用 yarn serve 运行 Vue.js 2 版本,使用 yarn serve:vue3 运行 Vue.js 3 版本。最终的 MR 显示了前面所有步骤的更改,但没有对 index.js(应用程序代码)进行更改,这是我们的目标

在 GitLab 项目中应用这种方法

添加 VueApollo v4 支持的提交 中,我们可以看到逐步指南未涵盖的其他细微差别:

  • 我们可能需要为我们的 facade 添加额外的导入(GitLab 中的代码使用 ApolloMutation 组件)
  • 我们需要更新 webpack 和 jest 的别名,以便我们的测试也能使用我们的 facade

单元测试

有关实施单元测试或修复在使用 Vue 3 时失败的测试的更多信息, 请阅读 Vue 3 测试指南