バックエンドエンジニアのためのVue.js/Nuxt.js入門
JS のフレームワークの 1 つである Vue.js・Nuxt.js の基本的な内容を、バックエンドエンジニアの方向けに説明します。
Vue.js とは
- UI 構築のための JavaScript フレームワーク
- 標準的な HTML を拡張したテンプレート構文を使って、HTML の出力を宣言的に記述できる
- JavaScript の状態の変化を自動的に追跡し、変化が起きると効率的に DOM を更新してくれる
- ユーザーの入力も追跡してくれる(双方向バインディング)
単一ファイルコンポーネント(SFC)
- 画面を開発していく時に、コンポーネント単位で区切って開発することが望ましい(理由は後述)。
- これを実現するために、Vue.js では単一ファイルコンポーネント(Single File Component; SFC)という仕組みがある。
- SFC では、1 つのコンポーネントにかかわるテンプレート(HTML)、スクリプト(JS)、スタイル(CSS)を 1 つの
.vue
ファイルにまとめる。
<template>
<button @click="count++">Count is: {{ count }}</button>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
data: () => ({
count: 0,
}),
});
</script>
<style scoped>
button {
font-weight: bold;
}
</style>
↑ https://ja.vuejs.org/guide/introduction.html#single-file-components より引用(一部改変)
-
<template> ~ </template>
: HTML を記述 -
<script> ~ </script>
: JS を記述-
lang="ts"
: JS ではなく TS で記述するための宣言
-
-
<style> ~ </style>
: CSS を記述-
scoped
: CSS が適用される範囲をこのコンポーネントに閉じることが可能
-
次のように、他のコンポーネントの取り込み(import)も簡単にできる。
<template>
<h1>Here is a child component!</h1>
<ButtonCounter />
</template>
<script lang="ts">
import { defineComponent } from "vue";
import ButtonCounter from "./ButtonCounter.vue";
export default defineComponent({
components: {
ButtonCounter,
},
});
</script>
↑ https://ja.vuejs.org/guide/essentials/component-basics.html#using-a-component より引用(一部改変)
- 昔はファイルを分割するときは、通常 HTML・JS・CSS で分割することが多かった。
- 一方で、Vue.js ではコンポーネント単位でファイルを分割する。これは以下の理由による。
- 「関心事項の分離」が「ファイルタイプの分離」と等しくない。
- SFC なら「ボタン」という関心事項に含まれるソースコード(テンプレート、ロジック、スタイル)一式を 1 ファイルに記述できる。
- 結果的にコンポーネントの一貫性と保守性が高くなる。
- 「関心事項の分離」が「ファイルタイプの分離」と等しくない。
Options API と Composition API
Vue.js では歴史的経緯から 2 つの API スタイルがある。
Options API
data
、methods
、mounted
といった数々のオプションからなる 1 つのオブジェクトを用いてコンポーネントのロジックを定義するスタイル。
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
// data() で返すプロパティはリアクティブな状態になり、
// `this` 経由でアクセスすることができます。
data: () => ({
count: 0,
}),
// ライフサイクルフックは、コンポーネントのライフサイクルの
// 特定のステージで呼び出されます。
// 以下の関数は、コンポーネントが「マウント」されたときに呼び出されます。
mounted() {
console.log(`The initial count is ${this.count}.`);
},
// メソッドの中身は、状態を変化させ、更新をトリガーさせる関数です。
// 各メソッドは、テンプレート内のイベントハンドラーにバインドすることができます。
methods: {
increment() {
this.count++;
},
},
});
</script>
Composition API
インポートした各種 API 関数を使ってコンポーネントのロジックを定義するスタイル。
以下は、上記のサンプルコードを Composition API で書き換えたものとなる。
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
// リアクティブな状態
const count = ref(0);
// 状態を変更し、更新をトリガーする関数。
function increment() {
count.value++;
}
// ライフサイクルフック
onMounted(() => {
console.log(`The initial count is ${count.value}.`);
});
</script>
詳細は以下を参照してください。(サンプルコードもこちらから引用しています)
基本的な文法
公式チュートリアルが充実しているので、そちらをやってみるのが良いと思います。
Vue Router
シングルページアプリケーションでは、クライアントサイドのルーターが必要になります。
- マルチページアプリケーション(MPA)の場合
- リンクを踏む
- サーバーサイドでルーティングして全データを取得
- 画面遷移
- シングルページアプリケーション(SPA)の場合
- リンクを踏む
- クライアントサイドでルーティングして必要なデータだけ API 呼び出し
- 画面更新
Vue.js 用のルーターが Vue Router です。使い方は以下の公式ガイドの通りで、至って簡単です。
なお、Nuxt.js では pages ディレクトリに vue ファイルを配置するだけでルーティングが登録されます。
Vuex
Vuex とは?
- Vue.js アプリケーションのための 状態管理パターン + ライブラリ。
- 状態管理パターンとして Flux の影響を強く受けている(後述)
- 予測可能な方法によってのみ状態の変更を行うというルールを保証。
- アプリケーションのあちこちで状態を変更できると、どこで状態変更がされたかが追いいづらくなる。デバッグが困難になる。
- アプリケーション内の全てのコンポーネントのための集中型のストアとして機能。
- 複数のコンポーネント間で状態を共有することができる。
- ただし、見方を変えると「グローバル変数」とも言えるので、何でもかんでも共有するのは好ましくない。
Flux とは?
- 単一方向にデータが流れる。
- 状態の変更を追いやすくなる。
- Action: ユーザーからの操作を表わすオブジェクト
- Dispatcher: Action を受け取って Store に渡す
- Store: データを保持する場所。Action を受け取ってデータを変更する。変更を通知する
- View: Store のデータを表示する
↑ https://github.com/facebookarchive/flux/tree/main/examples/flux-concepts より引用
Vuex では...
- Action: ユーザーからの操作を表わすオブジェクト。API を叩くなどのデータ処理を実行する。
- Mutations: 状態を変更できる唯一の手段。Action から commit されることで実行できる。
- State: データを保持する場所。変更を通知する。
- Vue Components: State のデータを表示する。Action を dispatch する。
↑ https://vuex.vuejs.org/ja より引用
概念だけだと分かりにくいので、コード例を示します。
簡単なカウンターを使って説明します。
import { createStore } from 'vuex';
createStore({
// store で管理しているデータ。
state: () => ({
counter: 0,
}),
// dispatch で呼び出されるメソッド。
actions: {
increment({ commit }) {
commit("INCREMENT");
},
},
// state を変更するメソッド。
mutations: {
INCREMENT(state) {
state.counter++;
},
},
});
<template>
<button @click="increment">{{ counter }}</button>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
computed: {
// store から counter を取り出す。
// 取り出した値はテンプレート内で使える。
counter() {
return this.$store.state.counter;
},
},
methods: {
// ボタンがクリックされたら
// store の increment を dispatch する。
increment() {
this.$store.dispatch("increment");
},
},
});
</script>
実際には モジュール に分割して利用することが多いです。
Nuxt2 では、store ディレクトリを用いることで簡単に利用ができます。
また、Nuxt2 までは Vuex が組み込まれていましたが、Nuxt3 では組み込まれなくなりました。状態管理ライブラリを使用する場合は Pinia が推奨され、別途簡単な導入手順を踏む必要があります。
Nuxt.js とは
- Vue.js アプリケーションを構築するためのフレームワーク
- 環境準備が大変、ディレクトリ構成迷う、というような課題を吸収してくれる
- Nuxt.js を使えば、Vue.js でアプリケーションを構築するときのデファクトスタンダードな構成が手に入る
Nuxt.js には以下の要素 + それらを利用するための諸々の設定が含まれている。
カテゴリ | Nuxt2 | Nuxt3 |
---|---|---|
Vue | Vue2 | Vue3 |
ルーティング | Vue Router 3 | Vue Router 4 |
状態管理 | Vuex | なし(useState, Pinia などから選択) |
SSR | vue-server-renderer | vue/server-renderer |
メタタグ管理 | vue-meta | @unhead/vue |
バンドラー | Webpack4 + Babel | Vite or Webpack5 |
サーバー | Express | nitro, h3 |
テスト | Jest | Vitest |
Nuxt.js による開発では、基本的には Vue.js の知識をそのまま利用できる。
加えて、Nuxt 特有のディレクトリ構造や機能はあるので、それらは別途必要に応じて学ぶ必要がある。
Discussion