🦐

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

2021/09/13に公開
2

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

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

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

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

https://github.com/azukiazusa1/vue3-migrate-test

Vue 2からの移行を体験してみたい場合には、vue2-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へ移行をしない選択肢もあるでしょう。

注意事項

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

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

アップグレードの実施

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

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

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

コミットログ

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

ツールのインストール

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

$ vue upgrade

今回はすでに最新の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@vue/compatにエイリアスし、Vueのコンパイラオプションでcompatモードを有効にします。

vue-cliでのサンプルは以下の通りです。

  • vue.config.js
vue.cofig.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
.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
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
src/rouer/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
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
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
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に比べて差分は少ないので一つにまとめてしまいます。
new Vuex.Storeの代わりにcreateStoreを使用するのと、Vue.use(Vuex)を削除します。

  • src/store/index.ts
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の大きな変更点の一つとして、Vueグローバルインスタンスが廃止になった点があります。

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

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

main.tsの修正

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

  • src/main.ts 修正前
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 修正後
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もついでに修正しておきます

  • 修正前
shims-vue.d.ts
declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}
  • 修正後
shims-vue.d.ts
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に型定義を与えます。

vuex.d.ts
import { Store } from "vuex";

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

$listenersの削除

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

  • src/components/AppInput.vue
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 変更前
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 変更後
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
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
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
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 = {};

コミットログ

参考

GitHubで編集を提案

Discussion