はじめに
この記事では、「既存プロジェクトをとにかくVue3へ移行して元通り動くようにする」が目的です。
「Composition APIで書き換える」といったVue 3の新機能への移行を紹介するものではありません。
公式のマイグレーションガイドはこちら:
https://v3.vuejs.org/guide/migration/introduction.html#quickstart
執筆時点ではVue3.0.0ですが、ここに書く問題は以降のバージョンで解決している可能性もあります。
もしお気づきの際はコメントいただけると幸いです。
雑感、設定やプラグイン周りで新しい仕様に合わせなければいけないところがありますが、コンポーネント資材は大部分がそのまま動作すると思います。
1日2日もあればとりあえず動作するところまでは持っていけるかと思います。
追記 2020/09/26:
ドキュメントからは一見読み取れないような細かな仕様変更によって動かなくなった箇所が結構ありました。
規模が大きめのプロジェクトの場合、本当に今移行が必要か考えたほうがいいと思います…。
Vue CLI
移行元のプロジェクトはかなり古いバージョンのvue-cliでセットアップされたものでした。
これを頑張ってバージョンアップしていくのはしんどいので、
新しいバージョンのVue CLIでプロジェクトを作り直して、そこに既存資材を移行しました。
$ sudo npm install -g @vue/cli
$ vue create project-name
Vue 3が選べるのは、Vue CLI 4.5以上となります。
追記: 今以降するならViteを推奨します。
main.js
必ず変更を要するのが、アプリの初期化部分です。
import Vue from 'vue'
import ...
new Vue({
el: '#app',
components: { App },
router
})
import { createApp } from 'vue'
import ...
const app = createApp(App)
app.use(router)
app.mount('#app')
この後の項目で順次置き換えていきますが、import Vue from 'vue'
のようにVueオブジェクトをインポートして使うことはなくなります。
プラグインの注入:
これまではnew Vue
の前にVue.use
でプラグインを注入していましたが、今回はcreateApp
で生成されたappインスタンスに対してuse
でプラグインを注入します。
マウントの挙動:
app.mount('#app')
これまではApp
コンポーネントが#app
エレメントを置き換える形でマウントされていましたが、Vue 3では#app
の内側にマウントされます。
つまり既存のコートは↓こうなります。
<div id="app">
<div id="app">
</div>
</div>
↑これを既存と同じ動作にするオプションがあるかと思いましたが、今のところ見つけられていません。
設定ファイル
これはVue 3というより、古めのVue CLIから移行する場合のみですが、webpack周りの設定ファイルが表からはなくなります。
カスタマイズが必要な場合は、vue.config.js
を作成して、そこに記述しましょう。
module.exports = {
...,
configureWebpack: {
...
}
}
プラグイン
Vue.js向けのプラグインをそのまま使おうとした場合に動かなくなる可能性があります。
長くなったので、こちらの記事に切り出しました。
『既存のVue.jsプラグインがVue 3で使えない場合の対応』
ざっくり言うと、動かなくなるものが結構ありそうですが、中身の大部分には問題がなく、プラグインのインストール部分などを少し直すだけで解決するケースが多そうです。
vuex
main.jsの書き換えのときに気づくことになると思いますが、直近までのバージョンのvuexやvue-routerは利用できません。
Vuex 4に置き換えます。
$ yarn add vuex@^4
書き換え必須なのがインストール部分となります。
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({ ... })
new Vue({
el: '#app',
store,
...
})
import { createApp } from 'vue'
import { createStore } from 'vuex'
const app = createApp(...)
const store = createStore({ ... })
app.use(store)
Vuex 4の変更点はチェックしていないものの、動かなくなった箇所は今のところありません。
vue-router
vue-routerも4に置き換えます。
$ yarn add vue-router@^4
こちらもインストール部分は書き換え必須で、routerインスタンス作成の部分も少し変わります。
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [...]
})
...
app.use(router)
vue-router4は、僕の場合、次に挙げる点について変更が必要でした。
*
にマッチさせたいとき
path: '*'
は使えなくなります。
{
path: '*',
name: 'NotFound',
...
}
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
...
}
router.match
無くなりました。
resolveというメソッドが同等のようです。
const matchedRoute = this.$router.match({ name: 'Index' })
const matchedRoute = this.$router.resolve({ name: 'Index' })
router.currentRoute
value
に潜る必要があります。
const currentRoute = router.currentRoute
const currentRoute = router.currentRoute.value
$router.push()が$routeを即時変更しない
$router.push()
は同期的に実行されなくなったようです。
this.$router.push({ query: { test: 123 } }).then(() => {
console.log(this.$route.query.test) // -> 123
})
console.log(this.$route.query.test) // -> 123
this.$router.push({ query: { test: 123 } }).then(() => {
console.log(this.$route.query.test) // -> 123
})
console.log(this.$route.query.test) // -> undefined
即時実行される前提で実装されたコードを洗い出して修正しなければいけないため絶望しています…。
Promise返すことすら知りませんでした。
paramsやqueryが必ず文字列で渡される
元々以下のように渡すと、遷移後の画面でそのままの値が取得できていました。
// 遷移元
this.$router.push({ ..., params: { id: 123 } })
// 遷移後
typeof this.$route.params.id // -> 123
それが、必ず文字列に変換されて届くようになったようです。
// 遷移元
this.$router.push({ ..., params: { id: 123 } })
// 遷移後
typeof this.$route.params.id // -> "123"
beforeの頃でも、リロードした際にはURL文字列である/xxx/123
からパースされるので、そのときに結局Stringになります。
つまりリロード前後で型が変わってしまうという現象が置きていましたが、afterからは最初から強制的に文字列になるため、リロード前後で変わりません。
また、paramsにはroutesで定義していないキーも渡せるため、「遷移先のページに秘密のデータを渡すぜ!」なノリでこんな風にObjectだとかを渡していた場合注意が必要です。
const secretData = { abc: 123 }
this.$router.push({ ..., params: { secretData } })
// 遷移後
this.$route.secretData // -> "[object Object]"
全て文字列になります。
beforeRouteLeave
そもそもこの処理はどうなんだ、というのは置いておいて、
Vue 3では、beforeRouteLeave
内でnext()
を実行せず、別の場所であとから実行しようとすると怒られて動きません。
beforeRouteLeave (_to, _from, next) {
if (...) {
this.solveLater = next
return
}
next()
}
next()
を実行しないケースがあるなら、nextを引数にとってはいけないみたいです。
その際、代わりにreturn Boolean
で遷移の許可/拒否を制御できるようになったぽいです(doc未確認)。
beforeRouteLeave (to, _from) {
if (...) {
this.solveLater = () => this.$router.push(to)
return false
}
return true
}
scrollBehavior
scrollBehavior
で返却すべきオブジェクトについて、x,yというプロパティがleft,topに変わります。
scrollBehavior (to, from, savedPosition) {
return { x: 0, y: 0 }
}
scrollBehavior (to, from, savedPosition) {
return { left: 0, top: 0 }
}
VueRouterについて、ここで挙げた以外にも、
利用ケースによっては他にも影響があるかもしれません。
vue-routerのマイグレーションガイド:
https://next.router.vuejs.org/guide/migration/#new-features
Vue.set / Vue.delete
Vue.jsは新しいプロパティの追加を検知できないので、その対処としてVue.set()
が必要となる場合がありました。
// これだと変更が検知されないので、
// this.awesomeObject.newKey = newValue
// こう↓
Vue.set(this.awesomeObject, 'newKey', newValue)
Vue3では、新しいプロパティが検知されるようになりました。
なので、単に使うのを辞めるだけです。
this.awesomeObject.newKey = newValue
Vue.delete
も同様です。
Vue.observable
Vue.observable
はありません。
今回はComposition API使わないと言ったものの、Vue.observable
頼りだった部分の書き換えに必要でした。
import Vue from 'vue'
class AwesomeClass {
count = 0
position = { x: 0, y: 0 }
constructor () {
Vue.observable(this) // インスタンス丸ごとリアクティブにしちゃえ
}
}
import { ref, reactive } from 'vue'
class AwesomeClass {
// プリミティブ向け
count = ref(0)
xxx () {
this.count.value = 1
console.log(this.count.value) // -> 1
}
// Object向け
position = reactive({ x: 0, y: 0 })
yyy () {
this.position.x = 1
console.log(this.position.x) // -> 1
}
}
コンポーネントの変更点への対処
公式の変更点を見ると、影響が出そうな箇所がありそうです。
destroyedの廃止
unmounted
になりました。
export default {
destroyed () {}
}
export default {
unmounted () {}
}
併せて、beforeDestroy
もbeforeUnmount
になりました。
あと、これは多分ユーザーが使うことを想定したプロパティじゃないのでドキュメントでは確認できませんが、
コンポーネントインスタンスが持つthis._isDestroyed
というプロパティはthis._.isUnmounted
になりました。
他のメタ的な値もthis._
にまとめられていると思います。
v-modelの仕様変更
v-model
エイリアスでprops/emitを貼る場合、
propsに渡される名前がvalue
からmodelValue
に変更になり、
emitすべきイベント名もinput
からupdate:modelValue
に変更になります。
詳細はこちら
dataオプションの挙動の変化
以下のソースを見てもらうのが分かりやすいのですが、data内に定義されたObjectは元のObjectとは別物になってしまいます。
const MASTER_DATA = {}
export default {
data () {
return {
MASTER_DATA
}
},
computed: {
sameAsOriginal () {
// Vue2 returns true
// Vue3 returns false
return MASTER_DATA === this.MASTER_DATA
}
}
}
個人的にはこれはかなりしんどい変更です。
import { toRaw } from 'vue'
...
sameAsOriginal () {
return MASTER_DATA === toRaw(this.MASTER_DATA) // -> true
}
dataオプションを通すと、Proxyオブジェクトとなるため、オリジナルの値にはtoRaw
を使ってアクセスする必要があるようです。
transitionのクラス名
transition時に付与されるクラス名が一部変更になりました。
// フェードインの開始&フェードアウトの終了
.fadeInOut-enter,
.fadeInOut-leave-to {
opacity: 0;
}
// フェードインの終了&フェードアウトの開始
.fadeInOut-enter-to,
.fadeInOut-leave {
opacity: 1;
}
-from
がつきます。
// フェードインの開始&フェードアウトの終了
.fadeInOut-enter-from,
.fadeInOut-leave-to {
opacity: 0;
}
// フェードインの終了&フェードアウトの開始
.fadeInOut-enter-to,
.fadeInOut-leave-from {
opacity: 1;
}
詳細はこちら
transition自体にv-ifつけたときの挙動
これは終了のアニメが動かなくなります。
<transition v-if="..">
<div />
</transition>
このように書かなければいけません。
<transition>
<div v-if=".." />
</transition>
前者が動いていたのは意図していたものではなかったようですが、それに依存して<transition>
をルートに持つコンポーネントをたくさん作っていた場合、苦労すると思います。
watchでArrayを監視
watchでArrayを監視した際の挙動が変わりました。
Vue 3では、監視対象のArrayの内容が変わっても検知されません。
Arrayそのものが置き換わったときにのみ検知されます。
watch: {
arr: {
handler (newVal, oldVal) {
console.log(newVal, oldVal)
},
deep: true
}
}
deep
というオプションをtrue
にすることで、既存と同じ動作にすることができるようです。
watchで$routeを監視
watch
で$route
やそのqueryやparamsを監視している場合、注意が必要です。
「記事id」をURLに持つ「記事詳細画面」で、記事idの変更を監視していたとします。
watch: {
articleId (current) {
console.log(current) // -> この画面を離れる際にwatchが発火し、undefinedを返す
}
},
computed: {
articleId () {
return this.$rotue.params.id
}
}
Vue3では、この詳細画面から全く別の画面に遷移する瞬間も、paramsの変更が検知され、watchが発火します。
(Vue 3.0.betaは大丈夫だったのにVue 3.0にしたらそうなりました…)
Styleの読み込み順の変更
以下のような状況においてh1
は、Vue.jsならblue
、Vue 3ならred
となります。
<template>
<my-component />
</template>
<style>
/* Global style */
h1 { color: red; }
</style>
<template>
<h1 />
</template>
<style>
h1 { color: blue; }
</style>
Vue 3ではCSSの結合順が異なるようで、App.vueに記述されたスタイルは一番下になります。
App.vueにグローバルなスタイルを置いている場合は注意です。
多くの場合、子コンポーネントはscopedでスタイルをあてたり、class名にスタイルをあてたりするので、
グローバルなスタイルが一番下に定義されようが優先順位で負けることは少なく、一見気づきづらいです。
ドキュメントからもこれに関する記載を見つけられませんでした。
結構困ったんですが、後に定義されるほど上に来るということを逆手にとって、
↓こういうグローバルCSSを書くためだけのコンポーネントを作ることでとりあえずは一番上に定義できました。
<template>
<my-component />
<global-style /><!-- App.vueで一番下に置けば一番上の行に定義される -->
</template>
<style>
h1 { color: red; }
</style>
おわりに
おわりです