この記事でわかること

本稿ではすでにVueに関心がある方向けに、環境構築の要らない、すぐに動かせるサンプルを用いながらVueの基本機能の概観をご説明します。

◆利用するバージョン◆
 Vue2系
◆本記事のスコープ◆
 コンポーネント利用の前提となるVueインスタンスの基本機能

はじめに

SPA(シングル・ページ・アプリケーション)のアーキテクチャを実現する代表的なFWの一つであるVue.jsですが、他のFWに比べて環境構築が容易という利点があります。

どれくらい容易かと言うと、下記のコードをメモ帳に貼り付けてhtmlとして保存し、ブラウザで開けば、Vueを使ったページの作成と表示が完了するのです。

<!DOCTYPE html>
<title>sample-0</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>

<div id="app"></div>

<script>
new Vue({
    el: '#app',
    template: '<p>{{msg}}</p>',
    data: {msg: 'hello world!'}
})
</script>

Vueの公式ガイドにも案内がある<script>直接読み込み(上記サンプルにおける3行目)により、直ちにVueの利用が可能です。

実際のアプリケーション開発では、大抵の場合Node.js, npm, webpack/ vue-cli等の周辺ツールを利用した、より高度な環境構築が必要となるのですが、今回はこの最小限の環境でVueの基本機能を一通り紹介していきます。


目次

  1. Vueオブジェクトとマウント
  2. Vueコンストラクタの主要プロパティ
  3. ディレクティブによるバインディング
  4. ライフサイクルフック

1.Vueオブジェクトとマウント

はじめに、前掲のサンプル(sample-0)の中身を解説します。

<!DOCTYPE html>
<title>sample-0</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>

<div id="app"></div>

<script>
new Vue({
    el: '#app',
    template: '<p>{{msg}}</p>',
    data: {msg: 'hello world!'}
})
</script>

このサンプル内では、純粋なHTMLとして定義されたDOMは、下記の空の<div>要素ただ一つです。

<div id="app"></div>

ですが、サンプルファイルを実際にブラウザで表示してみると、定義された<div>の代わりに、下記のDOMがレンダリングされていることが確認できます。

<p>hello world!</p>

この現象は、下記の2つのキーワードによって理解できます。


Vueオブジェクト…

Vueオブジェクトは、Vue.jsのAPIを束ねるモジュールです。
コンストラクタを利用してインスタンスを生成し、DOMにマウントして扱います。
インスタンス自身のメソッドを呼び出して機能を呼び出すこともできますが、
コンストラクタにて様々なプロパティやオプションオブジェクトを指定するだけでも多くの機能が利用できます

宣言例)
new Vue({
    el: '#app',
    template: '<p>{{msg}}</p>',
    data: {msg: 'hello world!'}
})

マウント…

Vueインスタンスのマウントとは、HTMLで定義した既存のDOM要素を、Vue.jsの生成するDOM要素で置き換えることです。
Vueオブジェクトのインスタンスから$mount()メソッドを呼び出す方法のほか、Vueオブジェクト内のelプロパティにDOMを指定することでマウントが可能です。
いずれの方法でも、DOMの指定にCSSで利用するセレクタを利用することができます

マウント例①)
new Vue({
    el: '#app',
})

マウント例②)
new Vue({
}).$mount('#app')

※①、②とも単体では同じ挙動

これらのキーワードを使って、サンプルファイルの処理を説明すると、下記になります。

①<script>タグ内で、Vueオブジェクトのインスタンスが生成される。

<script>
new Vue({
    el: '#app',
    template: '<p>{{msg}}</p>',
    data: {msg: 'hello world!'}
})
</script>

②コンストラクタのtemplateプロパティ、dataプロパティの指定から、Vueオブジェクトは下記の仮想DOMを生成する。
(※テンプレート中の {{ }} というシンタックスは、囲まれた文言をjavascript/Vueの式として評価する。)

<p>{{msg}}</p>
↓
<p>hello world!</p>

③コンストラクタのelプロパティの指定の指定から、Vueオブジェクトは下記のDOMにマウントされる。

<div id="app"></div>

④レンダリングにあたり、「③」の静的なDOMが「②」のDOMへと動的に置き換わる。


