Vue.js さくっと復習
基礎概念
Vue.jsとは?
Webのユーザインターフェースを簡単に作るためのJavaScriptフレームワーク
単一ファイルコンポーネントの構造
Vue.js では 1つのファイルに ロジック(script), 表示(template), デザイン(style) を記述する。<script>ではJavaScript/TypeScript, <template>ではHTML, <style>ではCSSを使用する。また、scriptで定義した変数をtemplate側で{{ 変数名 }}とすることで、変数の中身が表示される。
この形式を 単一ファイルコンポーネント (SFC: Single File Component) と呼ぶ。
<script setup>
const name = "Yanagi";
console.log(name);
</script>
<template>
<h1>Name</h1>
<p>{{ name }}</p> <!-- Yanagi -->
</template>
<style>
h1 {
color: red;
}
</style>
ディレクティブ
Vue には ディレクティブ(directive) という仕組みがあります。
ディレクティブとは、script で定義したデータや処理を、template(HTML部分)に結びつけるための特別な書き方 です。 これにより、変数や計算結果などを HTML に簡単に反映できます。
v-bind
v-bind は 要素やコンポーネントの属性にデータをバインドする ディレクティブです。
HTML の属性に JavaScript(Script) の値を埋め込みたいときに使います。
以下では、script で定義したURLをaタグのhref属性に埋め込んでいます。
<script setup>
import { ref } from "vue";
const url = ref("https://vuejs.org");
const Id = ref('vue-link');
</script>
<template>
<!-- v-bind を使って属性に変数をバインド -->
<a v-bind:id="Id" v-bind:href="url">Vue公式サイト</a>
</template>
また、v-bindは多用されるため、以下のようにv-bind: は : に省略可能。
<script setup>
import { ref } from "vue";
const url = ref("https://vuejs.org");
const Id = ref('vue-link');
</script>
<template>
<!-- v-bind を使って属性に変数をバインド -->
<a :id="Id" :href="url">Vue公式サイト</a>
</template>
あるいは、一度に複数の属性を設定できます。
<script setup>
import { ref } from "vue";
const url = ref("https://vuejs.org");
const Id = ref('vue-link');
</script>
<template>
<!-- v-bind を使って属性に変数をバインド -->
<a v-bind="{ id: Id, href: url}">Vue公式サイト</a>
</template>
また、v-bind は class や style を動的に制御することもできます。
<script setup>
import { ref } from "vue";
const isActive = ref(true);
</script>
<template>
<!-- class のバインディング -->
<p :class="{ active: isActive }">クラス付与の例</p>
<!-- style のバインディング -->
<p :style="{ color: isActive ? 'red' : 'gray' }">スタイル切り替え</p>
</template>
<style>
.active {
font-weight: bold;
}
</style>
v-on
v-on は HTML 要素で発生するイベントを script 側の処理と結びつけるためのディレクティブ です。
つまり、template 上で起こる操作(クリック、入力、マウス移動など)を script 側の関数や式で処理できるようにします。
以下は、ボタンを押すたびに count が増え、画面に表示される処理を v-on で作っています。
<script setup>
import { ref } from "vue";
const count = ref(0);
function increment() {
count.value++;
}
</script>
<template>
<p>カウント: {{ count }}</p>
<!-- ボタンをクリックすると increment 関数が実行される -->
<button v-on:click="increment">+1</button>
</template>
また、v-onは多用されるため,以下のようにv-on: は @ に省略可能です。
<button @click="increment">+1</button>
また、v-on では、DOM イベントオブジェクトを関数の引数として受け取ることができます。
イベントオブジェクトには、クリック位置や入力内容、マウスの動きなど、イベントに関する情報が入っています。
以下では、v-onを使用し、入力フォームで文字を取得して即座に表示できるようなプログラムになっています。この場合、ユーザーが入力するたびに handleInput 関数が呼ばれ、event.target.value から文字を取得して inputText に反映されます。
<script setup>
import { ref } from "vue";
const inputText = ref("");
function handleInput(event) {
// event.target.value で入力値を取得
inputText.value = event.target.value;
}
</script>
<template>
<input type="text" @input="handleInput" placeholder="文字を入力">
<p>入力内容: {{ inputText }}</p>
</template>
また、v-onのイベントハンドラには以下のように引数を渡すことも可能です。
引数を渡したい時は、$eventを第一引数にすることを忘れないようにしましょう。
<script setup>
import { ref } from "vue";
const inputText = ref("");
function handleInput(event, arg2) {
// event.target.value で入力値を取得
inputText.value = event.target.value;
console.log(arg2);
}
</script>
<template>
<input type="text" @input="handleInput($event, 2)" placeholder="文字を入力">
<p>入力内容: {{ inputText }}</p>
</template>
さらに、v-on では、**イベント発生時の挙動を簡単に制御する「イベント修飾子」**を使うことができます。 イベント修飾子はイベント名の後に.prevent,.stop, .onceなど をつけて書きます。
以下では、@submit.prevent="handleSubmit" としているため、
ブラウザのデフォルト送信(ページリロード)を防ぎつつ、handleSubmit を呼びことができる。
<script setup>
import { ref } from "vue";
const name = ref("");
function handleSubmit() {
alert(`送信内容: ${name.value}`);
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<input type="text" v-model="name" placeholder="名前">
<button type="submit">送信</button>
</form>
</template>
以下は、.stopを使用した例であり、.stop が付いているので 子要素(button)をクリックしても、親要素(div)のalertが実行されなくなります。
<template>
<div @click="alert('親がクリックされました')">
<p>親要素(この枠内をクリックすると親の処理が実行されます)</p>
<!-- stop を付けると親には伝播しない -->
<button @click.stop="alert('ボタンがクリックされました')">ボタン</button>
</div>
</template>
さらに、v-on では、**特定のキー押下時だけ処理を実行する「キー修飾子」**を使うことができます。
キー修飾子はイベント名の後に .enter, .esc, .space などをつけて書きます。
以下では、@keyup.enter="handleEnter" としているため、
Enter キーが押されたときだけ handleEnter を呼び出すことができます。
他のキーでは処理は実行されません。
<script setup>
import { ref } from "vue";
const inputText = ref("");
const count = ref(0);
function handleEnter(event) {
inputText.value = event.target.value;
count.value++;
event.target.value = "";
}
</script>
<template>
<input
type="text"
@keyup.enter="handleEnter"
placeholder="Enter を押すとカウント"
>
<p>現在の入力: {{ inputText }}</p>
<p>Enter 押下回数: {{ count }}</p>
</template>
v-if/v-else/v-else-if
v-if は、条件によって要素を表示・非表示にするための仕組みです。
以下は、v-if/v-else/v-else-ifを用いて、スコアに応じて評価を表示しています。
<script setup>
import { ref } from "vue";
const score = ref(75);
</script>
<template>
<p v-if="score >= 90">評価: A</p>
<p v-else-if="score >= 70">評価: B</p> <!-- score = 75 のため、ここが表示される -->
<p v-else-if="score >= 50">評価: C</p>
<p v-else>評価: D</p>
</template>
また、以下のように<template>内に<template>を作り、
<template> 自体に v-if を付けて複数要素をまとめて条件付きレンダリング することもできます。
<script setup>
import { ref } from "vue";
const isLoggedIn = ref(true);
</script>
<template>
<!-- 複数要素をまとめて v-if で制御 -->
<template v-if="isLoggedIn">
<p>ようこそ、ユーザーさん!</p>
<button>ログアウト</button>
</template>
<template v-else>
<p>ログインしてください</p>
<button>ログイン</button>
</template>
</template>
v-show
v-show は v-ifと違い、要素を非表示にする際に DOM を削除せず、CSS の display: none を使って隠す ディレクティブです。
<script setup>
import { ref } from "vue";
const isVisible = ref(true);
function toggle() {
isVisible.value = !isVisible.value;
}
</script>
<template>
<button @click="toggle">切り替え</button>
<!-- v-show は CSS display をnoneかnoneではないかに切り替えるだけ -->
<p v-show="isVisible">この要素は表示/非表示が切り替わります</p>
</template>
以下がv-ifとv-showを比べたものです。
-
v-if:条件に応じて DOM を生成・破棄 → 初期レンダリングコストあり
画面上で一度だけ表示するかもしれない要素 →v-ifを使用
-
v-show:DOM は常に存在 → CSS のdisplayを切り替えるだけ
頻繁に表示/非表示を切り替える要素 →v-showを使用
v-for
v-for は 配列やオブジェクトの中身を取り出して繰り返し表示するディレクティブです。
配列をループ
配列の要素を順番に取り出して表示する基本的な使い方です。
ここでは fruits 配列からフルーツ名を繰り返し表示しています。
<script setup>
import { ref } from "vue";
const fruits = ref(["りんご", "バナナ", "みかん"]);
</script>
<template>
<ul>
<!-- 配列の要素を fruit として取り出す -->
<li v-for="fruit in fruits" :key="fruit">
{{ fruit }}
</li>
</ul>
</template>
オブジェクトをループ
オブジェクトのプロパティを繰り返し表示できます。
<script setup>
import { ref } from "vue";
const user = ref({
name: "太郎",
age: 25,
country: "Japan",
});
</script>
<template>
<ul>
<li v-for="value in user" :key="value">
{{ value }}
</li>
</ul>
</template>
<!-- 出力:
<li>太郎</li>
<li>25</li>
<li>Japan</li>
-->
分割代入
オブジェクトの配列をループする際、分割代入で id, name, age を直接取り出せます。
id を :key に使うことで一意性を保証します。
<script setup>
import { ref } from "vue";
const users = ref([
{ id: 1, name: "太郎", age: 25 },
{ id: 2, name: "花子", age: 22 },
]);
</script>
<template>
<ul>
<!-- 分割代入で { id, name, age } を直接展開 -->
<li v-for="{ id, name, age } in users" :key="id">
{{ name }} ({{ age }}歳)
</li>
</ul>
</template>
数値でループ(n in 10)
数値を指定すると、指定した数値までの数値が繰り返し処理されます。
<template>
<ul>
<!-- n には 1~10 が順番に入る -->
<li v-for="n in 10" :key="n">
{{ n }}
</li>
</ul>
</template>
<template> で複数要素をまとめてループ
<template> に v-for を付けることで、複数の要素を一度に繰り返すことができます。
ここではフルーツ名と区切り線をまとめてループしています。
<script setup>
import { ref } from "vue";
const fruits = ref(["りんご", "バナナ", "みかん"]);
</script>
<template>
<ul>
<template v-for="fruit in fruits" :key="fruit">
<li>{{ fruit }}: おいしい!</li>
<li>--- 区切り ---</li>
</template>
</ul>
</template>
v-model
v-model は フォーム入力とデータを双方向にバインディングするディレクティブです。
つまり、入力欄に文字を入力すると Vue 側のデータが更新され、逆にデータを書き換えると入力欄も更新されます。
inputでの使用
<input> に v-model をつけると、入力値と変数(ここでは message)が常に同期します。
ユーザーが入力欄に文字を入力すると即座に message が更新され、同時に画面表示も変わります。
<script setup>
import { ref } from "vue";
const message = ref("こんにちは");
</script>
<template>
<div>
<!-- 入力欄と message を同期 -->
<input v-model="message" />
<p>入力値: {{ message }}</p>
</div>
</template>
<!-- 出力例
入力欄に「こんばんは」と入力すると
<p>入力値: こんばんは</p> と表示される
-->
チェックボックスでの使用
チェックボックスに v-model を付けると、真偽値(true / false)を自動で同期できます。
<script setup>
import { ref } from "vue";
const checked = ref(false);
</script>
<template>
<div>
<input type="checkbox" v-model="checked" />
<p>チェック状態: {{ checked }}</p>
</div>
</template>
<!-- 出力例
チェックなし: チェック状態: false
チェックあり: チェック状態: true
-->
複数チェックボックスでの使用
複数のチェックボックスに v-model をつけると、選択された値が配列として保持されます。
<script setup>
import { ref } from "vue";
const selectedFruits = ref([]);
</script>
<template>
<div>
<label>
<input type="checkbox" value="りんご" v-model="selectedFruits" /> りんご
</label>
<label>
<input type="checkbox" value="バナナ" v-model="selectedFruits" /> バナナ
</label>
<label>
<input type="checkbox" value="みかん" v-model="selectedFruits" /> みかん
</label>
<p>選択中: {{ selectedFruits }}</p>
</div>
</template>
ラジオボタンでの使用
ラジオボタンに v-model を付けると、選択された値が変数に反映されます。
複数の選択肢から 1 つを選ぶ場合に使います。
<script setup>
import { ref } from "vue";
const picked = ref("");
</script>
<template>
<div>
<label>
<input type="radio" value="A" v-model="picked" /> A
</label>
<label>
<input type="radio" value="B" v-model="picked" /> B
</label>
<p>選択: {{ picked }}</p>
</div>
</template>
セレクトボックスでの使用
セレクトボックスに v-model を付けると、選択された <option> の値を変数に保持できます。
<script setup>
import { ref } from "vue";
const selected = ref("");
</script>
<template>
<div>
<select v-model="selected">
<option disabled value="">選んでください</option>
<option>りんご</option>
<option>バナナ</option>
<option>みかん</option>
</select>
<p>選択: {{ selected }}</p>
</div>
</template>
修飾子(.lazy, .number, .trim)
v-model には便利な修飾子があります。
-
.lazy: 入力欄を離れたタイミング(blur時)に反映 -
.number: 入力値を数値に変換 -
.trim: 前後の余計な空白を削除
以下では .number を使って、入力値を文字列ではなく数値として扱っています。
<script setup>
import { ref } from "vue";
const age = ref(0);
</script>
<template>
<div>
<input v-model.number="age" />
<p>数値: {{ age }} (型: {{ typeof age }})</p>
</div>
</template>
<!-- 出力例
入力欄に「25」と入力すると
<p>数値: 25 (型: number)</p> と表示される
-->
defineModel
defineModel は Vue 3.3 で追加された Composition API です。
カスタムコンポーネントで v-model を使うときに、これまで必要だった props / emits の定義を省略でき、シンプルに記述できます。
また、引数を指定することで 複数の v-model を扱うこともできます。
<script setup>
import { ref } from "vue";
import MyForm from "./MyForm.vue";
const name = ref("太郎");
const userAge = ref(20);
</script>
<template>
<MyForm v-model:username="name" v-model:age="userAge" />
<p>{{ name }} ({{ userAge }}歳)</p>
</template>
<!-- 子コンポーネント MyForm.vue -->
<script setup>
const username = defineModel<string>("username");
const age = defineModel<number>("age");
</script>
<template>
<div>
<label>
名前: <input v-model="username" />
</label>
<label>
年齢: <input type="number" v-model="age" />
</label>
</div>
</template>
リアクティブデータ
ref/reactive
Vue では、変数を「リアクティブ(反応的)」にすることで、変数の値が変わったときに自動で画面(DOM)が更新され、変数の変化を画面に反映します。そのために使うのが ref と reactive です。
ref
- 単純な値(数値・文字列・真偽値など)をリアクティブにしたいときに使う。
-
.valueプロパティを通して値を読み書きする。
<script setup>
import { ref } from "vue";
const count = ref(0); // countはリアクティブな変数
function increment() {
count.value++;
}
</script>
<template>
<p>現在のカウント: {{ count }}</p>
<button @click="increment">+1</button>
</template>
reactive
-
reactiveは オブジェクトや配列を丸ごとリアクティブ化する ときに使います。 -
.valueは不要で、そのままオブジェクトのプロパティを操作できます。
<script setup>
import { reactive } from "vue";
const user = reactive({
name: "Yanagi",
age: 25
});
function birthday() {
user.age++;
}
</script>
<template>
<p>{{ user.name }} は {{ user.age }} 歳です。</p>
<button @click="birthday">誕生日</button>
</template>
使い分けのイメージ
- 数値や文字列、真偽値など単一の値 → ref
- オブジェクトや配列で複数の値をまとめたい → reactive
- しかし、基本的には ref を推奨(公式でもref推奨)
→ シンプルで直感的、型推論が効きやすく、分割代入してもリアクティブ性が失われにくいから。
computed
computed は 「ある変数をもとにして作られる計算結果」 を自動で管理してくれる仕組みです。
computedを使用すると、依存する変数が変わると自動で再計算され、テンプレートに反映されます。
また、計算結果がキャッシュされるため、依存する値が変わらない限り、再計算は行われません。
<script setup>
import { ref, computed } from "vue";
const firstName = ref("Yanagi");
const lastName = ref("Taro");
// フルネームを自動計算
const fullName = computed(() => {
console.log("computed 実行");
return `${lastName.value} ${firstName.value}`;
});
function getFullName() {
console.log("method 実行");
return `${lastName.value} ${firstName.value}`;
}
</script>
<template>
<p>computed: {{ fullName }}</p>
<p>method: {{ getFullName() }}</p>
</template>
メソッドとの違い
| 項目 | computed | メソッド |
|---|---|---|
| 計算結果 | キャッシュされる | 毎回呼び出すたびに再計算 |
| 再計算の条件 | 依存している変数が変わったとき | 呼び出されるたびに必ず |
上の例だと、テンプレートで fullName を何度使っても、
実際の計算は依存する firstName / lastName が変わったときだけ実行されます。
一方 getFullName() は呼び出されるたびに毎回実行されます。
watch/watchEffect
vue には「変数の変化を監視して処理を実行する」仕組みとして
watch と watchEffect の 2 種類があります。
watch
watch は監視する変数を自分で指定し、 特定の変数を明示的に監視 します。
監視している変数が変化したときに処理が行われます。
また、「古い値・新しい値」を受け取ることも可能です。
<script setup>
import { ref, watch } from "vue";
const count = ref(0);
// count を明示的に監視
watch(count, (newValue, oldValue) => {
console.log(`count: ${oldValue} → ${newValue}`);
});
</script>
<template>
<button @click="count++">count: {{ count }}</button>
</template>
watchEffect
watchEffect は 使っている変数を自動で検出して監視 します。
内部で参照した変数が変化すると、そのたびに再実行されます。
また、watchEffectでは「古い値」「新しい値」を取得することはできません。
<script setup>
import { ref, watchEffect } from "vue";
const count = ref(0);
// 依存関係は自動検出
watchEffect(() => {
console.log(`count の現在値: ${count.value}`);
});
</script>
<template>
<button @click="count++">count: {{ count }}</button>
</template>
computed vs watch vs watchEffect
| 項目 | computed | watch | watchEffect |
|---|---|---|---|
| 主な目的 | 値を計算して返す | 特定の値を監視して 処理を実行 |
依存する値を自動検出して処理を実行 |
| 値の返却 | あり (キャッシュされる計算結果) |
なし | なし |
| キャッシュ | あり(依存が変わらない限り再計算しない) | なし(監視対象が変わるたび必ず実行) | なし(依存が変わるたび 必ず実行) |
| 監視対象の指定 | 自動で依存関係を検出 | 明示的に指定する必要あり | 自動で依存関係を検出 |
| 新旧値の取得 | 不要 | 可能(newValue, oldValue を取得できる) |
不可(常に最新の値のみ利用) |
コンポーネント
Vue ではアプリケーションを コンポーネント単位 で分割して作ります。
コンポーネントは 再利用可能な UI 部品 のようなもので、
データやイベントのやり取りを行いながら構築します。
コンポーネントツリー
Vue ではコンポーネントは 親子関係 で構成され、
アプリ全体を「コンポーネントツリー」として考えます。
子コンポーネント (child.vue)
<template>
<p>子コンポーネントです</p>
</template>
<script setup>
</script>
親コンポーネント (parent.vue)
親コンポーネントが子コンポーネント(child.vue)を <Child /> として使用している
<template>
<h2>親コンポーネント</h2>
<Child />
<Child />
</template>
<script setup>
import Child from './Child.vue';
</script>
コンポーネントツリー
上記のコンポーネントの例では、Parent が Child を 2 つ持っているような
コンポーネントツリーになっている。
App
└─ Parent
├─ Child
└─ Child
Props
props は 親コンポーネントから子コンポーネントにデータを渡すための仕組み です。
子コンポーネントは props で受け取った値を表示したり、処理に利用することができます。
子コンポーネント (child.vue)
-
definePropsで親から渡されるmessageを受け取る - 型を指定できる(ここでは
String) - 渡された
messageをtemplate内で使用できる
<template>
<p>子コンポーネントです。メッセージ: {{ message }}</p>
</template>
<script setup>
defineProps({
message: String
});
</script>
親コンポーネント (parent.vue)
-
ParentではChildのmessageに値を渡している
<template>
<h2>親コンポーネント</h2>
<Child message="こんにちは、子コンポーネント!" />
<Child message="もう一つのメッセージ" />
</template>
<script setup>
import Child from './Child.vue';
</script>
Emit
emit は 子コンポーネントから親コンポーネントにイベントやデータを送る仕組み です。
子がボタンを押したり入力したときなどに、親へ「通知」を送れます。
子コンポーネント (child.vue)
-
defineEmitsを使って、親に送るイベント名を定義する - ボタンが押されたら
emit("send", "こんにちは!...")を実行する - これで親に「send」というイベントが通知され、データ
"こんにちは!..."が渡される
<template>
<div>
<p>子コンポーネント</p>
<!-- ボタンをクリックしたら sendMessage 関数を実行 -->
<!-- クリックで親コンポーネントにイベント(send)とデータ(こんにちは!...)を送信 -->
<button @click="sendMessage">親にメッセージ送信</button>
</div>
</template>
<script setup>
// defineEmits で親に送るイベント名を定義
// この場合、'send' というイベントを親に通知できる
const emit = defineEmits(["send"]);
// ボタンがクリックされたときに呼ばれる関数
function sendMessage() {
emit("send", "こんにちは! 親コンポーネント");
}
</script>
親コンポーネント (parent.vue)
- 子からのイベントを
@send="onReceive"のように受け取る - 受け取ったメッセージを
onReceive関数で処理できる
<template>
<h2>親コンポーネント</h2>
<!-- 子が 'send' イベントを送ると onReceive 関数を実行 -->
<Child @send="onReceive" />
<p>受け取ったメッセージ: {{ receivedMessage }}</p>
</template>
<script setup>
import { ref } from "vue";
import Child from "./Child.vue";
const receivedMessage = ref("");
// 子から 'send' イベントが送られたときに呼ばれる関数
// 引数 msg に子から送られた「こんにちは! 親コンポーネント」が入る
function onReceive(msg) {
receivedMessage.value = msg;
}
</script>
Slot
Slot は 親コンポーネントから子コンポーネントに任意のコンテンツを差し込む仕組み です。
名前付き slot や slot props を使うことで、より柔軟なコンテンツの差し込みが可能です。
子コンポーネント (Card.vue)
- 複数の slot を名前で区別できる
- slot props を使うと、子から親にデータを渡せる
<template>
<div class="card">
<!-- 名前付き slot: header -->
<header>
<slot name="header">デフォルトのヘッダー</slot>
</header>
<!-- 名前付き slot: body -->
<main>
<slot name="body">デフォルトの本文</slot>
</main>
<!-- 名前付き slot: footer に slot props を渡す -->
<footer>
<slot name="footer" :info="footerInfo">デフォルトのフッター</slot>
</footer>
</div>
</template>
<script setup>
const footerInfo = "子コンポーネントから渡すフッター情報";
</script>
親コンポーネント (Parent.vue)
- 名前付き slot に差し込むときは
#slotNameを使用 - slot props を受け取る場合は
{ prop }の形で受け取れる
<template>
<Card>
<!-- header slot に差し込む -->
<template #header>
<h3>親が差し込んだヘッダー</h3>
</template>
<!-- body slot に差し込む -->
<template #body>
<p>親が差し込んだ本文</p>
</template>
<!-- footer slot に差し込む(slot props を受け取る) -->
<template #footer="{ info }">
<p>親が差し込んだフッター - 子からの情報: {{ info }}</p>
</template>
</Card>
</template>
<script setup>
import Card from "./Card.vue";
</script>
Props vs Emit vs SlotProps
| 項目 | データの方向 | 受け取り方 / 使い方 | どういうものか | 使い分けの目安 |
|---|---|---|---|---|
| Props | 親 → 子 | 子で defineProps で受け取る |
親から子に渡す 初期データや設定 |
子の初期状態や設定値を渡したいとき |
| Emit | 子 → 親 | 親で @event="handler" で受け取る |
子から親に送る イベント通知 |
子の操作や状態変化を親に知らせたいとき |
| Slot Props | 子 → 親(slot 内のみ) | 親で <template #slot="{ prop }"> で受け取る |
子の状態や値を親に渡して 表示や処理に利用 | 子の値を親で表示したり加工したいとき |
Discussion