kurosame’s diary

フロントエンドが少しできます

Vue3の事前調査まとめ

概要

来月 Vue3 がリリース予定です!

先日ステータスが RC となりました
https://github.com/vuejs/rfcs/issues/189

ただし、Vue3 にバージョンアップするのは、Vuex や Vue Router などの主要なエコシステムが Vue3 対応を正式にリリースしてからになると思います

Vue3 のコードはこちらに公開されています
https://github.com/vuejs/vue-next

中身のコードを見てみると、全部 TypeScript で書かれているのが分かります
今回コンパイラーから DOM、レンダリングコードまですべてゼロから実装し直したらしいです

以下は VueCLI 用ですが、vue-next を試すプラグインがありました
これを使うことで Vue およびそのエコシステムや vue-loader などの開発環境周りもまとめて Vue3 仕様にバージョンアップしてくれるものと思われます
https://github.com/vuejs/vue-cli-plugin-vue-next

残念ながら、私が今携わっているシステムでは CLI を使ってないので、バージョンアップしてどのくらいの影響が出るのかの確認は来月以降に見送ります

Vue3 移行時に破壊的変更を伴うものは、Vue2 で事前に対応できるように配慮されています
(たとえば、Vue3 では旧slot構文が削除されるので、事前に Vue2.6 からv-slot構文が追加されています)

よって、Vue2 のマイナーバージョンアップに追従して、事前に破壊的変更を伴うものを対応していれば、Vue3 への移行はかなりスムーズになると思います

今回は以下を調査しました

  • 新機能
  • 既存機能の変更
  • Composition API を実際に使ってみる

※ネットから適当に拾って書いてます

ちなみに Vue3 ではnew Vueしてるところから書き方が変わってたので、バージョンアップしても秒で落ちます

// Vue2
new Vue({
  render: (h): VNode => h(App)
}).$mount('#app')

// Vue3
createApp(App).mount('#app')

新機能

Teleport

あるコンポーネント内の要素を別のコンポーネントの DOM にマウントできる

React の Portal 機能と同等です

<Teleport to="#modals">
  <div>A</div>
</Teleport>
<Teleport to="#modals">
  <div>B</div>
</Teleport>

<!-- 上記のTeleport内の要素がtoで指定したidの要素の中にマウントされる -->
<div id="modals">
  <div>A</div>
  <div>B</div>
</div>

親子関係のコンポーネントであれば、$emitを書かなくて済む
親子孫というネストしたコンポーネントで孫から親に$emitする際、孫 -> 子子 -> 親のそれぞれで$emitを書く必要がある
そして、子は孫から親に中継するだけの自分自身には関係ない$emitとなる
この問題を解決するのに Vuex を使う手もあるが、Vuex に connect するコンポーネントは限定すべきである

上記を解決するのに Teleport 機能は利用できるかなと思います

Suspense

以下のコードだと ComponentA がロードされるまでの間、<div>Loading...</div>を出力し、ロード後 ComponentA を表示してくれる

React にも同機能があります

<Suspense>
  <template #default>
    <ComponentA />
  </template>
  <template #fallback>
    <div>Loading...</div>
  </template>
</Suspense>

使い所は多そう

既存機能の変更

v-model が複数書ける

<example v-model:name="name" v-model:value="value" />

コンポーネントのルート要素を複数にできる

React でいう Fragment 構文に相当します

Vue3 では以下はエラーになりません

<template>
  <div>AAA</div>
  <div>BBB</div>
</template>

Vue2 だと上記はエラーになるので、以下のように書く必要がありました

<template>
  <div>
    <div>AAA</div>
    <div>BBB</div>
  </div>
</template>

その他

  • style scopedの改善
    • ::v-deep,::v-slotted,::v-globalの追加
    • >>>/deep/などの書き方は将来的に削除される
    • ただし、古い書き方をコンパイラーで警告するのみでしばらくは互換性を保つっぽい
    • 参考
  • slot構文は廃止、v-slot構文へ移行する必要あり
  • |演算子を使ったフィルター機能は Vue3 で削除
  • $on,$off,$onceは Vue3 で削除

Composition API

この機能が 1 番の目玉だと(個人的に)思ってます
Vue2 でも@vue/composition-apiをインストールすれば、Composition API が使えます

import VueCompositionAPI from '@vue/composition-api'

Vue.use(VueCompositionAPI)

以下のようなcomponents/ExampleTable.vueを Composition API で書き直してみます

import Vue from 'vue'
import ExampleChart from '@/components/ExampleChart.vue'

export default Vue.extend({
  name: 'ExampleTable',
  components: { ExampleChart },
  data(): {
    pagination: { sortBy: string; descending: boolean }
  } {
    return {
      pagination: { sortBy: 'id', descending: true }
    }
  },
  computed: {
    items(): {}[] {
      return this.$store.getters.items || []
    },
    headers(): { text: string; value: string }[] {
      return [
        { text: 'ID', value: 'id' },
        { text: '名前', value: 'name' }
      ]
    }
  },
  methods: {
    getTableData() {
      this.$store.dispatch('GET_TABLE_DATA')
    }
  }
})

↓↓↓↓↓

import {
  defineComponent,
  reactive,
  computed,
  SetupContext
} from '@vue/composition-api'
import ExampleChart from '@/components/ExampleChart.vue'