Vue.jsにおけるレンダリングはこのように、Vueオブジェクトが動的に生成したデータを、マウントされたDOMの上に置き換えていくことで、UIを構築する仕組みとなっています。


2.Vueコンストラクタの主要プロパティ

最初のサンプル(sample-0)では、Vueオブジェクトのコンストラクタに様々なプロパティを渡すことによってDOMの書き換えが行われることを確認しました。

次のサンプル(sample-1)では、個別のプロパティの働きを理解するため、
Vueオブジェクトのコンストラクタに渡せるプロパティのうち重要な3つのプロパティをピックアップして一挙に動かしてみます。

<!DOCTYPE html>
<title>sample-1</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>

<div id="app">
    <button v-on:click="increment">increment index!</button>
    <p>the current factor: {{array[idx].val}}</p>
    <p>is this final factor?: {{isFinal ? "yes": "no"}}</p>
</div>

<script>
new Vue({
    el: '#app',
    data: {
        idx: 0,
        array: [
            {val: "inital factor"},
            {val: "middle factor"},
            {val: "final factor"}
        ]
    },
    computed: {
        isFinal: function() {
            // whether the displayed factor is final one
            return this.idx === this.array.length - 1;
        }
    },
    methods: {
        increment: function() {
            if (this.isFinal) {
                // initialize index
                this.idx = 0;
            } else {
                // increment index
                this.idx++;
            }
        }
    }
})
</script>

このサンプルを実際にブラウザで表示すると、「increment index!」と書かれたボタンと、それに連動して(リアクティブに)表示文言を変える2つの<p>要素がレンダリングされます。

このサンプルは、ボタンを押すことで索引を進め、UIの裏側に隠蔽された❶「配列の中身」を順に表示するとともに、❷「”いま表示されている要素が配列の最後の要素かどうか?”という付随的な情報」をも表示するというアプリケーションです。

ここでは、UIを実現するために以下の3種類のデータが定義されています。

  1. 「3つの要素からなる配列」と、「配列の参照に使う索引」という、UIの裏にある状態のデータ
  2. 「参照されている要素が最後の要素かどうか」という、付随的に算出されるデータ
  3. ボタンを押すというイベントに紐づけられた、「索引を進める」という一連の処理内容を示すデータ

この3種類のデータが、今回Vueオブジェクトのコンストラクタにあるdata, comuted, methodsという3つのコンストラクタ・プロパティによってそれぞれ定義されているのです。


データ定義(data)

dataプロパティは、UIにおいて利用したい「状態」のデータを、Vueオブジェクト内で保持するためのコンストラクタ・プロパティです。
キーにdata名、値にdata内容を取るオブジェクト形式で指定します。
dataの値が変わると、dataのバインディングされたDOMにもリアルタイムで反映されます。(例: idx)

指定例)
new Vue({
    data: {
        idx: 0,
        array: [
            {val: "inital factor"},
            {val: "middle factor"},
            {val: "final factor"}
        ]
    },

算出プロパティ(computed)

算出プロパティは、前提として基となるデータがdataプロパティ等で既に定義されており、そうした基データに何らかの処理を与えたものをデータ・プロパティとして公開する仕組みです。
基となるデータの変更を検知し、リアクティブに最新の算出値を保持することができます。
キーにcomputed名、値に関数を取るオブジェクト形式で指定します。

指定例)
new Vue({
    computed: {
        isFinal: function() {
            // whether the displayed factor is final one
            return this.idx === this.array.length - 1;
        }
    },

複雑な式を扱うテンプレートの見通しを良くする目的でも利用します。(下記参照)

例示)
<!-- computedを使わない場合(読みにくい) -->
<p>is this final factor?: {{(this.idx === this.array.length - 1) ? "yes": "no"}}</p>

<!-- computedを使う場合(見通しが改善) -->
<p>is this final factor?: {{isFinal ? "yes": "no"}}</p>

メソッド(methods)

methodsは、Vueコンストラクタ内にメソッドを定義するためのコンストラクタ・プロパティです。
キーにmethod名、値に関数を取るオブジェクト形式で指定します。
computedとは指定の形式が同じでが使途が明確に異なり、
computedが「計算結果をreturnしてキャッシュし、常に公開し続ける」ことを目的とするのに対し、
methodsは単に「呼び出されるたび、定義された処理を行う」ことを目的としています。

指定例)
new Vue({
    methods: {
        increment: function() {
            if (this.isFinal) {
                // initialize index
                this.idx = 0;
            } else {
                // increment index
                this.idx++;
            }
        }
    }

後述する「v-onディレクティブ」によって、ユーザ操作によるイベントと紐づけて使われることが多いです。

例)
<button v-on:click="increment">increment index!</button>


なお、data, computed, methodsの各オブジェクトに定義されたデータ・プロパティは、各オブジェクト内で紐づけられたキーを識別子として公開され、テンプレート中およびスクリプト中で利用することができます。

テンプレートの例)
<!-- array, idxの両dataは、テンプレートから直接参照できる -->
<!-- 配列の中身や、オブジェクトの値も取得することができる -->
<p>the current factor: {{array[idx].val}}</p>
スクリプトの例)
        increment: function() {
            // methodsの定義の中でcomputedプロパティを参照できる
            if (this.isFinal) {
                // dataプロパティも参照できる
                this.idx = 0;
            } else {
                // 同一オブジェクト内の参照は、「this」キーワードが必要
                this.idx++;
            }
        }


