vue3.svg

【Vue 3】Vue 2との違いについて

Vue 3

はじめに

NuxtJSをメインに使用していることもあって、執筆時点でも今だにVue 2を使用している筆者ですが、 Nuxt3がRC版になったということでようやく重い腰を上げてVue 3を触っていきたいと思います。

この記事では、Vue 2からVue 3への変更点について、いくつかピックアップして紹介していきます。

参考:はじめに | Vue.js

導入

Vue 3の導入(インストール)については公式ドキュメントを参考にしてください。

参考:インストール | Vue.js

導入方法自体は変わらないと思います。

CLIについては、Vue 2と同様に公式のVue CLIが提供されていますが、 Vue 3からはViteというツールも使用できます。

Viteについては割愛しますが、Vue CLIよりもビルドがかなり速いため、快適に開発を進めたい方にはおすすめです。 筆者も簡単にしか試したことがないので、時間がありそうなら記事にまとめたいと思います。

Composition API

Vue 2からの最も大きな変更点といえば、Composition APIが導入されたことでしょう。 これについては、内容として重くなりそうなので別の記事でまとめています。

Fragments

Vue 2では、コンポーネントのルート要素が1つである必要がありました。

Vue 2
<template>
  <div>
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  </div>
</template>

Vue 3からは、複数の要素をルート要素に指定することができます。 余分な階層が減らせるので、地味に嬉しい変更点です。

Vue 3
<template>
  <header>...</header>
  <main>...</main>
  <footer>...</footer>
</template>

グローバルAPI

Vueインスタンスの生成方法がグローバルAPIを使用する方法に変わります。 CDNを用いる場合は、以下の違いになります。

Vue 2
new Vue({
  el: '#app',
  ...
})
Vue 3
Vue.createApp({
  ...
}).mount('#app')

その他、Vue 2ではグローバルAPIとして定義されていたものが、 Vue 3からはインスタンスAPIとして定義されています。 詳細は以下公式ドキュメントを参考にしてください。

参考:グローバルAPI | Vue.js

dataの宣言

CDNを用いた場合、Vue 2ではdataをオブジェクトとして宣言できていましたが、 Vue 3からは関数での宣言に変わります。

普段Vueファイルを使用している場合は、あまり影響はなさそうです。

Vue 3
Vue.createApp({
  data: () => ({
    ...
  })
}).mount('#app')

ライフサイクル

Vue 3から一部ライフサイクルの名称が変更されます。 役割自体は変わりません。

beforeDestroybeforeUnmount
destroyunmounted

トランジッションクラス

<transition>で定義されるクラス名が一部変更となります。

v-enterv-enter-from
v-leavev-leave-from

SCSSを使用する場合はこちらのほうが見た目(可読性)的にも助かります。

fillter

Vue 3からはfillterが利用できなくなります。 実際のところ、computedでことたりるのであまり問題はないかと思います。

emits

Vue 2では、以下のように$emitを使用してカスタムイベントを設定できました。

MyComponent.vue | Vue 2
<template>
  <button @click="$emit('myclick')">Click Me!!</button>
</template>
Page.vue
<template>
  <my-component @myclick="onClick" />
</template>

<script>
export default {
  methods: {
    onClick() {
      //...
    }
  }
}
</script>

Vue 3からは、上記の記述だけでは不十分となり、新たに追加されたemitsプロパティに使用するカスタムイベントをすべて指定する必要があります。

MyComponent.vue | Vue 3
<template>
  <button @click="$emit('myclick')">Click Me!!</button>
</template>

<script>
export default {
  emits: ['myclick']
}
</script>

また$emitで呼び出した際に、そのイベントが有効であるかを検証することができます。 以下は、引数が文字列であることを検証します。

Vue 3
<script>
export default {
  emits: {
    myEvent: (text) => {
      return typeof text === 'string'
    }
  },
  methods: {
    doMyEvent1() {
      this.$emit('myEvent', 'TEST')  //これはOK
    },
    doMyEvent2() {
      this.$emit('myEvent', 111) //これはNG(警告が表示される)
    }
  }
}
</script>

v-model(双方向バインディング)

単体

Vue 2では、v-modelの値はvalueプロパティに紐付き、inputイベントで値の更新を行なっていました。