export default defineComponent({
  name: 'ExampleTable',
  components: { ExampleChart },
  setup(_, ctx: SetupContext) {
    const pagination = reactive({
      sortBy: 'id',
      descending: true
    })
    const headers = computed(() => [
      { text: 'ID', value: 'id' },
      { text: '名前', value: 'name' }
    ])
    const items = computed(() => ctx.root.$store.getters.items || [])

    function getTableData() {
      ctx.root.$store.dispatch('GET_TABLE_DATA')
    }

    return { pagination, headers, items, getTableData }
  }
})

上記は単純に Composition API 仕様に書き換えただけです

ポイント

  • とりあえず defineComponent に全部渡すように書くことで、defineComponent の型定義のおかげで型推論が効くようになっている
    • VS Code で不自由なく実装できた
  • setup 関数に色々記載する
    • HTML 側で使う場合は return する
  • computed<string[]>のようにジェネリクスで型書けるのよい
    • でも書かなくても型推論が効いてる
  • this(Vue コンテキスト)を書く必要がなくなった
    • 今までのthisに相当する部分は setup 関数の第 2 引数から取れる
  • vue-loader などビルド環境周りをバージョンアップしてないけど、動いた
  • vue-devtools でちゃんと中身見れた

設計上のポイント

Composition API の書き方にする際はコンポーネントの実装を再設計する必要があると考えています

テーブルコンポーネントを例にします

Vue2 の書き方だと以下のように最上位がdatacomputedmethodsとなっているので、その中にテーブルのヘッダー、中身、フッターの処理を書くことになります
規模の大きいテーブルチャートになるとヘッダーを変えたいだけなのに、テーブルの中身やフッターの処理まで目を通すことになり、メンテナンス性に優れているとは言えません

export default Vue.extend({
  name: 'ExampleTable',
  components: { ExampleChart },
  data(): {
    header: // テーブルヘッダーの処理
    items: // テーブル内のデータの処理
    footer: // テーブルフッターの処理
  } {
    return {
      header: // テーブルヘッダーの処理
      items: // テーブル内のデータの処理
      footer: // テーブルフッターの処理
    }
  },
  computed: {
    // テーブル内のデータの処理
    itemsData(): {}[] {
      return this.$store.getters.tableData || []
    },
    // テーブルヘッダーの処理
    headers(): { text: string; value: string }[] {
      return [
        { text: 'ID', value: 'id' },
        { text: '名前', value: 'name' }
      ]
    }
  },
  methods: {
    // テーブル内のデータの処理
    function getTableData() {
      ctx.root.$store.dispatch('GET_TABLE_DATA')
    }
  }
})

Composition API だと以下のようになります
最上位が「処理の内容」です

明らかに Composition API で作られたコンポーネントの方が良い設計だというのは伝わるかなと思います
そして、この書き方だとコードの分離や再利用も容易に行なえます

たとえば、テーブルヘッダーの処理はどのテーブルも同様の処理をしているはずなので、別モジュールとして実装し、それを各テーブルコンポーネントが import して使うなど可能になります

export default defineComponent({
  setup(_, ctx: SetupContext) {
    // テーブルヘッダーの処理
    const headers = computed(() => [...])

    // テーブル内のデータの処理
    const pagination = reactive(...)
    const itemsData = computed(...)

    function getTableData() {...}

    //  テーブルフッターの処理
    ...

    // HTMLで使うやつだけreturnすればよい
    return { pagination, headers }
  }
})

処理を別モジュールで実装してもよいという書き方をしましたが
それはつまり、単一のコンポーネントではなく、システム全体のコンポーネント設計力が求められるということかなと思います

その他

こうゆうバージョンアップではあるあるですが、高速化・ファイルサイズの軽量化・メンテナンス性の向上も期待されるとあります

  • 高速化

  • ファイルサイズの軽量化

    • Vue の内部的に不要なパッケージの削除
    • Tree Shaking
      • これは元々採用している機能
      • export されてるが import されていない関数をバンドルファイルから削除するやつ
  • メンテナンス性の向上

    • 全面的な TypeScript の採用

まとめ

Vue は React にかなり影響を受けているので、React の機能を Vue でも使えるようにするというパターンが今回もそして今後も多いと思います

Composition API も Vue の独自機能のように見えますが、React Hooks にかなり影響を受けています(Function-based Component API

最近以下のような関数型プログラミングをサポートする機能を JS に導入する提案がされているのを見ます

Composition API や React Hooks も関数型プログラミングをサポートする機能と言えます
これらを採用することは同時に関数型プログラミングパラダイムシフトすることを求められます

Composition API や React Hooks をプロジェクトで採用する際は、このことも踏まえてチームで話し合う必要があるでしょう

React Hooks が登場した時と同じですが、Composition API を採用すると、以下の機能は今後書く機会が減っていくのかなと思います

  • Class Component
  • Mixins
  • Higher-order Component

また、Vue2 の書き方が今後使えなくなるということは(今の所)ないです

  • 段階的に Composition API に移行する
  • 新しく追加したコンポーネントのみ Composition API で書く
  • 今のシステムでは採用しない

など柔軟に移行計画が立てられます

Vue2 の書き方から、単純に Composition API に移行するだけであれば、比較的楽に移行できそうです

ただし、前述したように設計方針が大きく変わるので、むやみに移行するのは避けたいです
加えて、Composition API はフロントエンドの実装方針を大きく変えるものなので、段階的に移行する場合でもチームでコンセンサスをとって慎重に進めたほうがよいかなと思います

簡単ではありますが、雑多に調査結果をまとめました
詳細は Vue3 のリリース後に使いながら、知識を深めていこうと思います