既存のVue.jsプロジェクトをVue 3へ移行したときに必要だった修正まとめ

# はじめに

この記事では、「既存プロジェクトを**とにかく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](https://ja.vitejs.dev/)を推奨します。

# main.js

必ず変更を要するのが、アプリの初期化部分です。

```js:before
import Vue from 'vue'
import ...

new Vue({
  el: '#app',
  components: { App },
  router
})
```

```js:after
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`の内側にマウントされます。

つまり既存のコートは↓こうなります。

```html
<div id="app">
  <div id="app">
  </div>
</div>
```

↑これを既存と同じ動作にするオプションがあるかと思いましたが、今のところ見つけられていません。

# 設定ファイル

これはVue 3というより、古めのVue CLIから移行する場合のみですが、webpack周りの設定ファイルが表からはなくなります。

カスタマイズが必要な場合は、`vue.config.js`を作成して、そこに記述しましょう。

```js:vue.config.js
module.exports = {
  ...,
  configureWebpack: {
    ...
  }
}
```

# プラグイン

Vue.js向けのプラグインをそのまま使おうとした場合に動かなくなる可能性があります。

長くなったので、こちらの記事に切り出しました。

『[既存のVue.jsプラグインがVue 3で使えない場合の対応](https://qiita.com/laineus/items/2b00aca6aa64a455e556)』

ざっくり言うと、動かなくなるものが結構ありそうですが、中身の大部分には問題がなく、プラグインのインストール部分などを少し直すだけで解決するケースが多そうです。

# vuex

main.jsの書き換えのときに気づくことになると思いますが、直近までのバージョンのvuexやvue-routerは利用できません。

Vuex 4に置き換えます。

```
$ yarn add vuex@^4
```

書き換え必須なのがインストール部分となります。

```js:before
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
const store = new Vuex.Store({ ... })
new Vue({
  el: '#app',
  store,
  ...
})
```

```js:after
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インスタンス作成の部分も少し変わります。

```js
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
  history: createWebHistory(),
  routes: [...]
})
...
app.use(router)
```

vue-router4は、僕の場合、次に挙げる点について変更が必要でした。

## `*`にマッチさせたいとき

`path: '*'`は使えなくなります。

```js:before
{
  path: '*',
  name: 'NotFound',
  ...
}
```

```js:after
{
  path: '/:pathMatch(.*)*',
  name: 'NotFound',
  ...
}
```

## router.match

無くなりました。
resolveというメソッドが同等のようです。

```js:before
const matchedRoute = this.$router.match({ name: 'Index' })
```

```js:after
const matchedRoute = this.$router.resolve({ name: 'Index' })
```

## router.currentRoute

`value`に潜る必要があります。

```js:before
const currentRoute = router.currentRoute
```

```js:after
const currentRoute = router.currentRoute.value
```

## $router.push()が$routeを即時変更しない

`$router.push()`は同期的に実行されなくなったようです。

```js:before
this.$router.push({ query: { test: 123 } }).then(() => {
  console.log(this.$route.query.test) // -> 123
})
console.log(this.$route.query.test) // -> 123
```

```js:after
this.$router.push({ query: { test: 123 } }).then(() => {
  console.log(this.$route.query.test) // -> 123
})
console.log(this.$route.query.test) // -> undefined
```

即時実行される前提で実装されたコードを洗い出して修正しなければいけないため絶望しています…。
Promise返すことすら知りませんでした。

## paramsやqueryが必ず文字列で渡される

元々以下のように渡すと、遷移後の画面でそのままの値が取得できていました。

```js:before
// 遷移元
this.$router.push({ ..., params: { id: 123 } })
// 遷移後
typeof this.$route.params.id // -> 123
```

それが、必ず文字列に変換されて届くようになったようです。

```js:after
// 遷移元
this.$router.push({ ..., params: { id: 123 } })
// 遷移後
typeof this.$route.params.id // -> "123"
```

beforeの頃でも、リロードした際にはURL文字列である`/xxx/123`からパースされるので、そのときに結局Stringになります。
つまりリロード前後で型が変わってしまうという現象が置きていましたが、afterからは最初から強制的に文字列になるため、リロード前後で変わりません。

また、paramsにはroutesで定義していないキーも渡せるため、「遷移先のページに秘密のデータを渡すぜ！」なノリでこんな風にObjectだとかを渡していた場合注意が必要です。

```js
const secretData = { abc: 123 }
this.$router.push({ ..., params: { secretData } })
// 遷移後
this.$route.secretData // -> "[object Object]"
```

全て文字列になります。

## beforeRouteLeave

そもそもこの処理はどうなんだ、というのは置いておいて、
Vue 3では、`beforeRouteLeave`内で`next()`を実行せず、別の場所であとから実行しようとすると怒られて動きません。

```js
beforeRouteLeave (_to, _from, next) {
  if (...) {
    this.solveLater = next
    return
  }
  next()
}
```

`next()`を実行しないケースがあるなら、nextを引数にとってはいけないみたいです。
その際、代わりに`return Boolean`で遷移の許可／拒否を制御できるようになったぽいです（doc未確認）。

```js
beforeRouteLeave (to, _from) {
  if (...) {
    this.solveLater = () => this.$router.push(to)
    return false
  }
  return true
}
```

# scrollBehavior

`scrollBehavior` で返却すべきオブジェクトについて、x,yというプロパティがleft,topに変わります。

```js:before
scrollBehavior (to, from, savedPosition) {
  return { x: 0, y: 0 }
}
```

```js:after
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()`が必要となる場合がありました。

```js:before
// これだと変更が検知されないので、
// this.awesomeObject.newKey = newValue
// こう↓
Vue.set(this.awesomeObject, 'newKey', newValue)
```

Vue3では、新しいプロパティが検知されるようになりました。

なので、単に使うのを辞めるだけです。

```js:after
this.awesomeObject.newKey = newValue
```

`Vue.delete`も同様です。

# Vue.observable

`Vue.observable`はありません。

今回はComposition API使わないと言ったものの、`Vue.observable`頼りだった部分の書き換えに必要でした。

```js:before
import Vue from 'vue'
class AwesomeClass {
  count = 0
  position = { x: 0, y: 0 }
  constructor () {
    Vue.observable(this) // インスタンス丸ごとリアクティブにしちゃえ
  }
}
```

```js:after
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
  }
}
```

# コンポーネントの変更点への対処

公式の[変更点](https://v3.vuejs.org/guide/migration/introduction.html#template-directives)を見ると、影響が出そうな箇所がありそうです。

## destroyedの廃止

`unmounted` になりました。

```js:before
export default {
  destroyed () {}
}
```

```js:after
export default {
  unmounted () {}
}
```

併せて、`beforeDestroy`も`beforeUnmount`になりました。

あと、これは多分ユーザーが使うことを想定したプロパティじゃないのでドキュメントでは確認できませんが、
コンポーネントインスタンスが持つ`this._isDestroyed`というプロパティは`this._.isUnmounted`になりました。
他のメタ的な値も`this._`にまとめられていると思います。

## v-modelの仕様変更

`v-model`エイリアスでprops/emitを貼る場合、
propsに渡される名前が`value`から`modelValue`に変更になり、
emitすべきイベント名も`input`から`update:modelValue`に変更になります。

詳細は[こちら](https://v3.vuejs.org/guide/migration/v-model.html#overview)

## dataオプションの挙動の変化

以下のソースを見てもらうのが分かりやすいのですが、data内に定義されたObjectは元のObjectとは別物になってしまいます。

```js
const MASTER_DATA = {}
export default {
  data () {
    return {
      MASTER_DATA
    }
  },
  computed: {
    sameAsOriginal () {
      // Vue2 returns true
      // Vue3 returns false
      return MASTER_DATA === this.MASTER_DATA
    }
  }
}
```

個人的にはこれはかなりしんどい変更です。

```js
import { toRaw } from 'vue'
...
    sameAsOriginal () {
      return MASTER_DATA === toRaw(this.MASTER_DATA) // -> true
    }
