vue3.svg

【Vue 3】共通化について考える

Vue 3

はじめに

コンポーネントを作成している際に、「また同じ処理作ってるなー。」とか「似た処理作ってるなー。」とか感じることは多々あるように思います。 Vue 2 で主流の Options API でも共通化の方法はありましたが、Vue 3 で Composition API が導入されたことにより、新たに共通化の方法が増えています。

この記事では、コンポーネントの共通化についていくつか紹介していきたいと思います。

ミックスイン

ミックスインは、各 Vue コンポーネントに対して共通的なプロパティとして混在させることができます。 例えば、共通的に使用するデータやメソッド、mounted()などのライフサイクルで共通的に実行したい処理などを各コンポーネントに展開できます。

ミックスインは Vue 2 からある仕様で、基本的には Options API で使用します。 ミックスイン自体も Options API の形で定義します。

mixins.js
export default {
  data: () => ({
    num: 0,
  }),
  methods: {
    incNum() {
      this.num++
    },
  },
  mounted() {
    console.log('mixin mounted!!')
  }
}

ミックスインを使用する場合は、コンポーネントのmixinsプロパティで対象のミックスインを指定します。

Component.vue
<script>
import mixins from '../assets/mixins'

export default {
  mixins: [mixins],
  data: () => ({
    text: 'hello'
  }),
  mounted() {
    console.log('component mounted!!')
  }
}
</script>

実際コンポーネントとしては、以下のようにマージされます。

Component.vue(展開後)
<script>
export default {
  data: () => ({
    num: 0,
    text: 'hello'
  }),
  methods: {
    incNum() {
      this.num++
    },
  },
  mounted() {
    console.log('mixin mounted!!')
    console.log('component mounted!!')
  }
}
</script>

mounted()のようなライフサイクルでは、ミックスインの処理の後にコンポーネント側の処理が実行されます。 また同一名のデータやメソッドなどが存在する場合は、コンポーネント側のものが優先されます。

上記は個別の設定で使用しますが、すべてのコンポーネントでミックスインを適用したい場合は以下のようにします(グローバルミックスイン)。

main.js
import { createApp } from 'vue'
import App from './App.vue'
import mixins from './assets/mixins';

const app = createApp(App);
app.mixin(mixins);
app.mount('#app');

ミックスインの注意点は、ミックスインで定義しているプロパティは、各コンポーネントで個別のデータとして扱われるということです。 要は、ミックスインのデータは共有されているわけではないので、あるコンポーネントで変更したものが他のコンポーネントに反映されることはありません。 あくまで共通の仕様を定義しているだけで、共通のデータを定義しているのではないのです。

一見便利なように思えるミックスインですが、公式ドキュメントに記載されている通り、いくつか欠点があるため推奨されていません。 Vue 3 では代わりに Composition API を使いましょうというのが公式の見解です。

ミックスインの欠点

・ミックスインはコンフリクトしやすい

・どこからともなく現れたようなプロパティ

・再利用性は制限されている

Vuex

Vuex は、Vue.js のアプリケーションにおいて、共通的なデータとして状態を管理するためのライブラリです。 このデータは、すべてのコンポーネントで参照・更新をすることができます。

具体的に共通的なデータをstateとして扱い、mutationsactionsで更新を行います。

NuxtJS をベースにしていますが、過去に Vuex について記事を上げていますので詳細はこちらを参照してください。

Composition API

Composition API の基本については、過去の記事を参照してください。

Composition API では、<script>の処理を別のファイルに分割することができます。 以下は簡単な例になります。

user.js
import { reactive, onMounted } from 'vue'

export default function() {
  const user = ref({ id: 1, name: 'hoge' })
  const setUser = (v) => { Object.assign(user, v) }

  onMounted(() => { console.log('user.js mounted!!') })

  return { user, setUser }
}
Component.vue
<script setup>
import { ref, onMounted } from 'vue'
import user from '../assets/user.js'

const { user, setUser } = user()
const n = ref(0)

onMounted(() => { console.log('component mounted!!') })
</script>

共通部分は関数で定義し、必要なものをオブジェクトとしてreturnします。 データやメソッドなどは、Composition API のルールに基づいて定義します。

呼び出し側は、関数として呼び出し、必要なデータやメソッドなどを自らが指定して取得します。 上記ではしていませんが、関数に引数としてデータを渡すことができるので、より柔軟に共通化することができます(再利用性がある)。

