はじめに
本記事では、JavaScriptフレームワークの一つであるVue.jsの公式チュートリアルを実行していく中で学んだことを記事にまとめていきたいと思います。
前置きが少しあるのでチュートリアルのコードから見ていきたい方はコチラから読み進めてください!
最初に、参考にした公式サイトを載せておきます。
実行方法について
公式のクイックスタートページにてチュートリアル環境以外に3つの実行環境が提供されていましたので紹介します。
Playground
JSFiddle
StackBlitz
2、3番目の環境での実行方法がよくわからなかったので操作なしで変更内容が反映される1番のPlaygroundが初学者にはお勧めです。私も1番で学習を進めていきます。
もしローカルマシン上で動かしたい方がいらっしゃればサイトの下部に説明があったので参考にしてください!
Vue.jsの主な機能
これから出てくる言葉は、まだ全て理解できなくても良いと公式から温かい言葉をいただいたので補足程度に調べたことを付け加えようと思います
宣言的レンダリング
Vue では、標準的な HTML を拡張したテンプレート構文を使って、HTML の出力を宣言的に記述することができます。この出力は、JavaScript の状態に基づきます。
テンプレート構文を使用して、データをHTMLに結びつけることを意味します。テンプレート内でデータバインディングを行うことで、複雑なアプリケーションでもコードがシンプルになります。
レンダリング:
デジタル情報を視覚的に表示するプロセスを指します。これは、Webページやアプリケーションにおいて、データやコードを視覚的に表示するために不可欠なプロセスです。
リアクティビティー
Vue は JavaScript の状態の変化を自動的に追跡し、変化が起きると効率的に DOM を更新します。
この機能により、開発者は状態管理やDOM操作のコードを書く必要が減り、アプリケーションの開発が簡素化されるそうです。
シングルファイルコンポーネント(SFC)とは
SFCとは、HTML、JavaScript、CSSを一つのファイルにまとめて記述できる形式です。これにより、コンポーネントごとに自己完結したコードを管理しやすくなります。
SFCは3つの主要なセクションで構成されています。
-
<template>
セクション
ここではコンポーネントのHTMLテンプレートを定義します。
-
<script>
セクション
ここでは、コンポーネントのロジック(データ、メソッド、ライフサイクルフックなど)を記述します。
-
<style>
セクション
ここでは、コンポーネントのスタイルを定義します。スコープ付きスタイルを適用することもできます。
APIスタイル
Vue コンポーネントを作成する際は、Options API
、そしてComposition API
と呼ばれる 2 種類の異なる API スタイルが利用できます。
これから順に二つの特徴を説明していきます。
Options API
Options API では、data、methods、mounted といった数々のオプションからなる 1 つのオブジェクトを用いてコンポーネントのロジックを定義します。
定義されたプロパティには、コンポーネントのインスタンスを指す this を使って、関数内からアクセスできます。
<script>
export default {
// data() で返すプロパティはリアクティブな状態になり、
// `this` 経由でアクセスすることができます。
data() {
return {
count: 0
}
},
// メソッドの中身は、状態を変化させ、更新をトリガーさせる関数です。
// 各メソッドは、テンプレート内のイベントハンドラーにバインドすることができます。
methods: {
increment() {
this.count++
}
},
// ライフサイクルフックは、コンポーネントのライフサイクルの
// 特定のステージで呼び出されます。
// 以下の関数は、コンポーネントが「マウント」されたときに呼び出されます。
mounted() {
console.log(`The initial count is ${this.count}.`)
}
}
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
Composition API
Composition APIでは、インポートした各種 API 関数を使ってコンポーネントのロジックを定義していきます。
SFC において、Composition API は通常、<script setup>
と組み合わせて使用します。
このタグになっていることがComposition APIを使用していることを示します。
<script setup>
import { ref, onMounted } from 'vue'
// リアクティブな状態
const count = ref(0)
// 状態を変更し、更新をトリガーする関数。
function increment() {
count.value++
}
// ライフサイクルフック
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
宣言的レンダリング
宣言的レンダリング(declarative rendering)とは、アプリケーションのUIを宣言的に記述することで、データとビュー(UI)の関係をシンプルに表現する方法です。
JavaScript の状態に基づいて HTML がどのように見えるかを記述することができます。
早速ソースコードを見ていきます
<script>
export default {
data() {
return {
message: 'Hello',
}
}
}
</script>
<template>
<h1>{{ message + ' World!' }}</h1>
</template>
message
プロパティはテンプレート内で使用可能です。このように、mustaches構文を使い、 message の値に基づいた動的なテキストをレンダリングすることができます。
属性バインディング
Vueでは、動的な値を属性にバインドするのに、v-bind
ディレクティブを使います。
ディレクティブはv-
から始まる特別な属性であり、テンプレート構文内で使用されます。
特にv-bind
は頻繁に使用されるため省略記法が存在します。
<script>
export default {
data() {
return {
titleClass: 'title'
}
}
}
</script>
<template>
<!-- ココ -->
<h1 v-bind:class="titleClass">Make me red</h1>
<!-- 省略すると -->
<h1 :class="titleClass">Make me red</h1>
</template>
<style>
.title {
color: red;
}
</style>
これを実行すると2行赤い文字で「Hello World!」と出力されます。
v-bind
ディレクティブは、要素のclassという属性を、コンポーネントが持つtitleClassというプロパティと同期させるよう Vue に指示しています。
イベントリスナー
v-on
ディレクティブを使うことでDOMイベントを登録時やイベント発火時にいくつかの JavaScriptを実行することができます。
こちらも@
に省略することができます。使い方はv-on:click="handler"
、あるいは省略して@click="handler"
として使用します。
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
メソッドの中では、this を使ってコンポーネントインスタンスにアクセスしています。
フォームバインディング
ここではまず
「v-bind
+ v-on
= v-model
」
を覚えてください。
両辺とも意味は等しく、input要素に双方向(two-way)バインディングを作成することができます。
難しく聞こえますが、入力されたものをテキストとして出力するときに使います。
<script>
export default {
data() {
return {
text: ''
}
},
methods: {
onInput(e) {
this.text = e.target.value
}
}
}
</script>
<template>
<!-- ココ -->
<input :value="text" @input="onInput" placeholder="Type here">
<!-- ココ:どちらも意味は一緒で下の場合メソッドはいらない -->
<input v-model="text" placeholder="Type here">
<p>{{ text }}</p>
</template>
覚える量は増えますが、よく使われているので覚えてしまった方がコードも短く書くことが出来るのでお勧めだと思います。
条件付きレンダリング
v-if
ディレクティブは、ブロックを条件に応じてレンダリングしたい場合に使用されます。ブロックは、ディレクティブの式が真を返す場合のみレンダリングされます。
v-if
に対して "else block" を示すために、v-else
ディレクティブを使用できます
v-else-if
は、名前が示唆するように、v-if
の "else if block" として機能します。
<script>
export default {
data() {
return {
awesome: true
}
},
methods: {
toggle() {
//論理値の反転
this.awesome = !this.awesome
}
}
}
</script>
<template>
<button @click="toggle">Toggle</button>
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>
</template>
ここではトグルボタン機能の実現のためにv-if
のみを使用しています。
リストレンダリング
配列に基づいて項目のリストをレンダリングするには、v-for
ディレクティブを使用します。
v-for
ディレクティブでは、item in items
という形式の特別な構文が必要になります。ここで、items
は元のデータの配列を指し、item
は反復処理の対象となっている配列要素のエイリアスを指します
JavaScriptでいう
for Each
文に該当します。
では、リスト一覧を表示するサンプルコードを見てみます。
<script>
// give each todo a unique id
let id = 0
export default {
data() {
return {
newTodo: '',
<!--リスト-->
todos: [
{ id: id++, text: 'Learn HTML' },
{ id: id++, text: 'Learn JavaScript' },
{ id: id++, text: 'Learn Vue' }
]
}
},
methods: {
addTodo() {
<!--追加ボタンのメソッド-->
this.todos.push({ id: id++, text: this.newTodo })
this.newTodo = ''
},
removeTodo(todo) {
<!--削除ボタンのメソッド-->
this.todos = this.todos.filter((t) => t !== todo)
}
}
}
</script>
<template>
<form @submit.prevent="addTodo">
<!--入力フォーム-->
<input v-model="newTodo" required placeholder="new todo">
<button>Add Todo</button>
</form>
<!--リスト一覧の表示-->
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
<button @click="removeTodo(todo)">X</button>
</li>
</ul>
</template>
- required
HTMLの標準属性です。この属性が付いていると、フォームの送信時にこの入力フィールドが空白であってはならないことを示します。ユーザーが入力を行わなければ、フォームは送信されず、ブラウザが警告を表示します。
v-for と v-if を組み合わせ、同じノードに両方が存在する場合、v-for よりも v-if のほうが優先順位が高くなることに注意してください。
算出プロパティー
他のプロパティーからリアクティブに算出されたプロパティーを computed オプションを使用して宣言することができます。
状態に基づいて異なるリスト項目をレンダリングするために用います。
<script>
let id = 0
export default {
data() {
return {
newTodo: '',
hideCompleted: false,
todos: [
{ id: id++, text: 'Learn HTML', done: true },
{ id: id++, text: 'Learn JavaScript', done: true },
{ id: id++, text: 'Learn Vue', done: false }
]
}
},
computed: {
// 算出プロパティ実装部分
filteredTodos() {
return this.hideCompleted
//論理値を逆にする
? this.todos.filter((t) => !t.done)
: this.todos
}
},
methods: {
addTodo() {
this.todos.push({ id: id++, text: this.newTodo, done: false })
this.newTodo = ''
},
removeTodo(todo) {
this.todos = this.todos.filter((t) => t !== todo)
}
}
}
</script>
<template>
<form @submit.prevent="addTodo">
<input v-model="newTodo" required placeholder="new todo">
<button>Add Todo</button>
</form>
<ul>
<li v-for="todo in filteredTodos" :key="todo.id">
<input type="checkbox" v-model="todo.done">
<span :class="{ done: todo.done }">{{ todo.text }}</span>
<button @click="removeTodo(todo)">X</button>
</li>
</ul>
<button @click="hideCompleted = !hideCompleted">
{{ hideCompleted ? 'Show all' : 'Hide completed' }}
</button>
</template>
<style>
.done {
text-decoration: line-through;
}
</style>
メソッドとして実現しても良いのですが、算出プロパティはリアクティブな依存関係にもとづきキャッシュされるため依存関係が更新されたときにだけ再評価されます。
正直ここのパートは私もよく理解できなかったので公式サイトを参照することをお勧めします。
ライフサイクルとテンプレート参照
これまで紹介してきたソースコードでは、リアクティビティーと宣言的レンダリングのおかげですべての DOM 更新を処理してくれました。しかし、DOMを手動で操作する必要のある場合があります。
コンポーネントがマウントされた後にref
属性を用いてテンプレート内の特定の要素やコンポーネントに直接アクセスすることができます。
ref は、要素や子コンポーネントへの参照を登録するために使用します。
<script>
export default {
mounted() {
this.$refs.pElementRef.textContent = 'mounted'
}
}
</script>
<template>
<p ref="pElementRef">Hello</p>
</template>
ソースコードでは、コンポーネントがDOMにマウントされると(mounted フックが呼ばれる)、mounted() メソッドが実行されテキストコンテンツを書き換えています。
各 Vue コンポーネントインスタンスは、生成時に一連の初期化を行います - 例えば、データ監視のセットアップ、テンプレートのコンパイル、インスタンスの DOM へのマウント、データ変更時の DOM の更新が必要になります。その過程で、ライフサイクルフックと呼ばれる関数も実行され、ユーザーは特定の段階で独自のコードを追加することが可能です。
ウォッチャー
ウォッチャーを用いることで数値が変化したらコンソールにログを記録するなど、 プロパティの変更を監視することが出来ます。
ソースコードを見てみます。
<script>
export default {
data() {
return {
todoId: 1, // 取得するToDoのID
todoData: null // 取得したToDoデータを格納する変数
}
},
methods: {
async fetchData() {
this.todoData = null; // データ取得前にtodoDataを初期化
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos/${this.todoId}`
); // JSONPlaceholderからToDoデータを取得
this.todoData = await res.json(); // 取得したデータをtodoDataに格納
}
},
mounted() {
this.fetchData(); // コンポーネントがマウントされたときにデータを取得
},
watch: {
todoId() {
this.fetchData(); // todoIdが変更されたらデータを再取得
}
}
}
</script>
<template>
<p>Todo id: {{ todoId }}</p> <!-- 現在のToDoのIDを表示 -->
<button @click="todoId++" :disabled="!todoData">Fetch next todo</button> <!-- ToDoのIDをインクリメントするボタン -->
<p v-if="!todoData">Loading...</p> <!-- データ取得中のローディングメッセージ -->
<pre v-else>{{ todoData }}</pre> <!-- 取得したToDoデータを表示 -->
</template>
watch
オプションでは、todoId の変更を監視します。
todoId が変更されると、fetchData メソッドを呼び出して、新しい todoId に対応するToDoデータを取得します。
コンポーネントの利用方法
親コンポーネントは、そのテンプレートにある別のコンポーネントを子コンポーネントとしてレンダリングすることができます。
子コンポーネントを使用するには、まずそれをインポートする必要があります。次に、components オプションを使用して、コンポーネントを登録します。
ではソースコードを見てみましょう
<template>
<h2>A Child Component!</h2>
</template>
<script>
import ChildComp from './ChildComp.vue'
export default {
components: {
ChildComp
}
}
</script>
<template>
<ChildComp />
</template>
ここでは、オブジェクトプロパティのショートハンドを使って、 ChildComp コンポーネントを ChildComp キーの下に登録しています。
props
子コンポーネントは、親コンポーネントから props を介して入力を受け取ることができます。そして親は属性と同じように、props を子に渡すことができます。動的な値を渡すには、v-bind という構文も使えます。
<script>
export default {
props: {
msg: String // 親コンポーネントから受け取るプロパティ 'msg' の型を String として定義
}
}
</script>
<template>
<h2>{{ msg || 'No props passed yet' }}</h2>
</template>
<script>
import ChildComp from './ChildComp.vue'
export default {
components: {
ChildComp
},
data() {
return {
greeting: 'Hello from parent'
}
}
}
</script>
<template>
<ChildComp :msg="greeting" />
</template>
親コンポーネントの data オブジェクトには greeting というプロパティがあり、これが子コンポーネントの msg プロパティに渡されます。
イベントの発行
子コンポーネントはprops を受け取るだけでなく、親コンポーネントにイベントを発行することが出来ます。
<script>
export default {
emits: ['response'], // 'response' イベントを親コンポーネントに送信することを宣言
created() {
this.$emit('response', 'hello from child'); // 'response' イベントを 'hello from child' メッセージとともに送信
}
}
</script>
<template>
<h2>Child component</h2>
</template>
<script>
import ChildComp from './ChildComp.vue'
export default {
components: {
ChildComp
},
data() {
return {
childMsg: 'No child msg yet'
}
}
}
</script>
<template>
<!-- childMsgの書き換え -->
<ChildComp @response="(msg) => childMsg = msg" />
<p>{{ childMsg }}</p>
</template>
スロット
props を経由したデータの受け渡しだけでなく、親コンポーネントはテンプレートフラグメントを スロット を経由して子コンポーネントへ渡すこともできます。
slot
要素を利用して親コンポーネントからのスロットコンテンツをレンダリングします。
こちらも書いたはいいものの理解できていないので詳しく知りたい方はコチラを参考にしてください。
ソースコードを見てみます
<template>
<slot>Fallback content</slot>
</template>
<script>
import ChildComp from './ChildComp.vue' // 子コンポーネントをインポート
export default {
components: {
ChildComp // 子コンポーネントを登録
},
data() {
return {
msg: 'from parent' // 親コンポーネントのメッセージデータ
}
}
}
</script>
<template>
<ChildComp>Message: {{ msg }}</ChildComp>
</template>
処理の流れを説明していきます。
1.親コンポーネントが初期化され、データオブジェクト msg に 'from parent' がセットされます
2.親コンポーネントのテンプレート内で Message: {{ msg }} と記述することで、子コンポーネントのスロットに Message: from parent というコンテンツが渡されます。
3.子コンポーネント ChildComp はスロットを使って親コンポーネントから渡されたコンテンツ Message: from parent を表示します。
さいごに
本記事では、Vue.jsのチュートリアルを用いて学んだことを記述しました。今回の学習内容のみでは自力ではソースコードを記述できなくても、他人が書いた簡単なソースコードの意味をギリ解釈できるかもというのが正直な感想です。
チュートリアルに関する内容はすべて書いたつもりですが、これからはメインガイドを用いて学習したより詳しいVueの内容を投稿できればなと思います!