Vue 3へのマイグレーション

Vue 2 から Vue 3 へのマイグレーションは、エピック&6252 で追跡されています。

Vue 3.xへのマイグレーションを容易にするため、コードベースで以下の非推奨機能を使用できないようにするESLintルールを追加しました。

Vueフィルタ

なぜですか?

Vue 3 APIからフィルタが完全に削除されました。

代わりに使用するもの

コンポーネントの計算されたプロパティ/メソッド、または外部ヘルパー。

イベントハブ

なぜですか?

$onVueインスタンスから$once$off のメソッドが削除されたため、Vue 3ではイベントハブを作成するために使用できません。

どのような場合に

イベントハブを使用していないVueアプリの場合は、絶対に必要な場合を除き、新しいイベントハブを追加しないようにしてください。例えば、子コンポーネントが親のイベントに反応する必要がある場合、propを渡すのが好ましいです。そして、子コンポーネントでそのpropのwatchプロパティを使用して、必要な副作用を作成します。

異なるVueアプリ間で)コンポーネントをまたいだ通信が必要な場合は、ハブを導入するのが正しい選択かもしれません。

代わりに使用するもの

新しいミットのようなイベントハブをインスタンス化するために使用できるファクトリを作成しました。

これにより、既存のイベントハブを新しい推奨アプローチにマイグレーションしたり、新しいイベントハブを作成したりするのが簡単になります。

import createEventHub from '~/helpers/event_hub_factory';

export default createEventHub();

ファクトリーを使用して作成されたイベントハブは、Vue 2のイベントハブ($on,$once,$off,$emit)と同じメソッドを公開するため、以前のアプローチとの下位互換性があります。

<template functional>

なぜですか?

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
  }
})

プロップスのデフォルト関数this アクセス

なぜですか?

Vue 3では、propsデフォルト値ファクトリ関数はthis (コンポーネントインスタンス)にアクセスできなくなりました。

代わりに使用するもの

他のプロップから必要な値を解決する計算プロップを記述します。これは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で更新されたバージョンを使用するための戦略が必要です。

目標

  • 新しいライブラリをサポートするために、既存のコードに加える変更はできるだけ少なくすべきです。その代わりに、ファサードとして機能する新しいコードを追加し、新しいバージョンと古いバージョンの互換性を保ちます。
  • 新しいバージョンと古いバージョンの切り替えは、ツール(webpack / Jest)の中に隠すべきであり、コードに公開すべきではありません。
  • マイグレーションに特化したすべてのファサードは、将来のマイグレーション手順を簡素化するために同じディレクトリに置くべきです。

ステップバイステップのマイグレーション

ステップバイステップのガイドでは、VueApolloデモプロジェクトをマイグレーションします。GitLabプロジェクトの複雑なツール設定のニュアンスを避けながら、マイグレーションに集中することができます。このプロジェクトは意図的にGitLabと同じツールを使用しています:

  • webpack
  • 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

note
Vue.version をスタブすることで、デモプロジェクトのVueApollo関連のイシューは解決されますが、特定のシナリオでは反応性が失われるため、アップグレードが必要です。

ステップ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では、両方のステップが1つにマージされ、即座にハンドラを登録し、設定を渡します:

app.use(apolloProvider)

この動作をバックポートするには、以下の知識が必要です:

  • apolloProvider Vueインスタンスに提供された追加オプションには、$options を介してアクセスできます。this.$options.apolloProvider
  • Vueインスタンスの現在のapp (Vue.js 3での意味)にアクセスするには、次のようにします。this.$.appContext.app
note
この場合、非公開のVue.js 3 APIに依存しています。しかし、@vue/compat ビルドは 3.2.x ブランチでのみ利用可能になる予定なので、この API が変更されるリスクは低減されています。

この知識があれば、ツールの初期化を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自体)は、クラス(以前はnew Vue )の代わりにcreateApp のようなファクトリを使用することを好みます。

VueApollo クラスには2つの目的がありました:

  • を作成するためのコンストラクタapolloProvider
  • コンポーネントへのアポロ関連ロジックのインストール

私たちのコードベースに存在する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とほぼ同じコード(インポートを除く)になりました。ファサードを別ファイルに移動し、Vue.js 3を使用する際にvue-apollo がインポートされた場合に、webpack を条件付きで実行するように設定します:

--- 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 バージョンの*both- を再び実行することができるはずです。 これまでのステップで変更したすべての最終的な MRでは、index.js (アプリケーション・コード) には変更がありません。

このアプローチをGitLabプロジェクトに適用してみましょう。

VueApollo v4のサポートを追加するコミットでは、ステップバイステップのガイドではカバーされていない追加のニュアンスを見ることができます:

  • ファサードにインポートを追加する必要があるかもしれません(GitLabのコードではApolloMutation コンポーネントを使用しています)。
  • webpack だけでなく Jest のエイリアスも更新する必要があります。