ミックスインの欠点としてあげていた 3 つのことは、これらの内容からある程度解決できることがわかるかと思います。

またライフサイクルについてですが、これは記述順で実行されます。 上記だと、Component.vueuser()の後にonMountedとあるため、user.js mounted!!component mounted!!の順でコンソールに表示されます。 この記述の順を変えれば、実行順も変わります。 この点もミックスインとは異なります。

では次の例を見てください。

user.js
import { reactive, readonly } from 'vue'

const user = ref({ id: 1, name: 'hoge' })
const roUser = readonly(user)
const setUser = (v) => { Object.assign(user, v) }

export { roUser as user, setUser }
Component.vue
<script setup>
import { ref } from 'vue'
import { user, setUser } from '../assets/user.js'

const n = ref(0)

setUser({ id: 2, name: 'fuga' })
</script>

こちらは関数ではなく、データやメソッドそのものをモジュール化しています。 このように定義した場合、データやメソッドは読み込んだすべてのコンポーネントで共有されます。 イメージとしては Vuex のような扱いになります。

少しわかりにくいかもしれませんが、関数としてモジュール化した場合は各コンポーネントに展開され、 そのものをモジュール化した場合は各コンポーネントから参照するといった感じです。

Composition API をうまく使えば、ミックスインや Vuex は不要になるかもしれません。

globalProperties

すべてのコンポーネントでアクセス可能なプロパティを、Vue インスタンスのconfig.globalPropertiesとして定義することができます。 定義するプロパティの先頭には、$を付けることが推奨されています。 主にグローバルに扱いたい定数や、共通の処理(メソッド)を定義する目的で使用します。

Vue 2 の経験がある方はVue.prototypeと同じものなので、なじみがあるかと思います。

main.js
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.config.globalProperties.$hello = 'Hello World!!'
app.mount('#app')

globalPropertiesは、Options API ではthis.[プロパティ名]とすることで参照することができます。

Options API
export default {
  methods: {
    hello() {
      console.log(this.$hello)  // Hello World!!
    }
  }
}

Composition API では、Options API のようにthisで Vue インスタンスを参照することができず、getCurrentInstanceで参照する必要があります。

Composition API
<script setup>
import { getCurrentInstance } from 'vue'

const app = getCurrentInstance()

const hello = () => {
  console.log(app.appContext.config.globalProperties.$hello)
}
</script>

上記の通り、Composition API では参照するのが少し面倒です。 そこで、次に説明する Provide/Inject で代替します。

Provide / Inject

通常親から子のコンポーネントへデータを渡すには、propsを使用します。 孫のコンポーネントへ渡そうと思うと、子を経由する必要があります。 このように階層が深くなるにつれてデータの受け渡しが困難になってきます。

そこで使用するのが、provideinjectです。 親がprovideで定義したデータを、子孫がinjectによって参照することができます。

少しわかりにくいと思いますので、具体例を見てみましょう。 以下は Composition API での例です。

Parent.vue
<script setup>
import { provide } from 'vue'
import Child from './components/Child'

provide('num', 1)  // データを設定する
</script>
Child.vue
<script setup>
import GrandChild from './components/GrandChild'
</script>
GrandChild.vue
<script setup>
import { inject } from 'vue'

const num = inject('num')  // データを参照する「1」
</script>

親(Parent)は、provideにより受け渡したいデータをキーとセットで設定します。 上記では'num'というキーで1を渡しています。 孫(GrandChild)は、injectでキーを指定することで、対応する値を受け取ることができます。 今回の場合、1が受け取れます。

このように Provide / Inject によって、親からその子孫の間でデータを共有することができます。

上記の例ではシンプルなデータを渡しましたが、refreactiveで生成したデータの受け渡しも可能です。 このとき、子孫はリアクティブなデータとして受け取ることが可能です。 つまり、親、子孫が互いの変更を検知できます。

Parent.vue
<script setup>
import { ref, provide } from 'vue'
import Child from './components/Child'

const num = ref(1)
provide('num', num)
</script>
GrandChild.vue
<script setup>
import { inject } from 'vue'

const num = inject('num')
num.value = 2
</script>

子孫にデータを変更させたくない場合は、readonlyを使用しましょう。

provide('num', readonly(num))

Options API でも Provide / Inject は利用可能ですが、ここでは割愛します。

参照:https://v3.ja.vuejs.org/guide/composition-api-provide-inject.html