「Vue.js入門」基礎まとめ（１）

[Vue.js入門](https://www.amazon.co.jp/dp/4297100916)を読んでいます。
この本の3章までの内容について、開発するアプリケーションやチームの規模に関わらず、Vue.jsを使う上で最低限知っておく必要あると感じました。
そこで、時間がない人向けにVue.js入門3章＋αの重要と思う部分をまとめたいと思います。
今回のタイトルに（１）とあるように、何回かに分けて記事にしていく予定です。
私自身がFrontEnd初心者のため、初心者の方向けの基礎的な内容です。

今回は、Vue.js入門の2章にあたる、Vue.jsの基本的な書き方についてです。
何かを新しくインストールすることなく、Vue.js独自の書き方や思想を取り入れ、簡単なアプリケーションを作ってVue.jsを体験することを目的としています。

# Vueオブジェクトとインスタンス
Vue.jsを使うために、まずはhtmlのscript要素でVue.jsを読み込みます。
下記の例ではCDNから最新の安定版2.6.11（2020/05時点）を読み込んでいます。

```xxx.html
<script src="https://unpkg.com/vue@22.6.11"></script>
```

これでVueオブジェクトを使う準備ができました。

Vue.jsの基本的な使い方は、
VueオブジェクトからVueインスタンスを生成し、VueインスタンスをDOM要素に紐づけ（マウント）、Vueインスタンスの中で定義したデータやメソッドを利用しUIを変化させたりする、…というものです。
VueオブジェクトからVueインスタンスを生成する方法のひとつとして、`var vm = new Vue({...})`というようにコンストラクタを使用する方法があります。
この場合、コンストラクタには、下記のようなオプションオブジェクトを引数として渡します。

| オプション名 | 概要 |
| ---- | ---- |
| el | マウントする対象のDOM要素（idあるいはclass名で指定）。 |
| data | DOM要素に表示したり、DOM要素から参照する変数群。 |
| computed | dataでは表現できないような複雑な処理を施された変数（みたいなもの）。 |
| method | イベント発生時などに呼び出す処理。 |
| filters | 引数を整形した文字列として返す。 |

elとdataを例にすると、下記のように書きます。

```test01.js
var vm = new Vue({
    el: '.app', // class=appのDOM要素にマウントする。
    data:{ // このVueインスタンス（vm）がマウントされたDOM要素のスコープ内で使用できる変数たち。
        data1 : "data1_test",
        data2 : "data2_test"
    }
})
```

```test01.html
<script src="https://unpkg.com/vue@2.6.11"></script>
<script src="js/test.js" defer></script>
<div class="app">
    {{data1}} <!-- Vueインスタンスが紐づいたDOM要素（class="app）のスコープ内。 -->
</div>
{{data2}} <!-- Vueインスタンスが紐づいたDOM要素（class="app）のスコープ外。 -->
```

`{{～}}`は、Mustache構文と言い、Vueインスタンスのdataオプションで宣言された変数を、値に置き換えて表示します。

test01.htmlをブラウザで実行し、htmlを見てみると、body部は下記の通りになっています。

```HTML
<body><div class="app">
    data1_test
</div>
{{data2}}</body>
```

`{{data1}}`については、`data1`が変数として扱われているため値が表示され、
`{{data2}}`については、Mustache構文ごとただの文字列と解釈されています。

このように、Vueインスタンスは紐づいたDOM要素内でのみ参照できることが分かります。

また、VueインスタンスのDOM要素への紐づけは、下記のように記述することも可能です。

```test01.js
var v = new Vue({
    data:{
        data1 : "data1_test",
        data2 : "data2_test"
    }
})
v.$mount(".app") // class=appの要素にマウントする。
```

Vueインスタンスを生成する際、elオプションを使いません。代わりに、Vueオブジェクトの$mountメソッドにDOM要素を指定します。
使用する場面としては、Vueインスタンスを生成した時点で紐づけたいDOM要素がまだ存在しない場合（何らかの操作でDOM要素が後から追加される場合など）です。DOM要素が追加された後に、$mountを実行しDOM要素に紐づける、という使い方をします。


## 各種オプション

先ほどの例では、elとdataの2つのオプションを紹介しました。他のオプションも見ていきます。
並行して、各種オプションと切り離せない大切な存在「ディレクティブ（`v-`から始まるVue.js専用のhtml属性）」についても見ていきます。

下記は、野菜を購入できる八百屋さんをイメージしたサンプルアプリケーションです。

|![0101.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/566554/8605fa76-4b3d-f894-c906-4934435bfd93.gif)
|:-:|


下記のような仕様です。

- 八百屋さんのタイトルをマウスオーバーすると、八百屋さんからのメッセージが変わります。
- 野菜の購入数を入力すると、合計金額を教えてくれます。
- 合計金額が一定金額を超えると、割引券がもらえます。

ソースは下記です。

```test02.html
<script src="https://unpkg.com/vue@2.6.11"></script>
<script src="js/test02.js" charset="UTF-8" defer></script>
<div id="app1"><span v-on:mouseover=mouseover() v-on:mouseleave=mouseleave>八百屋</span>{{message}}</div>
<br>
<br>
<div id="app2">
    {{vege.name}}{{vege.price}}円<input type="number" min=0 v-bind:value="vege.num" v-on:input="vege.num=$event.target.value"/>個
    <br>
    「お会計は{{totalPrice | threeDigitComma}}円です。」<br>
    <div v-show=isNextDiscount>「{{discountCondition | threeDigitComma}}円以上なので、次回使える割引券を差し上げます。」</div>
</div>
```

```test02.js
var vm1 = new Vue({
    el: '#app1', // id=app1のDOM要素にマウントする。
    data:{
        message : "「いらっしゃいませ！」",
        messageOver : "「当店では新鮮なお野菜を用意しております！」",
        messageLeave : "「いらっしゃいませ！」"
    },
    methods:{
        mouseover :  function(){
            this.message = this.messageOver
        },
        mouseleave :  function(){
            this.message = this.messageLeave
        }
    }
})

var vm2 = new Vue({
    el: '#app2', // id=app2の要素にマウントする。
    data:{
        vege : {name:"にんじん", price:100, num:0},
        discountCondition : 1000
    },
    computed:{ // 
        totalPrice :  function(){
            return this.vege.price * this.vege.num
        },
        isNextDiscount :  function(){
            return this.totalPrice >= this.discountCondition
        }
    },
    filters:{
        threeDigitComma : function(value){
            return value.toLocaleString()
        }
    }
})
```

`id="app1"`と`id="app2"`という独立したDOM要素があり、それぞれ別のVueインスタンスをマウントしています。

以降で、ソースを具体的に見ていきます。

### methods

※ソースは抜粋

```test02.html
<div id="app1"><span v-on:mouseover=mouseover() v-on:mouseleave=mouseleave>八百屋</span>{{message}}</div>
```

```test02.js
var vm1 = new Vue({
    el: '#app1', // id=app1のDOM要素にマウントする。
    data:{
        message : "「いらっしゃいませ！」",
        messageOver : "「当店では新鮮なお野菜を用意しております！」",
        messageLeave : "「いらっしゃいませ！」"
    },
    methods:{
        mouseover :  function(){
            this.message = this.messageOver
        },
        mouseleave :  function(){
            this.message = this.messageLeave
        }
    }
})
```
`id="app1"`に紐づいているVueインスタンスには、methodsオプションが宣言されています。
htmlのspan要素`<span v-on:mouseover=mouseover() ...>`の`v-on:mouseover`の部分で、
この要素で発生したmouseoverイベントを検知し、Vueインスタンスで宣言されているmethodsのひとつ`mouseover()`を呼び出します。
このように、methodsオプションは、イベント発火時に呼び出す用途で使われることが多いようです。
`mouseover()`および`mouseleave()`内では、dataオプションで宣言したメッセージを書き換えています。

### v-on

`v-`から始まる属性は、Vue.js専用のhtml属性です（ディレクティブ）。
`v-on`については、上記の例の通り、`v-on:イベント名`でイベント（input、clickなど）を検知し、`v-on:イベント名=...`の`=`以降に続く処理を実行します。

Vue.js専用のhtml属性は、他にも`v-bind`、`v-for`、`v-show`などいろいろあります。後述します。

つまり、`id="app1"`では、
html側でマウスオーバーやマウスリーブのイベントが検知され、メソッドが呼び出され、メソッド内でdataオプションの値が変更され、その変更に伴い即時にhtmlも変更される、ということが起きています。
Vue.jsは、このように<font color=red>Vueインスタンスの状態（dataオプション）の変更を検知し、リアルタイムにhtmlにVueインスタンスの変更を反映する</font>という<font color=red>リアクティブな仕組み</font>の上に成り立っています。

### computed

※ソースは先述の抜粋

```test02.html
<div id="app2">
    {{vege.name}}{{vege.price}}円<input type="number" min=0 v-bind:value="vege.num" v-on:input="vege.num=$event.target.value"/>個
    <br>
    「お会計は{{totalPrice | threeDigitComma}}円です。」<br>
    <div v-show=isNextDiscount>「{{discountCondition | threeDigitComma}}円以上なので、次回使える割引券を差し上げます。」</div>
```

```test02.js
var vm2 = new Vue({
    el: '#app2', // id=app2の要素にマウントする。
    data:{
        vege : {name:"にんじん", price:100, num:0},
        discountCondition : 1000
    },
    computed:{ // 
        totalPrice :  function(){
            return this.vege.price * this.vege.num
        },
        isNextDiscount :  function(){
            return this.totalPrice >= this.discountCondition
        }
    },
    filters:{
        threeDigitComma : function(value){
            return value.toLocaleString()
        }
    }
})
```

`id="app2"`に紐づいているVueインスタンスには、computedオプションが宣言されています。
htmlの`{{totalPrice | threeDigitComma}}`の部分でcomputed`totalPrice`を呼び出しています（`| threeDigitComma`については後述します。）
このように、computedオプションで宣言されたものは、dataオプションで宣言された変数と同様に、Mustache構文`{{...}}`で呼び出すことができます。

では、`totalPrice`では、どのような処理が行われているのでしょうか。

`totalPrice`はreturnで終了しており、returnの結果に`totalPrice`という名前をつけた変数という扱いです。この中で参照している`this.vege`は、dataオプションで宣言されている変数`vege`です。
htmlには、`<input type="number" min=0 v-bind:value="vege.num" v-on:input="vege.num=$event.target.value"/>`という記述があり、変数`vege`に対し何かしらの処理を行っていることが分かります。
先述した通り、`v-on:input`は、inputイベント発火時の挙動を記述します。`$event.target.value`は、イベントが発火したDOM要素のvalue属性を示します。
つまり、`v-on:input="vege.num=$event.target.value"`と記述することで、inputイベント発火時、dataオプションに宣言された`vege.num`に、入力されたvalue属性の値を代入する、という処理が行われます。
`vege.num`を参照している`totalPrice`の結果も変化し、リアルタイムでhtmlに反映されます。

`id="app1"`と同様に、
画面に値が入力されることで、Vueインスタンスの状態（dataオプション）が変化し、dataオプションを参照しているcomputedの結果も変化し、リアルタイムでhtmlの表示が変化する、というリアクティブな仕組みが働いています。

### v-bind

ところで、input要素に、`v-bind:value="vege.num"`という記述があります。この`v-bind`属性は、先述した`v-on`と同様にVue.js専用の属性です。
input要素のvalue属性に、Vueインスタンスの変数を代入しています。
これのおかげで、htmlの初期表示時にvalue属性に0（`vege.num`）が代入されるため、0が表示されます。

### v-show

`<div v-show=isNextDiscount>`という記述があります。`v-show`は、`=`以降の値がtrueの場合、そのDOM要素を表示します。逆にfalseの場合、そのDOM要素を非表示にします。
`isNextDiscount`は、野菜の購入金額が一定以上の場合にtrueを返すcomputedです。つまり、野菜の購入金額が一定以上の場合、特別なメッセージを表示しています。

`v-show`と似た挙動をする属性として、`v-if`が存在します。これらの使い分けは下記のとおりです。

- v-show：DOM要素自体は存在している上で、表示/非表示を制御する（スタイルのdisplayにあたる）。DOM操作よりもスタイル操作のほうがコストがかからないため、頻繁に表示/非表示を制御する場合はこちらを推奨。
- v-if：DOM要素自体を生成/削除する。レンダリングのコストを削減できるので、「必要になったら表示する/必要じゃないときは表示しない」という場合に使うと良い。


### filters

htmlの金額を表示するMustache構文内において、`{{totalPrice | threeDigitComma}}`というように、`threeDigitComma`がパイプされています。
`threeDigitComma`はVueインスタンスのfiltersオプションに宣言されています。filtersオプションは、パイプで渡される引数`value`の表示を変更する目的で使われます。
filtersの中では、同じVueインスタンス内のdataオプションなどは参照できません。ですので、計算を加える、置換するといった、純粋にフォーマットを変更することしかできません。
`threeDigitComma`内では、引数に金額を渡し、3桁カンマで表示するようフォーマットを変更しています。


### v-for

ここまで、八百屋さんをイメージしたサンプルアプリケーションで説明をしてきました。
しかし、この八百屋さんでは、野菜をひとつしか購入することができません。
そこで、複数の野菜をリストにして持ちたいと思います。

※ソースは先述の抜粋から変更

```test02.html
<div id="app2">
    <div v-for="vege in vegeList" v-bind:key="vege.name"> <!--★追加-->
         {{vege.name}}{{vege.price}}円<input type="number" min=0 v-model="vege.num"/>個 <!--★修正-->
    </div><!--★追加-->
    <br>
    「お会計は{{totalPrice | threeDigitComma}}円です。」<br>
    <div v-show=isNextDiscount>「{{discountCondition | threeDigitComma}}円以上なので、次回使える割引券を差し上げます。」</div>
</div>
```

```test02.js
var vm2 = new Vue({
    el: '#app2', // id=app2の要素にマウントする。
    data:{
        vegeList : [ // ★リストに修正
            {name:"にんじん", price:100, num:0},
            {name:"ごぼう", price:210, num:0},
            {name:"だいこん", price:201, num:0}
        ],
        discountCondition : 1000
    },
    computed:{ // 
        totalPrice :  function(){ // ★修正
            return this.vegeList.reduce(
                function(sum, vege){
                    return sum + (vege.price * vege.num) 
                },
                0
            )
        },
        isNextDiscount :  function(){
            return this.totalPrice >= this.discountCondition
        }
    },
    filters:{
        threeDigitComma : function(value){
            return value.toLocaleString()
        }
    }
})
```

test02.jsにて、野菜をリストにしました。それに伴い、合計金額を計算する`totalPrice`も、リストに対して計算を行うよう修正しました。
また、test02.htmlは、`<div v-for="vege in vegeList" v-bind:key="vege.name">`という要素を追加しました。
`v-for`は、`v-for:item in list`といった使い方をし、Vueインスタンスで宣言されたリストをDOM要素内でループ参照することを可能にします。
`v-bind:key="vege.name"`は、`v-for`でループする要素を特定するキーを取得しています。一見不要に見えますが、`v-for`の性能のため、必要な記述です。


### v-model

実は、`<div v-for...></div>`で挟んだ1行も、下記のように修正しています。

・修正前

```
        {{vege.name}}{{vege.price}}円<input type="number" min=0 v-bind:value="vege.num" v-on:input="vege.num=$event.target.value"/>個
```

・修正後

```
        {{vege.name}}{{vege.price}}円<input type="number" min=0 v-model="vege.num"/>個
```

`v-model`を使うことで、入力フォームにおいて`v-on:input`（あるいは`v-on:change`）と`v-bind`を書く手間を省くことができます。


これで、この八百屋さんで複数の野菜を購入することができるようになりました！

|![0102.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/566554/f645cc84-a94e-3302-54dd-1e715f70c83f.gif)|
|:-:|

下記は、ソース全体です。

```test02.html
<script src="https://unpkg.com/vue@2.6.11"></script>
<script src="js/test02.js" charset="UTF-8" defer></script>
<div id="app1"><span v-on:mouseover=mouseover v-on:mouseleave=mouseleave>八百屋</span>{{message}}</div>
<br>
<div id="app2"><div v-for="vege in vegeList" v-bind:key="vege.name">
    {{vege.name}}{{vege.price}}円<input type="number" min=0 v-model="vege.num"/>個
    </div>
    <br>
    「お会計は{{totalPrice | threeDigitComma}}円です。」<br>
    <div v-show=isNextDiscount>「{{discountCondition | threeDigitComma}}円以上なので、次回使える割引券を差し上げます。」</div>
</div>
```

```test02.js
var vm1 = new Vue({
    el: '#app1', // id=app1の要素にマウントする。
    data:{
        message : "「いらっしゃいませ！」",
        messageOver : "「当店では新鮮なお野菜を用意しております！」",
        messageLeave : "「いらっしゃいませ！」"
    },
    methods:{
        mouseover :  function(){
            this.message = this.messageOver
        },
        mouseleave :  function(){
            this.message = this.messageLeave
        }
    }
})

var vm2 = new Vue({
    el: '#app2', // id=app2の要素にマウントする。
    data:{
        vegeList : [
            {name:"にんじん", price:100, num:0},
            {name:"ごぼう", price:210, num:0},
            {name:"だいこん", price:201, num:0}
        ],
        discountCondition : 1000
    },
    computed:{ // 
        totalPrice :  function(){
            return this.vegeList.reduce(
                function(sum, vege){
                    return sum + (vege.price * vege.num) 
                },
                0
            )
        },
        isNextDiscount :  function(){
            return this.totalPrice >= this.discountCondition
        }
    },
    filters:{
        threeDigitComma : function(value){
            return value.toLocaleString()
        }
    }
})
```

ここまで読んで頂き、ありがとうございました！
# 次回

今回は、簡単なアプリケーションを作りながら、Vue.jsの基本について書きました。
Vue.jsにはライフサイクル、コンポーネントという考え方があったり、Vue.jsで開発するための便利ツールなどもあったりします。
それについては、以降の記事で書いていきたいと思います。

# 最後に
[Vue.js入門](https://www.amazon.co.jp/dp/4297100916)を学習の基本に活用させて頂き、
より理解を深めるために皆様の記事をたくさん参考にさせて頂きました。ありがとうございました！