当記事はVue.js 3.3の入門記事です。
私がVue.jsを触り始めたころの記憶を頼りに「最低限これだけ知っておけばQiitaとかStack Overflowを利用して自走できるだろう」という程度の範囲・量に絞って解説します。
執筆にあたって用いた動作確認用環境は以下のとおりです。
- Ubuntu 22.04.3 LTS
- Apache HTTP Server 2.4.52-1ubuntu4.7
- Vue.js 3.3.12
概要
Vue.jsはDOMを「コンポーネント指向」という考え方にもとづいて構築・操作するために必要な値・関数を提供することに特化したライブラリです。
事実上のフロントエンド専用ライブラリであり、バックエンドエンジニアの方などがVue.jsを触るようなことはまずないと思います。
当初はJavaScriptで実装して、それをミニファイなどして公開というかたちが採られていたようですがバージョン3.0.0からはTypeScriptで実装されるようになりました。
ただ、ソースコードを落としてきて、ローカルでJavaScriptにトランスパイルしてから利用する……といった手間は不要です。ちゃんとトランスパイルされたものが公開されていますので、ご安心ください。
開発背景
Vue.jsはGoogleのAngularやMetaのReactと並ぶ人気のフロントエンドライブラリですが、GoogleやMetaといった大企業が開発を始めたライブラリではなく「Evan You」というエンジニアの個人成果物としてスタートしたという特徴があります。
Evan You氏はかつてGoogleで勤務されていたのですが、その折にプロジェクトでAngularに触れて感銘を受けたそうです。
ただ、Angularは結構重い――大規模開発向けのライブラリということで、同氏が考えるAngluarの良いところだけを厳選して抽出した「軽量版Angular」「小規模開発向けAngular」のようなものを目指して開発が始まったそうです。
それが現在では、同氏とVue.jsコミュニティによって日夜改良が進められるほどの大規模なライブラリにまで発展しました。
略歴
- 2015年10月27日
- Vue.js 1.0.0リリース
- 当初はJavaScriptで記述
- 2016年09月30日
- Vue.js 2.0.0リリース
- 2020年09月18日
- バージョン3.0.0リリース
- バージョン3.0.0以降はTypeScriptで記述
- 2023年05月11日
- バージョン3.3.0リリース
特徴・用途
Vue.jsの特徴や用途などについて、もうすこし詳しく説明します。
コンポーネント指向
先述のとおり、Vue.jsはコンポーネント指向という考え方にもとづいてDOMを構築・操作することに特化したライブラリです。
コンポーネント指向とはシステム全体や一定のプログラム範囲を「コンポーネント」という単位で分割して捉える考え方です。なんだか難しく聞こえるかもしれませんが、これはちょうどオブジェクト指向プログラミングがオブジェクトという単位でプログラム・システムを分割して捉えるのと似たようなものです。
ただ、ここでいうコンポーネントが具体的にどういうものを指すのかは明確に定義されているわけではないようで、個々の媒体によって微妙に定義が揺れている印象です。参考までに、Wikipediaの記事を貼っておきます。
とはいえ、それら諸定義においても共通した主張はあります。それは「コンポーネントとはそれ単体である程度機能し、一定の役割を担えるプログラムの集合である」という点です。また、コンポーネント(部品)という名前のとおり「あるコンポーネントを、機械の部品を取り換えるように別のコンポーネントに置換できる(しやすい)」という点もコンポーネントに求められている要素のようです。
上記主張から、どういうものをコンポーネントと見なしているかがなんとなく伝わってくるかと思います。個人的には、マイクロサービスにおける各種サービスはまさにコンポーネントの代表例だと思いますし、Vue.jsを始めとするライブラリと呼ばれるプログラム群もコンポーネントに該当すると考えております。
もう1つ述べておくと、オブジェクト指向におけるオブジェクトというのがソースコードという地盤上に構築される概念なのに対して、コンポーネント指向におけるコンポーネントはもう少し上位のレイヤーに属している印象です。実装段階における概念ではなく、設計段階における概念の1つがコンポーネントであるように思われます。よって、システムをコンポーネント指向にもとづいて設計し、オブジェクト指向にもとづいて実装するのは普通に成立すると考えています。
Vue.jsにおけるコンポーネント指向
ひるがえって、Vue.jsにおけるコンポーネントとは「Webページを構成する各種UI部品を形成しているHTML・CSS・JavaScriptを1つのJavaScriptオブジェクトにまとめたもの」を指します。
具体例を挙げます。以下要件を満たすUI部品を、Vue.jsのコンポーネントとして実装するとどうなるか、具体的なソースコードを掲載します。
-
<button>
タグである - ラベルは32px・赤文字で「0」と記載
- クリックするたびにラベルの数字が1ずつ大きくなる
// こんな感じで実装できます。
const CounterButton = {
data: function () {
return {
count: 0
};
},
render: function () {
return [
Vue.h(
"button",
{
onClick: function () {
this.count += 1;
}.bind(this),
style: {
color: "red",
fontSize: "32px"
}
},
this.count
)
];
}
};
今の時点では上記実装の意味がわからなくても大丈夫です。上記のように記述すれば、先述の要件を満たすUI部品を構築することができるということだけ理解しておいてください。
Vue.jsではWebページを構成するUI部品を上記のようなコンポーネントというかたちで定義して、それらコンポーネントを組み合わせることで画面を構築するという考え方を採用しています。
文書構造を.htmlファイルに、見た目や装飾周りを.cssファイルに、動きを付けたり制御するのは.jsファイルにそれぞれ分けて記述するWeb標準のやり方とは全然違うことがわかります。そういう意味では、Vue.jsにおける「コンポーネント指向」は「UI部品指向」と呼び変えたほうが理解しやすいですし、実態を捉えた表現かもしれませんね。
Vue.jsの利点と欠点
WebページをUI部品単位で分けて定義・操作するVue.js流DOM制御方法の利点と欠点は何でしょうか。
正直なところ、私はこの問いに対して答えることができません。私がまだまだ未熟というのもあるのでしょうが、この方法はWeb標準の実装方法よりも便利といえば便利ですし、不便といえば不便です。ハッキリ言って、好みの問題であるように思います。
ただ、UI部品を.jsファイル内で動的に生成・挿入する機会が多い動的なWebコンテンツであるほど、Vue.jsとの相性は良いと感じました。
逆に、.htmlファイル上で定義したDOM構造が大きく変化しない静的なWebコンテンツほどVue.jsとの相性は良くない気がします。好みの問題でしょうが、そういった静的なWebコンテンツをVue.jsで実装するのは個人的にはあまり賛成できません。
まぁ、私の意見を聞くよりも、実際に使ってみて利点・欠点を掴んでいただくほうが良いかと思いますので、このあたりで当項は締めたいと思います。
バージョン2.xと3.xで仕様が異なる
日本のIT業界はVue.jsのバージョン2.x系からバージョン3.x系への過渡期にある印象です。
バージョン2.x系とバージョン3.x系ではいろいろと仕様が異なります。ネット上や書籍などでVue.jsについて学ぶときは、その資料で扱われているバージョンを意識するようにしてください。
this
を多用する
Vue.jsでは値・関数を this
を介して参照する機会が非常に多いです。そのため、関数を定義するときは function
文で定義するか =>
でアロー関数として定義するかで挙動に差異が生じたりします。その点にご注意ください。
個人的には全て function
で統一するのが綺麗だと考えています。よって、当記事内では原則として function
で統一しています。
書き方がいくつかある
Vue.jsは、ある処理を実装するにあたって、いくつかの書き方ができるようになっています。冗長な書き方から、スマートな書き方まで用意されています。
これはユーザーのことを考えての配慮なのでしょうが、Vue.js入門者からすると混乱を招くだけになっているように感じています。
当記事内にて説明している書き方においても、それとは違った書き方が用意されていることが多いので必ずVue.js公式サイトのAPIリファレンスなどに一度は目を通すようにしてください。
導入手順
Vue.jsの導入手順について解説します。
npm経由で導入
Vue.jsにはいくつかの導入経路が用意されています。CDNで読み込んだり、Vue CLIやViteといった環境構築ツールを使う手もあります。
npmでインストールする場合は以下のとおりです。
$ npm install vue
公式サイトの導入手順説明ページも参照ください。
4種類のビルド
Vue.jsには大きく4種類のビルドがあります。
1つ目は「グローバル版」とでも呼べるものです。 vue.global.js
のように、ファイル名のどこかに「global」という語句が含まれているのが特徴です。
当該ファイルを <script>
タグで読み込むとグローバル空間に Vue
というオブジェクトが登録されますので、左記オブジェクトを介してVue.jsのAPIを利用します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta content="initial-scale=1.0, width=device-width" name="viewport">
<title>Vue.js 3.2入門</title>
<!--
ここでVue.jsを読み込みます。
-->
<script src="./lib/vue.global.js"></script>
<!--
Vue.js読み込み後に自前のスクリプトを読み込みます。
-->
<script defer src="./main.js"></script>
</head>
<body></body>
</html>
// オブジェクト「Vue」を介してAPIを利用します。
const app = Vue.createApp(...);
app.mount(...);
2つ目は「ESモジュール版」とでも呼べるものです。 vue.esm-browser.js
のように、ファイル名のどこかに「esm-browser」という語句が含まれているのが特徴です。 import
文で読み込んで使います。
// 使いたい値・関数をインポートします。
import {
createApp
} from "./vue.esm-browser.js";
const app = createApp(...);
app.mount(...);
// 1つのオブジェクトに値・関数をまとめるかたちでインポートするのもアリです。
import * as Vue from "./vue.esm-browser.js";
const app = Vue.createApp(...);
app.mount(...);
3つ目は「バンドラー版」とでも呼べるものです。 vue.esm-bundler.js
のように、ファイル名のどこかに「esm-bundler」という語句が含まれているのが特徴です。ESモジュール版と同様に import
文で読み込んで使います。
一見すると、ESモジュール版とバンドラー版は同じビルドであるように思えますが、後述する「Single File Component(SFC)」という仕組みを利用するときはバンドラー版でないと正常に動作しませんのでSFCを使いたいときはバンドラー版を使ってください。
// 使いたい値・関数をインポートします。
import {
createApp
} from "./vue.esm-bundler.js";
const app = createApp(...);
app.mount(...);
// 1つのオブジェクトに値・関数をまとめるかたちでインポートするのもアリです。
import * as Vue from "./vue.esm-bundler.js";
const app = Vue.createApp(...);
app.mount(...);
基本的には上記3種類のいずれかのビルドを使うことになります。あとはCommonJS規格で動作するように調整されたビルドもあるのですが、いまいち使う機会がよくわからないので説明は割愛します。
開発環境向けと本番環境向け
ファイル名に「prod」という語句が含まれているものは本番環境向けビルド、含まれていないものは開発環境向けビルドとなります。
本番環境向けはミニファイされているほか、実装に際しての警告通知機能が省略されています。
この警告通知機能は「とりあえず動くけど、今の実装だとパフォーマンスが悪いよ?」とか「定義されたバリデーション処理でエラーが起きてます」といったことを通知してくれる便利機能です。
この警告によってVue.jsの仕様を学べる側面もありますので、最初のうちは開発環境向けビルドを使うようにしてください。
テンプレート機能の有無
ファイル名に「runtime」という語句が含まれているものはテンプレート機能非搭載ビルド、含まれていないものはテンプレート機能搭載ビルドとなります。
Vue.jsには「テンプレート」という機能があり、それを利用する場合はテンプレート機能が含まれたビルドを選択する必要があります。ただ、テンプレート機能搭載ビルドはテンプレート機能のぶんだけファイルサイスが大きくなっています。
なお、バンドラー版は常にテンプレート機能が搭載されています。
まとめ
以上を踏まえて、各環境・実装者向けにどのようなビルドを選ぶべきかを簡単にまとめます。
まず「Vue.jsのこと、よくわからないからとりあえず触ってみるか!」という人はエラー通知機能とテンプレート機能が搭載されたグローバル版ビルド vue.global.js
をオススメします。npmなどでインストールして利用するのもよいですがCDNで持ってくるのがお手軽です。
Vue.jsの扱いに慣れてきたらwebpackを始めとするバンドラーとともにnpmなどでインストールして、バンドルして使ってみましょう。そのさいは vue.esm-bundler.js
の仕様を推奨します。
基本的な使いかた
Vue.jsの基本的な操作や仕様について解説します。
以下のようなディレクトリ構造で、グローバル版ビルド vue.global.js
を読み込んで動かしている状況想定で解説を進めます。
■ ※適当なディレクトリ
├■ lib
│└■ vue.global.js
├■ index.html
└■ main.js
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta content="initial-scale=1.0, width=device-width" name="viewport">
<title>Vue.js 3.3入門</title>
<script src="./lib/vue.global.js"></script>
<script defer src="./main.js"></script>
</head>
<body></body>
</html>
// このファイルにVue.jsの処理を書いていきます。
コンポーネントの基本仕様
Vue.jsについては説明しなければならないことが多いのですが、とりあえずコンポーネントの基本仕様から説明しようと思います。
Vue.jsはDOMをコンポーネントという単位で分割して管理・制御します。
コンポーネントはHTML・CSS・JavaScriptをひとまとめにしたものですが、その実態はただのオブジェクトです。コンポーネントを構成する各種情報をオブジェクト内にプロパティとして定義しておくと、Webページ読み込み時にVue.jsがそれら設定を読み込んで画面上に描画してくれます。
例えば、文書構造を設定する場合はプロパティ render
に関数を渡し、その関数内からコンポーネントの文書構造を定義した値をreturnします。
// 赤字で「Hello world.」と表示するように設定したコンポーネントです。
const Component = {
render: function () {
return [
Vue.h(
"p",
{
style: {
color: "red"
}
},
["Hello world."]
)
];
}
};
// 適当に<textarea>タグを置いてみます。
const Component = {
render: function () {
return [
Vue.h("textarea", {}, [null])
];
}
};
まるで設定ファイルを弄るかのようにDOMを定義・制御できることから、このような仕組みを「宣言的レンダリング」と言います。
アプリケーション
コンポーネントの基本について触れましたので、つづけて詳細な説明に移ろうかと思いましたが、その前に「アプリケーション」について説明させてください。ここでいうアプリケーションとは一般的な意味でのアプリケーションではなく、Vue.js固有概念としてのアプリケーションです。
何度も説明しているように、Vue.jsはコンポーネントを組み合わせてDOMを構築します。コンポーネントの実態はオブジェクトであり、オブジェクト内にコンポーネントの挙動を決める値を定義していきます。
ところで、ここで素朴な疑問にぶつかります。コンポーネントはオブジェクトでしかありませんが、コンポーネントに定義した内容をどのようにしてDOMに反映させればよいのでしょうか。ECMAScriptであれば関数 Node.appendChild
などでDOMに挿入できますが、Vue.jsのコンポーネントは Element
オブジェクトや HTMLElement
オブジェクトではありませんのでECMAScriptの関数を使ってDOMに反映することはできません。
コンポーネントに定義した内容をDOMに反映させるには、DOMに反映させたいコンポーネントを「アプリケーション」に定義し、アプリケーションを任意のHTML要素に「マウント」する必要があります。
まず、Vue.jsの関数 createApp
の第1引数に適当なコンポーネントを渡します。すると戻り値としてアプリケーションが返ってきます。
// 適当なコンポーネントです。
const Component = { ... };
// 関数「createApp」に渡すとアプリケーションが生成されます。
const application = Vue.createApp(Component);
このアプリケーションには関数 mount
が実装されています。関数 mount
の引数にコンポーネントで定義したDOMを挿入したいHTML要素を指定することで、そのHTML要素直下にコンポーネントで定義したDOMが挿入されます。
const Component = { ... };
const application = Vue.createApp(Component);
// id「foo」が指定されたHTML要素直下に挿入します。
const element = docuemnt.querySelector("#foo");
application.mount(element);
指定するHTML要素はなんでもよいですが、Vue.jsを使ってWebコンテンツを構築する場合というのはフロントエンド全体をVue.jsで制御するのが一般的かと思います。そのためタグ <body>
を指定するのが基本です。
const Component = { ... };
const application = Vue.createApp(Component);
application.mount(document.body);
関数 createApp
に渡すコンポーネントはイチイチ変数に格納せずとも、引数部分で定義してもOKです。また、生成したアプリケーションを変数に代入せず、関数 createApp
にチェーンして関数 mount
を記述しても問題ありません。
// 見本として、適当なコンポーネントを渡しています。
Vue.createApp({
render: function () {
return [Vue.h("p", {}, ["こんな感じに実装できます。"])];
}
}).mount(document.body);
なんだか面倒な手順を踏んでいる気もしますが、クラスを利用するときに new
でインスタンスを生成してから触っていることを思えば、まぁ、こういうものかという感じですね。
なお、関数 createApp
に渡すことができるコンポーネントは1つです。そのため左記のようなコンポーネントを「ルートコンポーネント」と言います。意識する場面はあまりないでしょうが知識として覚えておいてください。
文書構造の定義とVノード (1 / 2)
アプリケーションについて説明しましたので、今度こそコンポーネントの詳細な仕様などについて解説していきます。
まず紹介するのは、基本中の基本といえる文書構造の定義方法です。
上で軽く触れましたが、文書構造を定義する場合はプロパティ render
を利用します。プロパティに関数を渡し、そのなかから文書構造を表した値をreturnします。
const Component = {
render: function () {
// この関数内から文書構造を表した値をreturnしてください。
}
};
文書構造は次のように作ります。
まず配列を用意します。
const Component = {
render: function () {
// 文書構造です。
const dom = [];
return dom;
}
};
この配列内に「Vノード」を格納します。
VノードとはVue.jsでのみ使われるHTML要素です。といっても、そんなに身構えるようなものではありません。jQueryでいうjQueryオブジェクトのように、Vue.jsで扱えるように拡張したHTML要素くらいに捉えていただいてけっこうです。
ECMAScriptでHTML要素を生成するには関数 createEelement
を使用しますが、Vue.jsでVノードを生成する場合は関数 h
を使用します。ずいぶんとシンプルな名前ですが、これはたびたび呼び出されるため、あえてシンプルな名前にしている……みたいな理由で命名されていた気がします。
const vNode = Vue.h();
第1引数に生成したいHTML要素のタグ名を文字列で指定します。
// <p>タグ型のVノードです。
const vNodeOfP = Vue.h("p");
// <div>タグ型のVノードです。
const vNodeOfDiv = Vue.h("div");
第2引数にはVノードに適用する設定などをオブジェクト形式で指定しますが、そのあたりはややこしいので後述します。
// こんな感じです。
// あとで詳しく説明します。
const vNode = Vue.h("p", {});
第3引数にはVノードの子要素を指定します。配列を指定し、そのなかに子要素たるノードを指定していきます。基本的にはテキストノードかHTMLノードになるかと思います。それぞれの指定方法は以下のとおりです。
// <div>Hello world.</div>
const vNode1 = Vue.h("div", {}, ["Hello world."]);
// <ul>
// <li>1</li>
// <li>2</li>
// </ul>
const vNode2 = Vue.h("ul", {}, [
Vue.h("li", {}, "1"),
Vue.h("li", {}, "2")
]);
ここまでの手順で生成したVノードをプロパティ render
に指定した関数内からreturnすることで文書構造の定義は完了です。以下は見本です。
// <header>ヘッダー</header>
// <main>
// <p>ボタンを押してください</p>
// <button>ボタン</button>
// </main>
// <footer>ヘッダー</footer>
Vue.createApp({
render: function () {
return [
Vue.h("header", {}, "ヘッダー"),
Vue.h("main", {}, [
Vue.h("p", {}, "ボタンを押してください"),
Vue.h("button", {}, "ボタン")
]),
Vue.h("footer", {}, "フッター")
];
}
}).mount(document.body);
文書構造の定義とVノード (2 / 2)
関数 h
の第2引数について解説します。
第2引数には、そのVノードに適用する各種設定をオブジェクト形式で記述します。
論より証拠ということで、いくつかの設定方法を列挙します。
スタイルはプロパティ style
を設け、そのなかに各CSSプロパティを設けて、設定値を文字列で指定します。JavaScriptでHTML要素のスタイルを操作するのとを使用感はほぼ同じです。
Vue.createApp({
render: function () {
return [
Vue.h("p", {
style: {
background: "rgb(221, 238, 255)",
borderRadius: "0.25rem",
boxShadow: "0 0.1rem 0.1rem",
fontSize: "2rem",
fontWeight: "bold",
margin: "1rem",
padding: "0.5rem 0.75rem",
textAlign: "center"
}
}, "カード型UI部品")
];
}
}).mount(document.body);
イベントリスナーは小文字の「on」に続けて、イベント名の1文字目を大文字にしたプロパティを用意し、そこに関数を指定します。全て小文字にするとディレクティブと見なされるので注意してください。
Vue.createApp({
render: function () {
return [
Vue.h("button", {
onClick: function (event) {
alert("ボタンが押されました");
}
}, "アラート!")
];
}
}).mount(document.body);
ディレクティブはそのまま記述します。
Vue.createApp({
render: function () {
return [
Vue.h("input", { type: "button" }, ["ボタン"]),
Vue.h("input", { type: "checkbox" }, []),
Vue.h("input", { type: "color" }, []),
Vue.h("input", { type: "date" }, []),
Vue.h("input", { type: "file" }, [])
];
}
}).mount(document.body);
コンポーネント内変数・関数
コンポーネントには、そのコンポーネント内からしか(原則として)参照できない変数と関数を持たせることができます。
変数はプロパティ data
に関数を指定し、そのなかでオブジェクトをreturnすることで定義します。
関数はプロパティ methods
にオブジェクトを指定し、そのなかに関数を持たせたプロパティを定義することで作成できます。
参照する場合はオブジェクト this
を介して参照してください。
Vue.createApp({
data: function () {
return {
message: "コンポーネント内変数はこのように定義します。"
};
},
render: function () {
return [
Vue.h("p", { style: { color: "blue" } }, this.message)
];
}
}).mount(document.body);
Vue.createApp({
methods: {
addExclamationMark: function (value) {
return value + "!";
}
},
render: function () {
return [
Vue.h("p", { style: { color: "green" } }, this.addExclamationMark("関数を呼び出す"))
];
}
}).mount(document.body);
Vue.createApp({
data: function () {
return {
message1: "関数内",
message2: "変数"
};
},
methods: {
getMessage: function (value) {
return this.message1 + "から" + this.message2 + "を呼び出す";
}
},
render: function () {
return [
Vue.h("p", {}, this.getMessage())
];
}
}).mount(document.body);
コンポーネント内でコンポーネントを利用 (1)
これまで関数 h
の第1引数には生成したいHTML要素のタグ名を指定することで、そのタグ型のVノードを生成してきました。
// <div>型のVノードです。
Vue.h("div", {}, null);
この第1引数にはコンポーネントを指定することもできます。コンポーネントを指定した場合は、そのコンポーネントがそのまま適用されるかたちになります。
const Component = {
render: function () {
return [
Vue.h("div", {}, ["Hello world."])
];
}
};
Vue.createApp({
render: function () {
return [
Vue.h(Component, {}, null)
];
}
}).mount(document.body);
コンポーネント内でコンポーネントを利用 (2)
あるコンポーネント内で別のコンポーネントを利用して文書構造を定義できるということは、汎用的なUI部品をコンポーネントとして定義しておけばいろいろなコンポーネントで使いまわせるという発想に繋がります。ただ、汎用的なUI部品といっても「完全に同じ内容・処理のUI部品」なんてものは、実務上ではほとんど存在しません。現実的に求められるのは「だいたい同じだけど細部が微妙に異なるUI部品」です。
ここまでに説明してきた方法では「完全に同じ内容・処理のUI部品」を使いまわすことはできても「だいたい同じだけど細部が微妙に異なるUI部品」を扱うことはできません。この問題を解決するには「コンポーネントを利用するコンポーネント」である「親コンポーネント」から、「コンポーネントに利用されるコンポーネント」である「子コンポーネント」に対して、何らかの値を渡す仕組みが必要です。
Vue.jsにはその仕組みが2つ用意されています。
1つ目は「スロット」です。スロットとは関数 h
の第3引数に指定した値を、関数 h
の第1引数に指定したVノード・コンポーネント内から参照する仕組み・機能のことです。オブジェクト this.$slots
で参照できます。
// 汎用的なカード型コンポーネントです。
const DefaultCard = {
render: function () {
return [
Vue.h("p", {
style: {
borderRadius: "0.5rem",
boxShadow: "0 0.1rem 0.3rem rgb(204, 204, 204)",
margin: "1rem",
padding: "0.5rem",
textAlign: "center"
}
}, this.$slots)
];
}
};
Vue.createApp({
render: function () {
return [
Vue.h(DefaultCard, {}, "カード1"),
Vue.h(DefaultCard, {}, "カード2"),
Vue.h(DefaultCard, {}, "カード3")
];
}
}).mount(document.body);
関数 h
の第3引数にVノードを指定しても参照できます。
// 汎用的なカード型コンポーネントです。
const DefaultCard = {
render: function () {
return [
Vue.h("p", {
style: {
borderRadius: "0.5rem",
boxShadow: "0 0.1rem 0.3rem rgb(204, 204, 204)",
margin: "1rem",
padding: "0.5rem",
textAlign: "center"
}
}, this.$slots)
];
}
};
Vue.createApp({
render: function () {
return [
Vue.h(
DefaultCard,
{},
Vue.h("button", {}, "ボタン1")
),
Vue.h(
DefaultCard,
{},
Vue.h("button", {}, "ボタン2")
),
Vue.h(
DefaultCard,
{},
Vue.h("button", {}, "ボタン3")
)
];
}
}).mount(document.body);
コンポーネント内でコンポーネントを利用 (3)
親コンポーネントから子コンポーネントに値を渡す2つ目の手段は、子コンポーネントのプロパティ props
に「親コンポーネントから値を受けるプロパティ名」を指定することです。
親コンポーネントは関数 h
の第2引数のオブジェクトに、子コンポーネントのプロパティ props
で指定されたプロパティを定義し、そのプロパティに子コンポーネントへ渡したい値を指定します。
以下例では、子コンポーネント DefaultCard
はプロパティ backgroundColor
に指定された値を受け取ることができるように設定しています。親コンポーネント側でも関数 h
の第2引数に、子コンポーネント側で指定されたプロパティを用意して、そこに子コンポーネントに渡したい値を指定しています。
// 汎用的なカード型コンポーネントです。
const DefaultCard = {
props: ["backgroundColor"],
render: function () {
return [
Vue.h("p", {
style: {
backgroundColor: this.backgroundColor,
borderRadius: "0.5rem",
boxShadow: "0 0.1rem 0.3rem rgb(204, 204, 204)",
margin: "1rem",
padding: "0.5rem",
textAlign: "center"
}
}, this.$slots)
];
}
};
Vue.createApp({
render: function () {
return [
Vue.h(DefaultCard, { backgroundColor: "rgb(255, 221, 238)" }, "カード1"),
Vue.h(DefaultCard, { backgroundColor: "rgb(238, 255, 221)" }, "カード2"),
Vue.h(DefaultCard, { backgroundColor: "rgb(221, 238, 255)" }, "カード3")
];
}
}).mount(document.body);
データバインディング
Vue.jsのウリの1つである「データバインディング」を紹介します。
データバインディングは「ある値を変更すると、その値を参照している値も自動的に変更される仕組み」のことです。
データバインディングの挙動を体験できる実装としては以下のようなものが挙げられます。テキストボックスに値を入力するとコンポーネント内変数 inputedValue
に入力内容を代入するのですが、値を代入するだけで <p>
タグの内容が入力内容に合わせて書き換わります。
Vue.createApp({
data: function () {
return {
inputedValue: ""
};
},
methods: {
/**
* テキストボックスに入力があったときのイベントリスナーです。
* @param {Event} event イベント情報です。
*/
onInputAtTextBox: function (event) {
this.inputedValue = event.target.value;
}
},
render: function () {
return [
Vue.h("input", { onInput: this.onInputAtTextBox, type: "text" }, null),
Vue.h("p", {}, `「${this.inputedValue}」`)
];
}
}).mount(document.body);
データバインディングは親コンポーネントから子コンポーネントへと渡された値にも適用されます。そのため親コンポーネントで値を弄ると、即座に子コンポーネントへ反映されます。
ただし、Vue.jsでは「単方向データバインディング」という考え方を採用しており、親コンポーネントの値は子コンポーネントに自動反映されますが、親コンポーネントから渡された値を子コンポーネント内で弄っても親コンポーネントには反映されません。Vue.jsを触り始めた段階だと陥りやすいポイントですのでご注意ください。
子コンポーネントから親に影響を与える
前述のとおり、Vue.jsでは親コンポーネントから子コンポーネントに影響を与えることはできても、子コンポーネントから親コンポーネントへ影響を与えることは、原則として許可されていません。
しかしながら、現実的には子コンポーネントから親コンポーネントに影響を与えたい場面は山のように存在します。そのような場面で役立つ実装方法を紹介します。
まずは動作確認用にコンポーネントを適当に作ります。1つは親コンポーネント、もう1つは子コンポーネントです。親コンポーネントをルートコンポーネントとしてアプリケーションを生成・マウントしておきます。
// 子コンポーネントです。
const Child = {};
// 親コンポーネントです。
const Parent = {};
Vue.createApp(Parent).mount(document.body);
子コンポーネントの文書構造に <textarea>
タグを定義し、子コンポーネントの <textarea>
タグで入力があった場合は親コンポーネントの <p>
タグに入力内容が反映されるようにしたいと思います。
まずは文書構造だけ定義します。この時点では <textarea>
タグに入力があっても <p>
タグには反映されません。
const Child = {
render: function () {
return [
// 子コンポーネントには<textarea>タグを定義しました。
Vue.h("textarea", {}, null)
];
}
};
const Parent = {
render: function () {
return [
// 子コンポーネントの<textarea>タグに入力された内容を反映するための<p>タグです。
// 現実装では反映できていません。
Vue.h("p", {}, null),
// 子コンポーネントも文書構造内に定義しておきます。
Vue.h(Child, {}, null)
];
}
};
Vue.createApp(Parent).mount(document.body);
ここからが本題です。子コンポーネントから親コンポーネントへ影響を与えるには、子コンポーネントから親コンポーネントへイベントを通知し、親コンポーネント側でイベントを拾う仕組みが必要です。
子コンポーネントから親コンポーネントへイベントを通知するには関数 $emit
を使用します。第1引数に発信するイベント名を文字列で指定し、第2引数にイベント受信側へ送りたい値を指定します。
今回は <textarea>
タグでイベント input
が発生したときに、カスタムイベント inputtextarea
を発信するように実装してみます。イベント情報としてイベント input
発生時点の <textarea>
タグの入力内容を付与します。
const Child = {
render: function () {
return [
Vue.h("textarea", {
// こんな感じで実装します。
onInput: function (event) {
this.$emit("inputtextarea", { value: event.target.value });
}.bind(this)
}, null)
];
}
};
const Parent = {
render: function () {
return [
Vue.h("p", {}, null),
Vue.h(Child, {}, null)
];
}
};
Vue.createApp(Parent).mount(document.body);
また、関数 $emits
でイベントを送信するときは、コンポーネント直下にプロパティ emits
を定義して、そこに送信するイベント名を記載しておきます。
const Child = {
// こんな感じです。
emits: ["inputtextarea"],
render: function () {
return [
Vue.h("textarea", {
onInput: function (event) {
this.$emit("inputtextarea", { value: event.target.value });
}.bind(this)
}, null)
];
}
};
const Parent = {
render: function () {
return [
Vue.h("p", {}, null),
Vue.h(Child, {}, null)
];
}
};
Vue.createApp(Parent).mount(document.body);
次に、受信側である親コンポーネントの実装に移りましょう。といってもやることは以下のとおりシンプルです。ここまで説明してきた内容で対応できます。
const Child = {
emits: ["inputtextarea"],
render: function () {
return [
Vue.h("textarea", {
onInput: function (event) {
this.$emit("inputtextarea", { value: event.target.value });
}.bind(this)
}, null)
];
}
};
const Parent = {
// 子コンポーネントからイベント「inputtextarea」に載せて送られてきた値を受けとるためのコンポーネント内変数を新たに定義します。
data: function () {
return {
valueOfChild: ""
};
},
render: function () {
return [
// <p>タグの子ノードに、子コンポーネントから送られてきた値を自動反映するように処理を変更します。
// 関数「h」の第3引数に、コンポーネント内変数「valueOfChild」を指定することでデータバインディングが有効化されます。
Vue.h("p", {}, this.valueOfChild),
Vue.h(Child, {
// 子コンポーネントのイベント「inputtextarea」と紐づくイベントリスナーを定義し、
// 子コンポーネントから送られてきた値をコンポーネント内変数「valueOfChild」に格納するようにします。
onInputtextarea: function (event) {
this.valueOfChild = event.value;
}.bind(this)
}, null)
];
}
};
Vue.createApp(Parent).mount(document.body);
ライフサイクルフック
Vue.jsのコンポーネントには「ライフサイクル」という概念が存在します。ライフサイクルとはそのコンポーネントが生成されてから消去されるまでの処理の流れのことです。
詳しくは上の記事などを読んでいただくとして、この場でざっくりと説明すると、コンポーネントは「生成」「表示」「更新」「削除」の順に状態が変化します。「更新」だけはコンポーネントの内容が変わるたびに発生します。
Vue.jsを触っていると上記タイミングのときにだけ動かしたい処理というのがでてきます。パッとは思いつかないのですが、とにかくVue.jsを使ってフロントエンドを弄っているとそういう欲求が生まれてきます。
そのときに役立つのが「ライフサイクルフック」という機能です。これは各ライフサイクルのタイミングで動かしたい処理を定義する機能、および左記機能によって定義された処理のことです。要するにイベントリスナーということです。
コンポーネント直下に各ライフサイクルフックを定義することで機能します。詳しくは以下ページを参照ください。
// 例えば、コンポーネントが生成された瞬間に動かしたい処理は以下のように実装します。
const Component = {
created: function () {
// ここにコンポーネント生成時にのみ動かしたい処理を書いていきます。
}
};
テンプレート機能
ここまでの説明で、ひとまずVue.jsの基礎中の基礎の部分は説明しました。ここからは少し発展した分野について取り上げます。
まずは「テンプレート機能」からです。
概要
テンプレート機能はVue.js標準機能の1つです。コンポーネントのプロパティ template
にHTML文字列を記述することで文書構造を定義できます。以下2つの実装は同じ文書構造を生成します。
// テンプレート機能を使わずに実装したパターンです。
Vue.createApp({
render: function () {
return [
Vue.h("p", { style: { color: "red" } }, "Hello world.")
];
}
}).mount(document.body);
// テンプレート機能を使って実装したパターンです。
Vue.createApp({
template: "<p style='color: red;'>Hello world.</p>"
}).mount(document.body);
プロパティ render
と template
がともにコンポーネント内に含まれている場合は render
のほうが優先して使われる点にご注意ください。
また、Vue.jsの導入手順を説明するさいに触れていますが、Vue.jsのビルドにはテンプレート機能を含んだビルドと含まないビルドがあります。ビルドのファイル名に「runtime」という文字が含まれているとテンプレート機能は非搭載です。
テンプレート機能内でコンポーネントを使う場合
プロパティ render
では関数 h
の第1引数にコンポーネントを指定するだけでコンポーネントを文書構造に組み込むことができました。対して、テンプレート機能では手順が異なります。
まず、コンポーネントにプロパティ components
を定義し、そこにオブジェクトを指定します。
const Component = {
components: {}
};
このオブジェクトのプロパティにコンポーネントを指定することで利用できるようになります。また、そのプロパティ名はテンプレート内でのコンポーネントを指すタグ名になります。以下例をご覧ください。
const ExampleComponent1 = {
render: function () {
return [
Vue.h("p", { style: { color: "red" } }, "Hello world.")
];
}
};
Vue.createApp({
components: {
"example-component-1": ExampleComponent1
},
template: "<p>1</p><example-component-1></example-component-1><p>2</p>"
}).mount(document.body);
上記のとおり、プロパティ components
でコンポーネントを登録し、プロパティ template
で参照するわけです。もちろん、プロパティ components
に指定したオブジェクトのプロパティ名は自由に指定できます。
Vue.js専用ディレクティブ
Vue.jsには専用のディレクティブが用意されています。詳しくは以下リンク先を参照していただくのがよいのですが、いくつか実装例を紹介します。
スタイルを定義する場合は v-bind:style
ディレクティブを使用します。ディテクティブの値には関数 h
の第2引数でスタイルを定義するときのオブジェクトを文字列にして渡します。文字列リテラルが3重になってしまい、シングルクォート・ダブルクォートをエスケープしなければならなくなりますので template
の値をバッククォートで囲っておくのがオススメです。
Vue.createApp({
template: `<p v-bind:style="{ color: 'blue', fontStyle: 'italic' }">123456789</p>`
}).mount(document.body);
イベントリスナーを定義する場合は v-on
ディレクティブを使います。以下は <button>
タグでイベント click
が発生したときにアラートを鳴らすようにした実装例です。
Vue.createApp({
methods: {
onClickAtButton: function (event) {
alert("ボタンが押されました!");
}
},
template: `<button v-on:click="onClickAtButton">ボタン</button>`
}).mount(document.body);
コンポーネント内変数をHTML要素のテキストノードとして利用する方法は以下のとおりです。 {{
と }}
の間にコンポーネント内変数や計算式を記述することで反映されます。
Vue.createApp({
data: function () {
return {
message: "コンポーネント内変数"
};
},
template: `<p>{{ message }}</p>`
}).mount(document.body);
HTML要素の表示・非表示は v-show
ディレクティブで簡単に制御できます。以下はボタンが押されるたびにメッセージが現れたり消えたりする実装です。
Vue.createApp({
data: function () {
return {
isShow: true
};
},
methods: {
onClickAtbutton: function (event) {
if (this.isShow) {
this.isShow = false;
} else {
this.isShow = true;
}
}
},
template: `<p v-show="isShow">表示中</p><button v-on:click=onClickAtbutton>表示 / 非表示</button>`
}).mount(document.body);
ディレクティブ v-if
v-for
と <template>
タグ
Vue.js専用ディレクティブのなかには v-if
系ディレクティブがあります。正確には v-if
v-else-if
v-else
の3つから成るのですが、これらディレクティブはコンポーネント内変数の値や親コンポーネントから渡された値をもとに表示するHTML要素を切り替えるというものです。 v-show
ディレクティブでも同じ処理はできますが、よりスマートにした感じですね。
v-for
ディレクティブは配列の値をもとに、配列の長さのぶんだけHTML要素を生成するという変わった挙動のディレクティブです。最初はとっつきにくいでしょうが、かなり使用頻度の大きいディレクティブであるため頑張って使いかたを覚えてください。
Vue.createApp({
data: function () {
return {
messages: ["a", "b", "c"]
};
},
// v-forディレクティブで配列から取り出した値を格納する変数は、v-bind:keyディレクティブでの指定することが推奨されています。
template: `<button v-bind:key="message" v-for="message in messages">{{ message }}</button>`
}).mount(document.body);
上記 v-if
ディレクティブと v-for
ディレクティブですが1つのHTML要素に対して、同時に適用することは推奨されていません。想定外の挙動をとる可能性があるためです。
これを回避する手段の1つとして <template>
タグを使う方法があります。 <template>
タグはHTML文字列上で使うことができますがDOMには反映されない不思議なタグです。一見するとなんのために存在しているのかよくわからないタグですが、以下のように使うことができます。
Vue.createApp({
data: function () {
return {
isShow: true,
messages: ["コンポーネント変数「isShow」が、", "trueのときはここが表示されます。", "ちゃんとv-forで処理されています。"]
};
},
methods: {
onClickAtButton: function (event) {
if (this.isShow) {
this.isShow = false;
} else {
this.isShow = true;
}
}
},
template: `
<template v-if="isShow">
<p v-bind:key="message" v-for="message in messages">{{ message }}</p>
</template>
<p v-else>コンポーネント変数「isShow」はfalseです。</p>
<button v-on:click="onClickAtButton">true / false</button>
`
}).mount(document.body);
SFC
最後にVue.jsの目玉機能であるSFC(Single File Component) について解説します。
概要
SFCは「.vueファイル」というVue.js独自規格のファイルでプログラムを構築できる機能です。
以下実装をご覧ください。ボタンをクリックするたびにボタンのラベルの数字が1つずつ大きくなっていくというアプリケーションです。
Vue.createApp({
data: function () {
return {
count: 0
};
},
render: function () {
return [
Vue.h(
"button",
{
onClick: function (event) {
this.count += 1;
}.bind(this),
style: {
color: "red"
}
},
this.count
)
];
}
}).mount(document.body);
これをSFCを使って実装すると以下のようなかたちになります。なお、SFCでは実装の大半を.vueファイルに記述しますがエントリポイントとして.jsファイルを必ず配置する必要があります。以下例では index.js
がエントリポイントです。
<template>
<button class="counter-button" v-on:click="count += 1;">{{ count }}</button>
</template>
<style scoped>
.counter-button {
color: red;
}
</style>
<script>
export default {
data: function () {
return {
count: 0
};
}
};
</script>
import * as Vue from "vue/dist/vue.esm-browser.js";
import App from "./App.vue";
Vue.createApp(App).mount(document.body);
好みはあると思いますがSFCのほうが全体的にすっきりしているかと思います。HTML・CSS・JavaScriptがそれぞれ別のブロックで管理されている点もWeb標準の方法に通ずるところがあって、とっつきやすいです。
ただし、SFCは1つだけ欠点があります。それは「Webブラウザは.vueファイルを認識しない」ということです。.vueファイルをwebpackなどのバンドラーを使って、.jsファイルに変換し、それを読み込ませるという面倒な手順が必要になります。
とはいえ、SFCはあまりにも便利すぎる機能ですし、Vue.jsを使う案件では99% SFCを使って開発することになると思いますので頑張って覚えてください。
導入手順
SFCの導入手順(正確にはSFC利用環境の構築手順)を説明します。公式にSFCの環境構築手順が載っていないので、いろいろなところを参考に作りあげた我流の環境構築手順となります。
今回はnpmで環境構築を行います。.vueファイルを.jsファイルに変換するプログラムにはwebpackを使用します。webpackをご存じない方は申し訳ありませんが各自で概要や使いかたを調べてください。
まずは以下コマンドを実行して必要なパッケージをインストールしてください。Vue.jsはインストール済みとします。
npm install --save-dev css-loader vue-loader vue-style-loader webpack webpack-cli
webpack.config.js
に以下のような設定を入れます。なお、npmのモジュールタイプはESモジュールタイプにしています。
import path from "node:path";
import url from "node:url";
import vueLoader from "vue-loader";
import webpack from "webpack";
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
// SFCでは.vueファイルを読み込むエントリポイントして.jsファイルを用意する必要があります。
// プロパティ「entry」には上記.jsファイルのパスを指定してください。
entry: path.join(__dirname, "src", "index.js"),
mode: "development",
module: {
rules: [
{
test: /\.vue$/,
use: ["vue-loader"]
},
{
test: /\.css$/,
use: ["vue-style-loader", "css-loader"]
}
]
},
output: {
filename: "main.js",
path: path.join(__dirname, "public")
},
plugins: [
// Vue.jsの警告機能周りの設定です。
// ひとまず最初は以下設定で動かしていただき、慣れてきたらお好みで弄ってください。
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false
}),
new vueLoader.VueLoaderPlugin()
]
};
これでwebpackを動かすとプロパティ entry
に指定したファイルをエントリポイントとしてバンドルが行われ、バンドルした.jsファイルをプロパティ output
で指定したパスに出力してくれます。
出力した.jsファイルは、もちろんそのままでは動かせません。あらかじめ.htmlファイルに、出力した.jsファイルのパスを参照して読み込むように <script>
タグを記述しておいてください。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta content="initial-scale=1.0, width=device-width" name="viewport">
<title>Vue.js 3.3入門</title>
<!--
出力先を参照して読み込めるように<script>タグを記述しておいてください。
-->
<script defer src="./main.js" type="module"></script>
</head>
<body></body>
</html>
使いかた
先ほど掲載した.vueファイルを見れば、おおよそどういう使いかたをするのか見当がつくと思いますが、一応基本的なことだけをざっと列挙します。
1点目。ファイルの拡張子は「vue」です。「App.vue」とか「DefaultButton.vue」といったファイル名にしてください。
2点目。.vueファイルは.jsファイルを読み込むように import
文で読み込みます。.jsファイルから.vueファイルを読み込めますし、.vueファイルから.vueファイルを読み込むことも可能です。ESモジュールに定義して export
文で公開されたコンポーネントを import
文で読み込むのと同じことです。
import App from "./App.vue";
3点目。.vueファイルは <template>
<style>
<script>
の3つのタグで構成します。それぞれHTML・CSS・JavaScriptを記述するブロックとなっています。 <template>
と <style>
は省略できますが <script>
は省略できません。ただ、現実的には <template>
と <style>
はほぼ間違いなく全ての.vueファイルで使うので省略することはありません。
4点目。 <script>
タグからコンポーネントを必ず export default
してください。あと、基本的に1つの.vueファイルには1つのコンポーネントだけを定義するようにしてください。
5点目。.vueファイルから import
したコンポーネントを利用するときはテンプレート機能のときのように、コンポーネントのプロパティ components
に登録してから <template>
タグ内に記述してください。
<template>
<p>Hello world.</p>
</template>
<script>
export default {};
</script>
<template>
<hello-world></hello-world>
</template>
<script>
import HelloWorld from "./HelloWorld.vue";
export default {
components: {
"hello-world": HelloWorld
}
};
</script>
さいごに
解説は以上です。とりあえず、これだけ知っておけばなんとかギリギリ自走に入れるかと思います。
逆に言うと、当記事では本当に最低限のことしか説明していません。当記事読了後は、どこかのタイミングで必ずVue.js公式ドキュメントを読むようにしてください。非常に多くの発見がそこにあります。
また、これは余談になりますが、Vue.jsのエコシステムなどに興味のある方は以下ワードで検索をかけてみることをオススメ致します。
- Vite
- Vue CLI
- Vue Router
- Vuetify
- Vuex