以上の各コンストラクタ・プロパティの働きをふまえれば、ボタンを押したときのsample-1の処理を下記のように理解することができます。

⓪dataおよびcomputedの値が初期状態で評価され、レンダリングされる。

<p>the current factor: {{array[idx].val}}</p>
<p>is this final factor?: {{isFinal ? "yes": "no"}}</p>
↓
<p>the current factor: {{array[0].val}}</p>
<p>is this final factor?: {{false ? "yes": "no"}}</p>
↓
<p>the current factor: initial factor</p>
<p>is this final factor?: no</p>

①ボタンをクリック(1回目)することによりincrementメソッドが呼ばれ、indexがインクリメントされる。

<button v-on:click="increment">increment index!</button>
    methods: {
        increment: function() {
            if (this.isFinal) {
                // initialize index
                this.idx = 0;
            } else {
                // increment index ←インクリメントの分岐に入る
                this.idx++;
            }
        }
    }

②dataのidxが「0→1」、付随してcomputedのisFinalが「false→false」と再評価され、それぞれがDOMにバインドされる。

<p>the current factor: inital factor</p>
<p>is this final factor?: no</p>
↓
<p>the current factor: middle factor</p>
<p>is this final factor?: no</p>

③ボタンをクリック(2回目)することにより再度incrementメソッドが呼ばれ、indexがインクリメントされる。(「①」と同様)

④dataのidxが「1→2」、computedのisFinalが「false→true」と再評価され、それぞれがDOMにバインドされる。

<p>the current factor: {{array[idx].val}}</p>
<p>is this final factor?: {{isFinal ? "yes": "no"}}</p>
↓
<p>the current factor: {{array[2].val}}</p>
<p>is this final factor?: {{true ? "yes": "no"}}</p>
↓
<p>the current factor: final factor</p>
<p>is this final factor?: yes</p>

⑤ボタンをクリック(3回目)することによりincrementメソッドが呼ばれ、indexが初期化される。

    methods: {
        increment: function() {
            if (this.isFinal) {
                // initialize index ←初期化の分岐に入る
                this.idx = 0;
            } else {
                // increment index
                this.idx++;
            }
        }
    }

⑥dataのidxが「2→0」、computedのisFinalが「true→false」と再評価され、それぞれがDOMにバインドされる。(「⓪」と同様の状態に戻る。)

<p>the current factor: {{array[idx].val}}</p>
<p>is this final factor?: {{isFinal ? "yes": "no"}}</p>

<p>the current factor: {{array[0].val}}</p>
<p>is this final factor?: {{false ? "yes": "no"}}</p>

<p>the current factor: initial factor</p>
<p>is this final factor?: no</p>

sample-0は、Vueオブジェクトのマウントにより、単にDOMを置き換えるのみのサンプルでしたが、
sample-1では、data, computed, methodのコンストラクタ・プロパティを組み合わせることで、マウントしたDOMの内部のデータを自在に操れるようになりました。


3.ディレクティブによるバインディング

