Vue.js

Vue2のプロジェクトをVue3へマイグレーションsする

Vue 3が正式リリースされてから約1年が経過しました。 Vuetifyのリリース目標である2021年Q3も近づく中でそろそろVue3へのアップデートを検討されている方もいらっしゃることでしょうか? この記事ではVue 2からVue 3への移行手順を記述していきます。

Vue 3 が正式リリースされてから約 1 年が経過しました。

Vuetify のリリース目標である 2021 年 Q3 も近づく中でそろそろ Vue3 へのアップデートを検討されているほうもいらっしゃることでしょうか?

この記事では Vue 2 から Vue 3 への移行手順を記述します。

参考用のプロジェクトとして以下レポジトリを用意しました。

Vue 2 からの移行を体験してみたい場合には、vue-todo-app のタグにチェックアウトしてください。

移行ビルドを使用する

Vue 2 から Vue 3 へ移行するためのツールとして、公式から@vue/compatが提供されています。

@vue/compat を使用すると、Vue 2 モードで動作するため、Vue 3 で削除や非推奨になった API も一部の例外を除いてそのまま使用できます。Vue3 で削除で非推奨になった機能は実行時に警告が出力されるようになります。この動作は、コンポーネントごと有効と無効を設定することもできます。

移行ビルドは将来のマイナーバージョン(2021 年の年末移行)で終了する予定なので、それまでに標準ビルドへの切り替えを目指す必要があります。

またアプリケーションの規模が大きく複雑な場合には移行ビルドを使用しても移行困難な場合があります。バージョン 2.7 のリリース(2021 年第 3 四半期後半に予定)で Composiiton API やその他の Vue 3 の機能が利用できる予定ですので、Vue 3 へ移行をしない選択肢もあるでしょう。

注意事項

現在次のような制限事項が存在しているため、マイグレーションツールを適用できない可能性があります。

  • VuetifyQuasarElemenntUIなどのコンポーネントライブラリに依存している場合。Vue 3 と互換性のあるバージョンがリリースされることを待つ必要があるでしょう。
  • IE11 サポート。Vue 3 は IE11 にサポートを中止しているので、IE11 のサポートの必要がある場合には Vue 3 への移行できないでしょう。
  • Nuxt.js。Nuxt 3 のリリースを待ちましょう。

アップグレードの実施

それでは実施に Vue 2 のアプリケーションを Vue 2 へ移行する手順を実施してみましょう。

非推奨スロット構文の削除

移行手順を実施する前の手順として、2.6.0 で非推奨となったスロット構文を削除する必要があります。

コミットログ

  • src/components/TodoForm.vue
<app-button>
-  <template slot="text">更新する</template>
+  <template v-slot:text>更新する</template>
</app-button>

ツールのインストール

必要に応じてツールをアップグレードします。 参考用のプロジェクトでは vue-cli を使用しているので、vue upgrage で最新の @vue/cli-service アップグレードします。

$ vue upgrage

今回はすでに最新の vue-cli を使用していたので差分は特にありません。

続いて以下のバージョンのパッケージをインストールします。

  • vue => 3.1
  • @vue/compat => 3.1(vue と同じバージョン)
  • vue-template-compiler => @vue/compiler-sfc@3.1.0 に置き換える
$ npm i vue@3.1 @vue/compat@3.1
$ npm i -D @vue/compiler-sfc@3.1.0
$ npm uninstall vue-template-compiler -D

vue-compiler-sfc の 3.2.x を使用していると Uncaught TypeError: Object(...) is not a function のようなエラーが発生するので注意します。ここのコミット時点では失敗して 1 時間くらい溶かしました。

コミットログ

ビルド設定の修正

ビルド設定で、vue@vue/compat にエイリアスし、Vue のコンパイラオプションで compat モードを有効にします。

