Vue2のプロジェクトをVue3へマイグレーションする
Vue 3が正式リリースされてから約1年が経過しました。
Vuetifyのリリース目標である2021年Q3も近づく中でそろそろVue3へのアップデートを検討されている方もいらっしゃることでしょうか?
この記事ではVue 2からVue 3への移行手順を記述していきます。
参考用のプロジェクトとして以下レポジトリを用意しました。
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へ移行をしない選択肢もあるでしょう。
注意事項
現在次のような制限事項が存在しているため、マイグレーションツールを適用することができない可能性があります。
- Vuetify・Quasar・ElementUIなどのコンポーネントライブラリに依存している場合。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 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.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
に置き換わります。mode
にhistory
を指定していた場合には、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
に比べて差分は少ないので一つにまとめてしまいます。
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の大きな変更点の一つとして、Vue
グローバルインスタンスが廃止になった点があります。
例えば、new Vue({})
はcreateApp
に、Vue.use()
はapp.use()
に置き換わります。(app
はcreateApp
によって生成されたインスタンスです)
また、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レベルのエラーも修正する必要があります。
試しに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>
すると、新たな警告が出力されます。
現在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: "",
},
},
});
compatConfig
にCOMPONENT_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 = {};
Discussion
vue upgrage -> vue upgrade
ですかね?
そのとおりでした、ご指摘ありがとうございます!