フロントエンド資源の主目的がUIの構築である以上、テンプレート上で用いるデータがうまく制御できたとして、それ単体の持つ意義は限定的であり、むしろそのデータを基にいかにしてDOMを操作するかということが重要です。

Vue.jsの提供するディレクティブは、そうしたダイナミックなDOM操作に対応しています。
ディレクティブは共通してテンプレートに属性の形式で書き込むことができ、頻用するDOM操作を直感的に行うことができますが、その用途はそれぞれ大きく異なります。ここでは、代表的な4つのディレクティブとその用法を概観してみます。


条件付きレンダリング: v-if/v-show

v-if/ v-showは、属性値がtrueのときのみ指定されたDOMをレンダリングするディレクティブです。
下記のサンプルを実際にブラウザで表示してみてください。

<!DOCTYPE html>
<title>sample-2</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>

<div id="app">
    <button v-on:click="toggleA">switch A</button>
    <button v-on:click="toggleB">switch B</button>
    <p v-if="flagA">this is DOM A (v-if)</p>
    <p v-show="flagB">this is DOM B (v-show)</p>
</div>

<script>
new Vue({
    el: '#app',
    data: {
        flagA: true,
        flagB: true
    },
    methods: {
        toggleA: function() {
            this.flagA = !this.flagA;
        },
        toggleB: function() {
            this.flagB = !this.flagB;
        }
    }
})
</script>

A,Bの2つのスイッチボタンはそれぞれflagA, flagBのON/OFFを切り替えるものであり、
flagA, flagBの2つのフラグ値はそれぞれv-if, v-showのディレクティブによって異なる<p>要素に指定されています。

ボタンを押すことで対象の<p>要素の表示・非表示が切り替わることを確認してください。
この例ではdataで定義したフラグを渡していますが、ディレクティブの属性値には真偽値として評価できる任意の式を指定できます。computedで定義したflag値等を渡せば、元データの更新に伴ってDOM要素の表示・非表示を自動的に切り替えるといったことも可能です。

また、v-if, v-showの違いは、上記のサンプルの挙動をブラウザのデベロッパーツールで観察するとよくわかります。
v-ifはDOM要素を実際に消すのに対し、
v-showはDOM要素の実体は残したまま、スタイルのdisplay=noneの指定によって見かけ上の非表示とするのです。

指定値がfalseの場合のレンダリング結果の違い)

<p v-if="false">this is DOM A (v-if)</p>
↓
<!---->

<p v-show="false">this is DOM B (v-show)</p>
↓
<p style="display: none;">this is DOM B (v-show)</p>

DOM操作の観点からは似て非なる機能を持つ両者ですが、
開発の現場では、パフォーマンスやセキュリティの要件によって使い分けることになります。


リストレンダリング: v-for

v-forは、配列やオブジェクトの中のデータをリストレンダリングするためのディレクティブです。
属性値には「要素 in 集団項目」という形式を取り、指定したタグの内部で要素を記述することで、実際に子要素を取り出すことができます。また、データは動的にバインドされているため、途中で中身のデータが変わったり、要素の数が増えてもレンダリングが可能です。

<!DOCTYPE html>
<title>sample-3</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>

<div id="app">
    <button v-on:click="addFactor">add factor!</button>
    <ul>
        <li v-for="factor in array">{{factor.val}}</li>
    </ul>
</div>

<script>
new Vue({
    el: '#app',
    data: {
        nextIndex: 4,
        array: [
            {val: "factor 1"},
            {val: "factor 2"},
            {val: "factor 3"}
        ]
    },
    methods: {
        addFactor: function() {
            this.array.push({val: ("factor " + this.nextIndex)});
            this.nextIndex++;
        }
    }
})
</script>

上記のサンプルではボタンを押すことで配列の要素が増えますが、増えた分の要素もリアクティブにレンダリングされることを確認してください。


属性値のバインディング: v-bind

汎用性が高くもっともよく使われるディレクティブで、テンプレートタグの様々な属性に対し動的なデータを紐づけることができます。
下記のサンプルでは、単一のフラグ値を様々な属性に紐づけています。フラグ値の変化が結果的に様々なDOM操作に繋がりうることを確認してください。

<!DOCTYPE html>
<title>sample-4</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>