```

dataオプションを通すと、Proxyオブジェクトとなるため、オリジナルの値には`toRaw`を使ってアクセスする必要があるようです。

## transitionのクラス名

transition時に付与されるクラス名が一部変更になりました。

```scss:before
// フェードインの開始＆フェードアウトの終了
.fadeInOut-enter,
.fadeInOut-leave-to {
  opacity: 0;
}
// フェードインの終了＆フェードアウトの開始
.fadeInOut-enter-to,
.fadeInOut-leave {
  opacity: 1;
}
```

`-from` がつきます。

```scss:after
// フェードインの開始＆フェードアウトの終了
.fadeInOut-enter-from,
.fadeInOut-leave-to {
  opacity: 0;
}
// フェードインの終了＆フェードアウトの開始
.fadeInOut-enter-to,
.fadeInOut-leave-from {
  opacity: 1;
}
```

詳細は[こちら](https://v3.vuejs.org/guide/migration/transition.html#overview)

## transition自体にv-ifつけたときの挙動

これは終了のアニメが動かなくなります。

```vue
<transition v-if="..">
  <div />
</transition>
```

このように書かなければいけません。

```vue
<transition>
  <div v-if=".." />
</transition>
```

前者が動いていたのは意図していたものではなかったようですが、それに依存して`<transition>`をルートに持つコンポーネントをたくさん作っていた場合、苦労すると思います。


## watchでArrayを監視

watchでArrayを監視した際の挙動が変わりました。

Vue 3では、監視対象のArrayの内容が変わっても検知されません。
Arrayそのものが置き換わったときにのみ検知されます。

```js
watch: {
  arr: {
    handler (newVal, oldVal) {
      console.log(newVal, oldVal)
    },
    deep: true
  }
}
```
`deep`というオプションを`true`にすることで、既存と同じ動作にすることができるようです。

## watchで$routeを監視

`watch`で`$route`やそのqueryやparamsを監視している場合、注意が必要です。

「記事id」をURLに持つ「記事詳細画面」で、記事idの変更を監視していたとします。

```js
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`となります。

```vue:App.vue
<template>
  <my-component />
</template>

<style>
/* Global style */
h1 { color: red; }
</style>
```

```vue:MyComponent.vue
<template>
  <h1 />
</template>

<style>
h1 { color: blue; }
</style>
```

Vue 3ではCSSの結合順が異なるようで、App.vueに記述されたスタイルは一番下になります。
App.vueにグローバルなスタイルを置いている場合は注意です。

多くの場合、子コンポーネントはscopedでスタイルをあてたり、class名にスタイルをあてたりするので、
グローバルなスタイルが一番下に定義されようが優先順位で負けることは少なく、一見気づきづらいです。

ドキュメントからもこれに関する記載を見つけられませんでした。

結構困ったんですが、後に定義されるほど上に来るということを逆手にとって、
↓こういうグローバルCSSを書くためだけのコンポーネントを作ることでとりあえずは一番上に定義できました。

```vue:App.vue
<template>
  <my-component />
  <global-style /><!-- App.vueで一番下に置けば一番上の行に定義される -->
</template>
```

```vue:GlobalStyle.vue
<style>
h1 { color: red; }
</style>
```

# おわりに

おわりです