こんにちは!
わたしたちは数多くのプロジェクトに Vue3 を導入してきましたが、保守性・パフォーマンスともに本当に魅力的なフレームワークだなといつも感じています。特にNuxt3が登場してから、ますます幅広い利用が可能なフレームワークとなりました。
本記事では、今流行りの Vue3 の注目機能& Vue2.x との違いについて、実際の経験談を交え詳しく解説いたします。導入を悩んでいる方は、ぜひ本記事を参考にしてください。
Nuxt2でVue3対応する場合は、別途モジュールをインストールしましょう。
残念ながら、Issue でも議論されている通り、2021年7月現在非対応の様子です。ただし、CompositionAPI(後述)は、別途モジュールを設定することで実現可能です。
世界中のユーザーから要望が上がってますので、近い将来対応される可能性が非常に高いです、NuxtJS ユーザーの方は、まずは手軽に(というよりも移行しやすいように)Composition API の導入から行ってみてはいかがでしょうか。
本記事では、私たち実装者の目線で重大な変更にのみ絞り解説したいと思います。
※より詳細な変更点については公式サイトを確認してください。
Vue インスタンス自体に大きな更新が入りました。以下の表に Vue インスタンスの更新内容をまとめています。
Vue 2.x | Vue 3.x |
Vue.config | app.config |
Vue.config.productionTip | 削除(参照) |
Vue.config.ignoredElements | app.config.compilerOptions.isCustomElement(参照) |
Vue.component | app.component |
Vue.directive | app.directive |
Vue.mixin | app.mixin |
Vue.use | app.use(参照) |
Vue.prototype | app.config.globalProperties(参照) |
Vue.extend | 削除(参照) |
Vue2 ユーザーにとって馴染み深いシンタックスが削除となります。CompositionAPI の標準化(CompositionAPI については後述)により、defineComponent
を使用する形となります。
Vue 3 では、継承やミックスインよりも、CompositionAPI を介したコンポジションを使用することが強く推奨されます。何らかの理由でコンポーネントの継承が必要な場合は、代わりに extends オプションを使用できます。
継承や Mixin を多用しているユーザーは注意が必要です。
以下の通り、更新が入りました。
v-model を渡すだけで、親の Component と双方向性するための Props 及び Emit が暗黙的にバインドされます。コード量が減りエレガントですね。
<ChildComponent v-model:title="pageTitle" />
<!-- 上記は以下の略-->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
Vue2 では、v-if
/ v-else
/v-else-if
を使用する際、key 属性指定が推奨されてましたが、Vue3 からは不要となりました。
<!-- Vue 2.x -->
<div v-if="condition" key="yes">Yes</div>
<div v-else key="no">No</div>
<!-- Vue 3.x -->
<div v-if="condition">Yes</div>
<div v-else>No</div>
加えて、<template v-for>
構文を使用する際、親に key を指定することで、子供に対する key 属性も不要となりました。よりシンプルなシンタックスとなりました。
<!-- Vue 2.x -->
<template v-for="item in list">
<div :key="'heading-' + item.id">...</div>
<span :key="'content-' + item.id">...</span>
</template>
<!-- Vue 3.x -->
<template v-for="item in list" :key="item.id">
<div>...</div>
<span>...</span>
</template>
Vue2 では、v-if
とv-for
が同じ要素に設定している場合、v-for
が優先されてきましたが、Vue3 からはv-if
が優先されます。
Vue3 では、v-bind
を使用し同じ属性値を設定した場合、属性の順番で優先順位が変わります。以下の例の通り、後に指定したv-bind
またはid
が適用されます。
<!-- template -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- result -->
<div id="blue"></div>
<!-- template -->
<div v-bind="{ id: 'blue' }" id="red"></div>
<!-- result -->
<div id="red"></div>
Vue2 ではv-on
に.native
を適用できましたが、Vue3 からは Emit を使用するように変更になりました。
<!-- Vue 2.x -->
<my-component
v-on:close="handleComponentEvent"
v-on:click.native="handleNativeClickEvent"
/>
<!-- Vue 3.x -->
<my-component
v-on:close="handleComponentEvent"
v-on:click="handleNativeClickEvent"
/>
<script>
export default {
emits: ['close']
}
</script>
Vue2 ではv-for
でループ時、$refs
を自動的に作成可能でしたが Vue3 からは明示的に$ref
を設定する必要があります。$ref
はフォーム関連でもよく使用するテクニックなので確実に抑えておきましょう。(input などのフォーム要素をループ処理でレンダリングした後、$ref
を扱いたい時などは注意が必要です)
<div v-for="item in list" :ref="setItemRef"></div>
// CompositionAPI
import { onBeforeUpdate, onUpdated } from 'vue'
export default {
setup() {
let itemRefs = []
const setItemRef = el => {
if (el) {
itemRefs.push(el)
}
}
onBeforeUpdate(() => {
itemRefs = []
})
onUpdated(() => {
console.log(itemRefs)
})
return {
setItemRef
}
}
}
Vue2 から Vu3 へのマイグレーション手順については公式サイトで公開されていますのでそちらを参照してください。
以下が注目機能の(順次解説)となります、Vue3 サイト公式でも紹介されていますが、少々わかりにくい表現が多いため、私たちの例を交えながら紹介したいと思います。
CompositionAPI は、Vue3 から新たに提供された API(新たな Vue ファイルの JSの記述方式)です。Vue2 系の Option API も引き続き提供される様子なので、Vue2 から移行してきたアプリケーションは、無理に CompositionAPI へ変更する必要はないです。
ただし後述に記載する通り、新規実装の場合は例外なく CompositionAPI での実装が推奨されます。(既存も余力あれば CompositionAPI への移行を推奨)
以下は、特定のユーザーのリポジトリのリスト表示する ComponentのOptionAPI による実装例です。
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
data () {
return {
repositories: [], // 1
filters: { ... }, // 3
searchQuery: '' // 2
}
},
computed: {
filteredRepositories () { ... }, // 3
repositoriesMatchingSearchQuery () { ... }, // 2
},
watch: {
user: 'getUserRepositories' // 1
},
methods: {
getUserRepositories () {
// using `this.user` to fetch user repositories
}, // 1
updateFilters () { ... }, // 3
},
mounted () {
this.getUserRepositories() // 1
}
}
searchQuery
文字列を使用してリポジトリを検索filters
を使用したリポジトリのフィルタリング従来の OptionAPI を使用した見慣れた実装ですが、機能が大きくなると、論理的に理解・把握の難しいコード(最初からコンポーネントを作成しなかった人にとって読みにくく、理解しにくいコード)となってしまう可能性が非常に高いです。
以下の色付けされた画像は、OptionAPI で記述されたソースコードの論理的整理を表していますが、OptionAPI での実装は複雑になりがちなのが伺えます…。
Vue3 では、従来の OptionAPI の課題を解決する新たな API としてCompositionAPI が提供されています。 以下は、私たちが実装した、学校の生徒の成績を表示する CompositionAPI のサンプルコードです。
import {
ref,
defineComponent,
onMounted,
onBeforeMount,
watch,
computed,
toRefs,
} from "vue";
import { listStudentScoreByStudentId, StudentScore } from "../api/student";
import _ from "lodash";
type Props = {
studentName: String;
studentId: String;
};
export default defineComponent({
name: "StudentScore",
props: {
studentName: {
type: String,
required: true,
},
studentId: {
type: String,
required: true,
},
},
emits: ["update:studentId", "logout"],
setup: (props: Props, { emit }) => {
// refでリアクティブな値を生成
const show = ref<boolean>(false);
const selectedCategory = ref<number>(1);
const studentScore = ref<StudentScore[]>([]);
const modalOpen = ref<boolean>(false);
// Propsの変更に確実に反応するためにRef化
const { studentId } = toRefs(props);
// methods
const toggleShow = () => (show.value = !show.value);
const fetchStudentScore = async () =>
(studentScore.value = await listStudentScoreByStudentId(props.studentId));
const updateSelectedCategory = (category: number) =>
(selectedCategory.value = category);
// computed
const studentScoreSummary = computed(() =>
_.chain(studentScore.value)
.filter(
(item) =>
item.category === selectedCategory.value &&
item.studentId === v.value
)
.sumBy("score")
.value()
);
// watchによる追跡(Propsの変更で発火)
watch(studentId, fetchStudentScore);
// ライフサイクルフック登録(マウント後に生徒情報をフェッチ)
onMounted(() => fetchStudentScore());
// emitによる子から親へのイベント発火
const removeStudent = () => emit("removeStudentById", studentId.value);
return {
show,
toggleShow,
studentScoreSummary,
updateSelectedCategory,
selectedCategory,
modalOpen,
removeStudent
};
},
});
正直見慣れない記述が幾つもあると思いますが、私たちの考える OptionAPI と CompositionAPI の違いをまとめると次のようになります。
以下は、OptionAPI と CompositionAPI の論理的整理を行なったサンプルですが、ソースコードの見通しが Composition API の方が優れているのが一目瞭然です。
任意の HTML の箇所に VueJS の Component をレンダリングする機能です。モーダルウィンドウを Body 直下に展開したい時などに使用します。
以下の例では、Body タグの直下にモーダルウィンドウのコンテンツをレンダリングしています。
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
モーダルウィンドウのコンテンツ
<button @click="modalOpen = false">
モーダルウィンドウを閉じる
</button>
</div>
</div>
</teleport>
モーダルウィンドウをposition:fixed
などで実装したいケースにとても有用です。(画面全体にオーバーレイを貼りたい時等)親のposition
の値を確認するストレスから解放されるでしょう。
Vue3 より Component へルートノードの設定が必須ではなくなりました。ReactJS の<></>
よりもシンプルな記述が可能になります。
<!-- Layout.vue -->
<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>
<!-- Layout.vue -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
親から子供の Component へイベント及び v-model をシンプルに渡せるようになりました。
<!-- 親のComponent -->
<template>
<StudentScore v-model:studentId="studentId" :student-name="studentName" @remove-student-by-id="removeStudentById" />
</template>
// 親のComponent
import { defineComponent, ref } from "vue";
import StudentScore from "./components/StudentScore.vue";
import { removeStudentById } from "../api/student";
export default defineComponent({
name: "App",
components: {
StudentScore,
},
setup() {
const studentId = ref<String>("exampleStudentId");
const studentName = "田中太郎";
const removeStudentById = (_studentId: String) => removeStudentById(_studentId);
return {
studentId,
studentName,
removeStudentById,
};
},
});
// 子供のComponent
import { defineComponent, toRefs, emit } from "vue";
type Props = {
studentId: String,
studentName: String
}
export default defineComponent({
name: "StudentScore",
emits: ["update:studentId", "removeStudentById"],
setup(props:Props) {
const { studentId, studentName } = toRefs(props);
// emitを使用し親のイベントを実行
const removeStudentById = () => emit("removeStudentById", studentId.value);
// v-modelをPropsへ渡すことでupdate:{prop_name}が自動的にemitへアサインされる
const updateStudentId = () => emit("update:studentId", "update value!")
return {
studentId,
studentName,
removeStudentById,
updateStudentId
};
},
});
emit がよりシンプルにエレガントな記述方法になりました!これにより Component 実装の幅がより広がったと思います。
Vue2 から Vue3 へメジャーバージョンアップし、VueJS は大規模なアプリケーション開発に強く進化しました。以前まで VueJS は、ReactJS と比較して大規模に弱いといった評価をされ続けていましたがこれでどちらも遜色なくなった印象です。
私たちは HTML の書きやすさ・シンプルな構文などから、VueJS が今後伸びていくのでは?と期待をしています、皆さんもよろしければ VueJS を応援してみてください。
大規模・堅牢なフロントエンド開発は、ラーゲイトまでお気軽にお問い合わせください。
スモールスタート開発支援、サーバーレス・NoSQLのことなら
ラーゲイトまでご相談ください
低コスト、サーバーレスの
モダナイズ開発をご検討なら
下請け対応可能
Sler企業様からの依頼も歓迎