<div id="app">
    <button v-on:click="toggleMasterFlag">switch</button>
    <p></p>
    <button v-bind:disabled="masterFlag">{{masterFlag ? "disabled now" : "this button will be disabled"}}</button>
    <p v-bind:style="{color: masterFlag ? 'red' : 'black'}">the color of this line will change</p>
    <p v-bind:hidden="masterFlag">this line will disapper</p>
</div>

<script>
new Vue({
    el: '#app',
    data: {
        masterFlag: false,
    },
    methods: {
        toggleMasterFlag: function() {
            this.masterFlag = !this.masterFlag;
        }
    }
})
</script>

また、よく使われることから、「v-bind:」を単に「:」と書くだけで同様の効果が得られる省略記法が存在します。

例①)
<p v-bind:hidden="masterFlag">this line will disapper</p>

例②)
<p :hidden="masterFlag">this line will disapper</p>

※①、②とも同じ挙動

イベントハンドリング: v-on

DOMのイベント(click, change, input等…)に対し、実行される処理をバインドすることができるディレクティブです。
素のjavascriptで行うような複雑なイベントリスナの宣言は必要なく、イベントを指定し、属性値に式やメソッドを指定するだけで利用可能です。

下記は、input要素のinputイベントとchangeイベントの両方にメソッドを紐づけることで、実際に入力された値を別々のdataのプロパティに退避して、対応するDOMへリアクティブに反映するサンプルです。
冒頭の入力欄に任意のテキストを入力してみてください。

<!DOCTYPE html>
<title>sample-5</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<div id="app">
    <input
     v-on:input="storeDataA($event.target.value)"
     v-on:change="storeDataB($event.target.value)"></input>
    <p>inputイベントで取得したデータ: {{ inputDataA }}</p>
    <p>changeイベントで取得したデータ: {{ inputDataB }}</p>
</div>
<script>
new Vue({
    el: '#app',
    data: {
        inputDataA: "入力されたデータがここに保持されます"
        inputDataB: "入力されたデータがここに保持されます"
    },
    methods: {
        storeDataA: function(inputDataA) {
            this.inputDataA = inputDataA;
        },
        storeDataB: function(inputDataB) {
            this.inputDataB = inputDataB;
        }
    }
})
</script>

なお、inputイベントやchangeイベントのようにユーザ操作から何らかの入力が存在する場合は、
ディレクティブの属性値の中において「$event.target.value」という記述により入力値を受け取ることが可能です。

また、このディレクティブもよく使われることから、「v-on:」を単に「@」と書くだけで同様の効果が得られる省略記法が存在します。

例①)
<input v-on:input="storeDataA($event.target.value)"></input>

例②)
<input @input="storeDataA($event.target.value)"></input>

※①、②とも同じ挙動

4.ライフサイクルフック

Vueインスタンスのライフサイクルは、その節目節目にcreated, updated, destroyedといった名前がついており、それらの節目に対応した「フック」(“ひっかかり”)が存在します。

こうしたフックに対し処理を登録しておくことで、インスタンスが実際にそのタイミングを迎えたときに自動的に処理を呼び出すことができます。

これらもVueの基本機能と呼べるものですが、その真価は個別のVueインスタンスをそれぞれコンポーネントと対応付けてアプリケーションを構築するときに発揮されます
本記事ではスコープ外となっているVueのコンポーネントシステムについては次の記事にて取り扱う予定なので、ライフサイクルフックの詳細についてはそちらで合わせてご紹介します。


まとめ

Vueの基本機能を、環境構築なしで動かせるサンプルを交えながら紹介しました。ミクロのレベルでどんなことができるのか?のイメージを掴んだり、プログラミングの際の手触りを把握するといった形で参考になれば幸いです。

SPAのフレームワークの中でも比較的敷居が低いと言われるVueですが、今回概観したような基本機能に支えられる形で成立しているコンポーネントシステムや、コンポーネントシステムを基にした設計パターン、Vueのコアライブラリでは手の届かない部分をフォローするVUEX、Vue Routerなどの周辺ライブラリ、また中~大規模開発に必要なより高度な環境構築や、その前提となるNode.js, npm, webpack/ vue-cliなどなど、理解を深めるうえで重要なトピックはたくさんあるので、それらについてもおいおい取り上げていければと思います。