`vue-cliP でのサンプルは以下のとおりです。

  • vue.config.js
// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.resolve.alias.set('vue', '@vue/compat')
 
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => {
        return {
          ...options,
          compilerOptions: {
            compatConfig: {
              MODE: 2
            }
          }
        }
      })
  }
}

コミットログ

コンパイルエラーの修正

ここまでの手順を進めたうえで、たくさんのコンパイルエラーに遭遇しています。 それらを 1 つずつ修正していきましょう。

eslintの修正

eslint-plugin-vue の 7.x をインストールし、'plugin:vue/vue3-recommended' に設定を置き換えます。

$ npm i eslint-plugin-vue@7 -D
  • .eslintrc.js
extends: [
-  "plugin:vue/essential",
+  "plugin:vue/vue3-recommended",
  "eslint:recommended",
  "@vue/typescript/recommended",
  "@vue/prettier",
  "@vue/prettier/@typescript-eslint",
],

lint を実行してみましょう。いくつかの修正不可能なエラーが出力されますが、後ほど修正します。

$ npm run lint

lint によって自動で修正された項目もあります。 .sync 修飾子が削除され、v-model に置き換えられた項目ですね。

コンポーネントでの v-model の使用方法が作り直され、 v-bind.sync が置き換えられました

  • src/components/EditTodo.vue
<todo-form
  @submit="onSubmit"
-  :title.sync="title"
-  :description.sync="description"
-  :status.sync="status"
+  v-model:title="title"
+  v-model:description="description"
+  v-model:status="status
/>

コミットログ

vue-routerのアップグレード

vue-router を使用している場合には、v4 へアップグレードします。

npm i vue-router@4

vue-router をアップグレードしたことによっていろいろコンパイルエラーが発生しているの修正していきましょう。

new VueRouter => createRouter

VueRouter はクラスではなくなったので、new VueRouter() の代わりに createRouter を使用します。

  • src/router/index.ts
- import VueRouter, { RouteConfig } from "vue-router";
+ import { createRouter } from "vue-router";
 
// 省略
 
- const router = new VueRouter({
+ const router = createRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes,
});

mode: history => history: createWebHistory()

mode オプションは削除され、history に置き換わります。modehistory を指定していた場合には、createWebHistory() を指定します。 また base オプションは、createWebHistory() の引数として受け取るようになります。

  • src/router/index.ts
- import { createRouter } from "vue-router";
+ import { createRouter, createWebHistory } from "vue-router";
 
// 省略
 
const router = createRouter({
-  mode: "history",
-  base: process.env.BASE_URL,
+  history: createWebHistory(process.env.BASE_URL),
  routes,
});
RouteConfig => RouteRecordRaw

これはそのまま TypeScript のタイプの名前の変更です。

  • src/router/index.ts
- import { createRouter, createWebHistory } from "vue-router";
+ import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
 
Vue.use(VueRouter);
 
- const routes: Array<RouteConfig> = [
+ const routes: Array<RouteRecordRaw> = [
Vue.use(VueRouter)の削除

Vue.use(VueRouter) は不要なので削除します。(後ほど正しい router の登録方法に置き換えます)

  • src/router/index.ts
- import Vue from "vue";
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
 
- Vue.use(VueRouter);

コミットログ

vuexのアップグレード

vuex も同様に v4 へアップグレードします。

$ npm i vuex@4

vue-router に比べて差分は少ないので 1 つにまとめてしまいます。 new Vuex.Store の代わりに createStore を使用するのと、Vue.use(Vuex) を削除します。

  • src/store/index.ts
- import Vue from "vue";
- import Vuex from "vuex";
+ import { createStore } from "vuex";
import todos from "@/store/todos/index";
 
- Vue.use(Vuex);
 
export default createStore({
  modules: {
    todos,
  },
});

コミットログ

Vue グローバルAPIの修正

Vue 3 の大きな変更点の 1 つとして、Vue グローバルインスタンスが廃止になった点があります。

例えば、new Vue({})createApp に、Vue.use()app.use() に置き換わります。(appcreateApp によって生成されたインスタンスです)

また Vue.config.productionTip は廃止になっています。

main.ts の修正

まずは main.ts を修正します。

  • src/main.ts 修正前
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
 
Vue.config.productionTip = false
 
new Vue({
  router,
  store,
  render: (h) => h(App),
}).$mount("#app");
  • src/main.ts 修正後
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")

型エラーが発生するので shims-vue.d.ts もついでに修正しておきます。

  • 修正前
declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}
  • 修正後
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent
  export default component
}
Vue.extendの削除

Options API で TypeScript を使っていた場合、Vue.extend を使ってコンポーネントの型推論を提供していました、これも削除されています。 代わりに defineComponent を使用します。

コンポーネントすべて置き換える必要があるのでかなり面倒ですね。.。

- import Vue from "vue";
+ import { defineComponent } from "vue";
 
- export default Vue.extend({
+ export default defineComponent({
$storeの型定義

this.$store の型定義が失われているので、vuex.d.ts ファイルを作成して this.$store に型定義を与えます。

import { Store } from "vuex";
 
declare module "@vue/runtime-core" {
  interface ComponentCustomProperties {
    $store: Store;
  }
}

$listeners の削除

$listeners オブジェクトは Vue 3 で削除され、単に $attrs オブジェクトの一部になりました。

  • src/components/AppInput.vue
<template>
  <label>
    {{ label }}
    <input id="input" v-bind="$attrs" />
  </label>
</template>
 
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  inheritAttrs: false,
  props: {
    label: {
      type: String,
      default: "",
    },
  },
});
</script>

フィルターの削除

Vue 3 ではフィルター構文が削除されました。 代わりに単純なメソッドか算出プロパティに置き換えます。

  • src/components/TodoItem.vue 変更前
<template>
  <div class="card">
    <div>
      <span class="title">
        <router-link :to="`todos/${todo.id}`">{{ todo.title }}</router-link>
      </span>
      <span class="status" :class="todo.status">{{ todo.status }}</span>
    </div>
    <div class="body">作成日:{{ todo.createdAt | formatDate }}</div>
    <hr />
    <div class="action">
      <button @click="clickDelete">削除</button>
    </div>
  </div>
</template>
 
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { Todo } from "@/repositories/TodoRepository/types";
 
export default defineComponent({
  props: {
    todo: {
      type: Object as PropType<Todo>,
      required: true,
    },
  },
  filters: {
    formatDate(date: Date): string {
      return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
    },
  },
  methods: {
    clickDelete() {
      this.$emit("delete", this.todo.id);
    },
  },
});
</script>
  • src/components/TodoItem.vue 変更後
<template>
  <div class="card">
    <div>
      <span class="title">
        <router-link :to="`todos/${todo.id}`">{{ todo.title }}</router-link>
      </span>
      <span class="status" :class="todo.status">{{ todo.status }}</span>
    </div>
    <div class="body">作成日:{{ formatDate }}</div>
    <hr />
    <div class="action">
      <button @click="clickDelete">削除</button>
    </div>
  </div>
</template>
 
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { Todo } from "@/repositories/TodoRepository/types";
 
export default defineComponent({
  props: {
    todo: {
      type: Object as PropType<Todo>,
      required: true,
    },
  },
  computed: {
    formatDate(): string {
      const { createdAt } = this.todo;
      return `${createdAt.getFullYear()}/${
        createdAt.getMonth() + 1
      }/${createdAt.getDate()}`;
    },
  },
  methods: {
    clickDelete() {
      this.$emit("delete", this.todo.id);
    },
  },
});
</script>

<template v-forの使用

VueCompilerError: <template v-for> key should be placed on the <template> tag.

Vue 2 では <template> タグに key を含めることができなかったため、それぞれの子要素に key を配置していました。Vue 3 では <template> タグに key を含めることができるようになったのでこれを修正します。

  • src/pages/TodoList.vue
- <template v-for="(todo, index) in todos">
-   <span :key="`index-${todo.id}`">{{ index + 1 }}</span>
-   <todo-item :todo="todo" :key="`item-${todo.id}`" @delete="deleteTodo" />
+ <template v-for="(todo, index) in todos" :key="todo.id">
+   <span>{{ index + 1 }}</span>
+   <todo-item :todo="todo" @delete="deleteTodo" />

ここまでの手順で問題なくアプリケーションが動作してるかと思います🎉。

コミットログ

個々の警告の修正

@vue/compat によってアプリケーションは動作していますが、完全に Vue 3 へ移行するためには warning レベルのエラーも修正する必要があります。

スクリーンショット 2021-09-12 14.18.22

試しに 2 つ目の deprecation COMPONENT_V_MODEL を修正しましょう。 これは、カスタムコンポーネントで v-model を使用する際にプロパティとイベント名が変更になったものです。

  • プロパティ: value -> modelValue
  • イベント: input -> update:modelValue

以下のように修正しました。

  • src/components/AppInput.vue
<template>
  <label>
    {{ label }}
    <input
      :value="modelValue"
      @input="(e) => $emit('update:modelValue', e.target.value)"
    />
  </label>
</template>
 
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  props: {
    modelValue: {
      type: String,
      default: "",
    },
    label: {
      type: String,
      default: "",
    },
  },
});
</script>

すると、新たな警告が出力されます。

スクリーンショット 2021-09-12 14.26.05

現在 Vue 2 のモードで動作させているものの Vue 3 の構文を使用しているために発生しているエラーです。 これを取り除くために compatConfig のオプションを修正します。

  • src/components/AppInput.vue
import { defineComponent } from "vue";
export default defineComponent({
+  compatConfig: {
+   COMPONENT_V_MODEL: false,
+  },
  props: {
    modelValue: {
      type: String,
      default: "",
    },
    label: {
      type: String,
      default: "",
    },
  },
});

compatConfigCOMPONENT_V_MODEL: false を追加しました。上記のように、コンポーネントごとに Vue 3 の動作をオプトインすることが可能です。

コンポーネント単位ではなく、グローバル設定で変更することも可能です。

import { configureCompat } from 'vue'
 
// 特定の機能のために compat を無効にする
configureCompat({
  FEATURE_ID_A: false,
  FEATURE_ID_B: false
})

コミットログ

完全な移行

すべての警告を削除できたら、移行ツールを取り除くことができます!

$ npm uninstall @vue/compat
  • vue.config.js
// vue.config.js
module.exports = {};

コミットログ

参考


Contributors

> GitHub で修正を提案する
この記事をシェアする
はてなブックマークに追加

関連記事