MyComponent.vue | Vue 2
<template>
  <button @click="onClick">change</button>
</template>

<script>
export default {
  props: {
    value: String
  },
  methods: {
    onClick() {
      this.$emit('input', 'changed!!')
    }
  }
}
</script>
Page.vue
<template>
  <div>
    {{ text }}
    <my-component v-model="text" />
  </div>
</template>

<script>
export default {
  data: () => ({
    text: 'no change'
  })
}
</script>

Vue 3からはvalueではなくmodelValueに紐づくようになり、値の更新はinputではなくupdate:modelValueを使用します。 また上述したように、emitsの指定が必要になる点にも注意が必要です。

呼び出し元の変更はありません。

MyComponent.vue | Vue 3
<template>
  <button @click="onClick">change</button>
</template>

<script>
export default {
  props: {
    modelValue: String   // ← valueから変更
  },
  emits: ['update:modelValue'],   // ← これを追加
  methods: {
    onClick() {
      this.$emit('update:modelValue', 'changed!!')   // ← inputから変更
    }
  }
}
</script>

複数

Vue 2では、複数の値を双方向バインディングする場合、v-modelではなくsync修飾子を使用しました。 値の更新は、update:[プロパティ名]を使用しました。

MyComponent.vue | Vue 2
<template>
  <div>
    <button @click="onClick1">change text1</button>
    <button @click="onClick2">change text2</button>
  </div>
</template>

<script>
export default {
  props: {
    text1: String,
    text2: String
  },
  methods: {
    onClick1() {
      this.$emit('update:text1', 'changed text1!!')
    },
    onClick2() {
      this.$emit('update:text2', 'changed text2!!')
    }
  }
}
</script>
Page.vue | Vue 2
<template>
  <div>
    <p>{{ text1 }}</p>
    <p>{{ text2 }}</p>
    <my-component :text1.sync="text1" :text2.sync="text2" />
  </div>
</template>

<script>
export default {
  data: () => ({
    text1: 'no change',
    text2: 'no change'
  })
}
</script>

Vue 3では、sync修飾子が使用できなくなり、代わりにv-model:[プロパティ名]を使用します。 コンポーネント側の定義は、emitsを追加すること以外変更はありません。

MyComponent.vue | Vue 3
<template>
  <div>
    <button @click="onClick1">change text1</button>
    <button @click="onClick2">change text2</button>
  </div>
</template>

<script>
export default {
  props: {
    text1: String,
    text2: String
  },
  emits: ['update:text1', 'update:text2'],   // ← これの追加だけ
  methods: {
    onClick1() {
      this.$emit('update:text1', 'changed text1!!')
    },
    onClick2() {
      this.$emit('update:text2', 'changed text2!!')
    }
  }
}
</script>
Page.vue | Vue 3
<template>
  <div>
    <p>{{ text1 }}</p>
    <p>{{ text2 }}</p>
    <!-- v-modelに変更 -->
    <my-component v-model:text1="text1" v-model:text2="text2" />  
  </div>
</template>

<script>
export default {
  data: () => ({
    text1: 'no change',
    text2: 'no change'
  })
}
</script>

修飾子

v-modelには元々、.trim.numberのような修飾子がありました。

  • .trim: 値の両端の空白を除去
  • .number: 値を数値に変換
Page.vue | Vue 2
<template>
  <my-component v-model.trim="text" />
</template>

<script>
export default {
  data: () => ({
    text: ''
  })
}
</script>

Vue 3では、これに加えてカスタム修飾子を定義することができます。 正確には、どのような修飾子が指定されているかをmodelModifiersプロパティとして受け取ることができます。

MyComponent.vue | Vue 3
<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: {   // ← これで受け取る
      default: () => {}
    }
  }
}
</script>
Page.vue | Vue 3
<template>
  <p>{{ text }}</p>
  <my-component v-model.capitarize.reverse="text" />
</template>

<script>
export default {
  data: () => ({
    text: ''
  })
}
</script>

上記の場合、modelModifiersは以下のように指定された修飾子をキーに持つオブジェクトとなります。

modelModifiers = {
  'capitarize': true,
  'reverse': true
}

