【Vue 3】Vue 2との違いについて
はじめに
NuxtJSをメインに使用していることもあって、執筆時点でも今だにVue 2を使用している筆者ですが、 Nuxt3がRC版になったということでようやく重い腰を上げてVue 3を触っていきたいと思います。
この記事では、Vue 2からVue 3への変更点について、いくつかピックアップして紹介していきます。
導入
Vue 3の導入(インストール)については公式ドキュメントを参考にしてください。
導入方法自体は変わらないと思います。
CLIについては、Vue 2と同様に公式のVue CLIが提供されていますが、 Vue 3からはViteというツールも使用できます。
Viteについては割愛しますが、Vue CLIよりもビルドがかなり速いため、快適に開発を進めたい方にはおすすめです。 筆者も簡単にしか試したことがないので、時間がありそうなら記事にまとめたいと思います。
Composition API
Vue 2からの最も大きな変更点といえば、Composition APIが導入されたことでしょう。 これについては、内容として重くなりそうなので別の記事でまとめています。
Fragments
Vue 2では、コンポーネントのルート要素が1つである必要がありました。
<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>
Vue 3からは、複数の要素をルート要素に指定することができます。 余分な階層が減らせるので、地味に嬉しい変更点です。
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>
グローバルAPI
Vueインスタンスの生成方法がグローバルAPIを使用する方法に変わります。 CDNを用いる場合は、以下の違いになります。
new Vue({
el: '#app',
...
})
Vue.createApp({
...
}).mount('#app')
その他、Vue 2ではグローバルAPIとして定義されていたものが、 Vue 3からはインスタンスAPIとして定義されています。 詳細は以下公式ドキュメントを参考にしてください。
dataの宣言
CDNを用いた場合、Vue 2ではdata
をオブジェクトとして宣言できていましたが、
Vue 3からは関数での宣言に変わります。
普段Vueファイルを使用している場合は、あまり影響はなさそうです。
Vue.createApp({
data: () => ({
...
})
}).mount('#app')
ライフサイクル
Vue 3から一部ライフサイクルの名称が変更されます。 役割自体は変わりません。
beforeDestroy
→ beforeUnmount
destroy
→ unmounted
トランジッションクラス
<transition>
で定義されるクラス名が一部変更となります。
v-enter
→ v-enter-from
v-leave
→ v-leave-from
SCSSを使用する場合はこちらのほうが見た目(可読性)的にも助かります。
fillter
Vue 3からはfillter
が利用できなくなります。
実際のところ、computed
でことたりるのであまり問題はないかと思います。
emits
Vue 2では、以下のように$emit
を使用してカスタムイベントを設定できました。
<template>
<button @click="$emit('myclick')">Click Me!!</button>
</template>
<template>
<my-component @myclick="onClick" />
</template>
<script>
export default {
methods: {
onClick() {
//...
}
}
}
</script>
Vue 3からは、上記の記述だけでは不十分となり、新たに追加されたemits
プロパティに使用するカスタムイベントをすべて指定する必要があります。
<template>
<button @click="$emit('myclick')">Click Me!!</button>
</template>
<script>
export default {
emits: ['myclick']
}
</script>
また$emit
で呼び出した際に、そのイベントが有効であるかを検証することができます。
以下は、引数が文字列であることを検証します。
<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
イベントで値の更新を行なっていました。
<template>
<button @click="onClick">change</button>
</template>
<script>
export default {
props: {
value: String
},
methods: {
onClick() {
this.$emit('input', 'changed!!')
}
}
}
</script>
<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
の指定が必要になる点にも注意が必要です。
呼び出し元の変更はありません。
<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:[プロパティ名]
を使用しました。
<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>
<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
を追加すること以外変更はありません。
<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>
<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
: 値を数値に変換
<template>
<my-component v-model.trim="text" />
</template>
<script>
export default {
data: () => ({
text: ''
})
}
</script>
Vue 3では、これに加えてカスタム修飾子を定義することができます。
正確には、どのような修飾子が指定されているかをmodelModifiers
プロパティとして受け取ることができます。
<script>
export default {
props: {
modelValue: String,
modelModifiers: { // ← これで受け取る
default: () => {}
}
}
}
</script>
<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
が指定された場合は文字を反転して値を更新しています。
<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>
の使用例として、以下のように簡単なモーダルコンポーネントを作成し、使用してみます。
<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>
<template>
<button @click="show = true">表示</button>
<my-modal :show="show">モーダルのコンテンツ</my-modal>
</template>
<script>
export default {
data: () => ({
show: false
})
}
</script>
<teleport>
タグを使用しなかった場合と使用した場合とで、要素の展開のされ方が以下のように変わります。
<body>
<div id="app">
<button>表示</button>
<div class="my-modal">
<div class="my-modal__contents">
モーダルのコンテンツ
</div>
</div>
</div>
</body>
<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
のようにコンポーネント内のdata
やmethods
などはそのまま利用できます。
Scoped CSSも有効です。
<teleport>
を使用する際の注意点としてもう1点、<teleport>
の要素が展開される前に移動先の要素が展開されている必要があります。
以下の例をみてください。
<template>
<p>コンテンツ</p>
<teleport to="#contents">
<p>移動元のコンテンツ</p>
</teleport>
</template>
<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>
を使用しなかった場合と同じ状態になるだけです。