この値を元に、値を更新する際の処理を追加します。 下記では、capitarizeが指定された場合は文字の先頭を大文字に、reverseが指定された場合は文字を反転して値を更新しています。

MyComponent.vue | Vue 3
<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: { 
      default: () => {}
    }
  },
  emits: ['update:modelValue'],
  computed: {
    val: {
      get() {
        return this.modelValue
      },
      set(val) {
        let v = val
        //先頭を大文字にする
        if (this.modelModifiers.capitarize) {
          v = v.charAt(0).toUpperCase() + v.slice(1)
        }
        //文字を反転する
        if (this.modelModifiers.reverse) {
          v = v.reverse()
        }
        this.$emit('update:modelValue', v)
      }
    }
  }
}
</script>

v-model:textのようにプロパティを指定する場合は、textModifiersのように[プロパティ名] + Modifiersとして修飾子の指定情報を受け取ります。

Teleport

Teleportは、Vue 3からの新機能になります。 <teleport>タグに内包された要素を、指定した要素内に移動させることができます。

<teleport>の使用例として、以下のように簡単なモーダルコンポーネントを作成し、使用してみます。

MyModal.vue
<template>
  <teleport to="body">
    <div class="my-modal" v-if="show">
      <div class="my-modal__contents">
        <slot />
      </div>
    </div>
  </teleport>
</template>

<script>
export default {
  props: {
    show: {
      type: Boolean,
      default: false
    },
  },
}
</script>
App.vue
<template>
  <button @click="show = true">表示</button>
  <my-modal :show="show">モーダルのコンテンツ</my-modal>
</template>

<script>
export default {
  data: () => ({
    show: false
  })
}
</script>

<teleport>タグを使用しなかった場合と使用した場合とで、要素の展開のされ方が以下のように変わります。

teleportなし
<body>
  <div id="app">
    <button>表示</button>
    <div class="my-modal">
      <div class="my-modal__contents">
        モーダルのコンテンツ
      </div>
    </div>
  </div>
</body>
teleportあり
<body>
  <div id="app">
    <button>表示</button>
  </div>
  <div class="my-modal">
    <div class="my-modal__contents">
      モーダルのコンテンツ
    </div>
  </div>
</body>

このように<teleport>は、toで指定した要素内に要素を移動させることができます。 上記の場合はto="body"としているため、<body>内の末尾に要素が移動したことになります。

移動先の注意点として、移動先の要素は一意である必要があります。 <body>以外に、以下のようにidを設定した要素を移動先として指定できます。 移動先の要素が存在しない場合は、コンソールに警告が表示されます。

移動先
<div id="contents"></div>
移動元
<teleport to="#contents">
  <p>移動元のコンテンツ</p>
</teleport>
移動結果
<div id="contents">
  <p>移動元のコンテンツ</p>
</div>

<teleport>を使用すると要素を移動させることができますが、MyModalのようにコンポーネント内のdatamethodsなどはそのまま利用できます。 Scoped CSSも有効です。

<teleport>を使用する際の注意点としてもう1点、<teleport>の要素が展開される前に移動先の要素が展開されている必要があります。 以下の例をみてください。

移動元 | MyComponent.vue
<template>
  <p>コンテンツ</p>
  <teleport to="#contents">
    <p>移動元のコンテンツ</p>
  </teleport>
</template>
移動先 | App.vue
<template>
  <my-component />
  <div id="contents" />
</template>

これは<my-component /><div id="contents" />の順で展開されます。 つまり、<teleport>の動作時には移動先の要素が展開されておらず、移動先の要素なしの警告がコンソールに表示されてしまいます。

展開順は非常に重要であり、以下のように同じ移動先を指定した<teleport>が複数ある場合は、展開された順に移動します。

移動元
<teleport to="#contents">
  <p>移動元のコンテンツ1</p>
</teleport>

<teleport to="#contents">
  <p>移動元のコンテンツ2</p>
</teleport>
移動結果
<div id="contents">
  <p>移動元のコンテンツ1</p>
  <p>移動元のコンテンツ2</p>
</div>

最後に、<teleport>にはto以外にdisabledプロパティを持ちます。 disabled = trueに設定した場合、<teleport>内の要素は移動せずにそのまま展開されます。 つまり、<teleport>を使用しなかった場合と同じ状態になるだけです。