参考にした書籍はこちら
Vue.js入門 基礎から実践アプリケーション開発まで
基礎的な使い方の話
導入
<script src="http://unpkg.com/vue"></script>
//バージョン指定
<script src="http://unpkg.com/vue@2.5.17"></script>
インスタンス生成
コンストラクタを利用してオブジェクト生成
new Vue({
    ...
})
オプション指定して生成
new Vue({
    el:...
    //データを整形
    //UIの状態・データ
    data:{...}
    //Vueインスタンスをマウントする要素
    filter:{...}
    //イベント発生時の振る舞い
    methods:{...}
    //データから派生して算出される値
    computed:{...}
})
例としてこんな感じ
new Vue({
    //DOM要素のオブジェクトかCSSセレクタで指定
    el: '#app',
    //利用するデータを定義、プロパティって呼ばれる
    data: [
        {
            name: '鉛筆',
            price: 100,
            quantity: 3
        },
        {
            name: '消しゴム',
            price: 80,
            quantity: 2
        }
    ],
    //金額に対して3桁事にカンマを入れるフィルター
    filters: {
        numberWithDelimiter: function (value) {
            if (!value) {
                return '0'
            }
            return value.toString().replace(/(\d)(?=(\d{3})+$)/g, '$1,')
        }
    },
    //データを元に
    computed: {
        totalPrice: function () {
            return this.items.reduce(function (sum, item) {
                return sum + (item.price * item.quantity)
            }, 0)
        },
        
    //methodsはまだ勉強してないから後で書く
})
$watchによるインスタンスの変更検知
vm = new Vue({...}) //中身は↑にある例みたいな感じで
    vm.$watch(function () {
        //鉛筆の個数
        return this.item[0].quantity
        },function (quantity) {
        //鉛筆の個数が変更されたら呼ばれる
        console.log(quantity)
})
watch()の第一引数に関し対象の値を返す関数、第二引数に値が変わった場合に呼ばれるコールバック関数を記載する。
鉛筆の個数の変化が観測されたらコンソールログに変更後の値が表示される。
開発の動作確認やログ出力に便利らしい。
データバインディング
データが決まればビューの内容が決定される。
鉛筆の個数が1本増えることで購入合計金額が1本分自動的に増える。
この仕組みをデータバインディングというらしい。
テンプレートへのデータ展開
vueインスタンスのデータを使って実際に画面上に表示される部分を書いていく
データを表示させる基盤部分をテンプレートというらしい。
Mustache記法によるデータの展開
テキストコンテンツへのデータ展開。
**{{}}**←を使ってデータをテキストとして扱う
<p>{{ items[0].name }}: {{ items[0].price }} × {{ items[0].quantity }}</p>
これでvueインスタンスのデータを参照できる。
表示はこんな感じ↓
鉛筆: 100 × 3
ディレクティブによるHTML要素の拡張
DOM要素の属性に展開していく。
例として**v-bind:属性名="データを展開した属性値"**というものをご紹介
<button id="b-button" v-bind:title="loggedInButton">購入</button>
<script>
    new Vue({
    el: '#b-button',
    data:{
        loggedInButton: 'ログイン済のため購入できます。'
    }
})
</script>
これで購入ボタンにマウスオーバーすると「ログイン済のため購入できます。」っていう吹き出し?が出てくる。
JavaScript式の展開
JavaScript式も使える
<p>{{ items[0].price * items[0].quantity}}</p>
「鉛筆の価格*鉛筆の個数」を表示↓
300
簡単なJavaScript式ならこれでいいけど、難しめな式は保守性が下がるので算出プロパティ(computed)やメソッド(method)を利用するのが吉。
フィルタ(filters)
汎用的なテキストフォーマット書影を適用する仕組み。
例えば金額の表示を「1000」→「1,000」としたいとき。
new Vue({
    ...
    filters: {
        //numberWithDelimiterはフィルタ名。任意。
        numberWithDelimiter: function (value) {
            if (!value) {
                return '0'
            }
            return value.toString().replace(/(\d)(?=(\d{3})+$)/g, '$1,')
        }
    }
})
テンプレート側は
{{1000 | numberWithDelimiter}}
これでフォーマットが整形される。
算出プロパティ(computed)
データから派生するデータをプロパティとして公開する仕組み。
データに対して何かしらの処理を加えたものをプロパティにしたい(インスタンスに持たせて参照できるようにしたい)時に使う。
例えばデータとして存在している鉛筆の個数と金額から計算される合計金額をプロパティにしたい場合。
computed: {
    totalPrice: function () {
        return this.items.reduce(function (sum, item) {
            return sum + (item.price * item.quantity)
        }, 0)
    }
}
テンプレート側は
<p>{{totalPrice}}</p>
これだけで合計金額を参照できる。めっちゃ便利。
ちなみに算出プロパティに依存した算出プロパティも定義できる。
例えば合計金額を税込合計金額したプロパティ
computed: {
    //合計金額
    totalPrice: function () {
        return this.items.reduce(function (sum, item) {
            return sum + (item.price * item.quantity)
        }, 0)
    },
    //税込合計金額
    totalPriceWithTax: function () {
        return Math.floor(this.totalPrice * 1.08)
    }
}
テンプレート側の記述もすっきりするし使いまわせるので積極的に使いたい。
reduce
余談だけどreduceという関数を知らなかったので調べたら便利なヤツだった。
goでいうと
array := []int{1,2,3,4}
var sum int
for i, arr := range array {
    sum += arr[i]
}
イメージ的にはこんな感じのことしてるっぽい。便利。(間違ってたらすみません)
thisによる参照
↑で紹介したコードにも記載されているが、thisが指すのはVueインスタンス自身。
//このコードが記載されているインスタンスのitems[0].priceを参照
this.items[0].price
ディレクティブ
標準のHTMLに独自の属性を追加してDOMの操作を行う。この属性をディレクティブ(directive)という。
条件付きレンダリング(v-if,v-show)
v-if
<p v-if="引数">
    //真なら表示、偽なら非表示
</p>
v-show
<p v-show="引数">
    //真なら表示、偽なら非表示
</p>
シンプル。
どちらも同じ結果が得られるが、使い分けは以下の通り。
・頻繁に表示・非表示を切り替えるならv-show
・ほとんど表示・非表示を切り替えない場合v-if
v-showはdisplayプロパティの値を変更する、v-ifはDOM要素を追加・削除するという違いがある。一般的に後者の方がレンダリングのコストが高い。
v-showは変更時にスタイルの変更だけを行うことでコストを抑え、v-ifはログイン状態を取得した後に表示するような場合、不要であればそもそも表示させない選択をさせることでレンダリングのコストを抑えることができる(らしい)。
<p v-show="!canBuy">
    {{1000 | numberWithDelimiter}}円以上からご購入いただけます
</p>
~~
        
computed: {
    ...
    canBuy: function () {
        return this.totalPrice >= 1000
    }
}
クラスとスタイルのバインディング(v-bind)
上の方で一度紹介したv-bindを利用する。
下記のような書き方。
v-bind:class="オブジェクトまたは配列"
v-bind:style="オブジェクトまたは配列"
クラス
属性値にオブジェクトを指定した場合、値が真のプロパティ名をclass属性値として反映する。
<p v-bind:class="{shark: true, mecha: false}"></p>
この場合sharkクラスのみが適用されている。
下記は、購買金額に達していない場合クラスを付与して文字を赤色に変更するサンプル
<p v-bind:class="{error: !canBuy}">
    1000円以上からご購入いただけます
</p>
<style>
.error {
    color:red
}
</style>
適用させたいクラスが多く複雑化した場合、算出プロパティとして定義するのがベターらしい。
<p v-bind:class="errorMessageClass">
    1000円以上からご購入いただけます
</p>
~~
computed: {
    ...
    errorMessageClass: function () {
        return {
            error: !this.canBuy
        }
    }
}
スタイル
属性値のオブジェクトのプロパティがスタイルのプロパティと対応して、インラインスタイルとして反映される。
下記の場合「
a
」になる<p v-bind:style="{color: 'red'}">a</p>
式と組み合わせて使うことも可能。
下記はcanBuyが偽の場合、属性値は「"border: 1pix solid red; color: red;"」となる。
<p v-bind:style="{border: (canBuy ? '' : '1px solid red'), color: (canBuy ? '' : 'red')}">
    1000円以上からご購入いただけます
</p>
長いし可視性悪くなりそうだから絶対テンプレートとして書きたくない…
クラス同様算出プロパティ化するのがベター。
<p v-bind:style="errorMessageStyle">
    1000円以上からご購入いただけます
</p>
computed: {
    ...
    errorMessageStyle: function () {
        return {
            border: this.canBuy ? '' : '1px solid red',
            color: this.canBuy ? '' : 'red'
        }
    }
}
v-bindの省略記法
下記のように省略可能
v-bind:disabled → :disabled
<p :class="{error: !canBuy}">
    1000円以上からご購入いただけます
</p>
リストレンダリング(v-for)
v-forディレクティブで、配列あるいはオブジェクトのデータをリストレンダリング(繰り返しレンダリング)できる。
v-for="要素 in 配列"
**v-bind:key=~**で一意なキーを各要素に与える。
<ul>
    <li v-for="item in arr" v-bind:key="item">{{item}}</li>
</ul>
new Vue({
    data: ['い', 'ろ', 'は']
    ...
})
下記のように表示される。
・い
・ろ
・は
現在の要素のインデックスが必要な場合は下記のように記述する。
<ul>
    <li v-for="(item, index) in arr" v-bind:key="item">{{index}} {{item}}</li>
</ul>
イベントハンドリング(v-on)
v-onディレクティブを利用することでイベントが発生した沖に属性値の式を実行することができる。
v-on:イベント名="式として実行したい属性"
下記のように記述すると個数入力欄が表示され、個数を増減することで表示される個数・金額が増減するようになります。
<ul>
    <li v-for="item in items" v-bind:key="item.name">
        {{item.name}}の個数: <input type="number" v-on:input="item.quantity = $event.target.value" v-bind:value="item.quantity" min="0">
    </li>
</ul>
入力欄の値が入力された(=イベント発生)時に、個数が入力されたターゲット(要素)のvalue(=item.quantity)の値を、item.quantityプロパティにセットする、といった感じ。
他のディレクティブと同じように後述するメソッドを使うのがベター。
v-onの省略記法
下記のように省略可能
v-on:click → @click
<button :disabled="!canBuy" @click="doBuy">購入</button>
フォーム入力バインディング(v-model)
一般的にフォームは複数の入力部品から成り立っている。これら一つ一つにv-on:inputとv-bind:valueを記述するのはなかなかに面倒。v-modelを利用することでその手間を省き簡潔に記述することができる。いわゆる双方向バインディングを実現するディレクティブ。
<input type="number" v-model="item.quantity" min="0">
これは下記と同じ動きをする。
<input type="number" v-on:input="item.quantity = $event.target.value" v-bind:value="item.quantity" min="0">
修飾子による動作の変更
v-modelを使った動作変更ではinputイベントを置き換えたが、inputイベントではなくchangeイベント(v-on:change)と同じ振る舞いを実現したい場合、ディレクティブの挙動を変更する**修飾子(Modifier)**という仕組みを利用する。
修飾子はディレクティブ.修飾子の形で利用する。
<input type="number" v-model.lazy="name" min="0">
修飾子はいくつかのディレクティブにのみ存在する。(詳しくは公式参照)
メソッド(methods)
メソッドはVueインスタンスのメソッドとして機能する。Vueインスタンスのmethodsプロパティで定義する。
methodsはデータの変更やサーバーにHTTPリクエストを送る際に用いる。
new Vue({
    ...
    methods: {
        メソッド名: function() {
            //処理
        }
    }
})
よくある利用法はv-onディレクティブの属性値にバインディングして、ビューのイベントが発生した時に呼び出す形。
テンプレート内でも**{{メソッド名()}}**のように記述して呼び出すことができる。
<button v-bind:disabled"!canBuy" v-on:click="doBuy">購入</button>
上記のようにメソッド名を指定した場合、イベントオブジェクトがデフォルトの引数として渡される。
このイベントオブジェクトは式の中で**$event**という特別な変数で参照可能。
式で書いた場合下記のようになる。
<button v-bind:disabled"!canBuy" v-on:click="doBuy($event)">購入</button>
イベントオブジェクト以外にもテンプレートから引数を渡したい場合は式で記述するとよい。
補足:イベントオブジェクトにはイベントが発生した要素や座標などの情報が含まれている。
算出プロパティとメソッドの使い分け
算出プロパティとメソッドはどちらも関数の形をとる点は同じである。
算出プロパティは依存しているデータが変更されない限り、一度計算した結果をキャッシュする特徴を持っている。
つまり、複数個所で同じ算出プロパティを利用したとしても、データの値が変更されていない限り利用する度に再計算は行わない。
よって、何度も計算を行わず計算した結果を再利用するものは算出プロパティとして定義するのが良い。(サンプルにあるtotalPriceとか)
注意点として、計算のキャッシュは依存するデータにもとづいて行われる。
つまり、Vueインスタンスのdataプロパティが更新されない限り更新されない
...
data: {
    messagePrefix: 'Hello'
},
computed: {
    message: function () {
        var timestamp = Data.now() {
            return this.messagePrefix + ', ' + timestamp
        }
    }
}
上記の算出プロパティはあいさつのメッセージとタイムスタンプを返す。
Data.now()で実行時の日時が取得される(=実行の度に値が変わるはずである)ため、数秒待った後に参照すると更新された最新のタイムスタンプが表示されると誤解されがちだが変わらない。VueインスタンスのdataプロパティであるmessagePrefixが更新されて初めてタイムスタンプの値も更新された最新の状態に変更される。
サンプル
ここまで記載してきたサンプルをまとめたものが以下。
<!DOCTYPE html>
<meta charset="utf-8" />
<title>はじめてのVue.js</title>
<script src="http://unpkg.com/vue"></script>
<div id="app">
    <ul>
        <li v-for="item in items" v-bind:key="item.name">
            {{item.name}}の個数: <input type="number" v-model="item.quantity" min="0">
        </li>
    </ul>
    <hr>
    <div v-bind:style="errorMessageStyle">
        <ul>
            <li v-for="item in items" v-bind:key="item.name">
                {{item.name}}: {{item.price}} × {{item.quantity}} ={{item.price * item.quantity | numberWithDelimiter}}円
            </li>
        </ul>
        <p>{{items[0].name}}: {{items[0].price}} × {{items[0].quantity}}</p>
        <p>小計: {{totalPrice | numberWithDelimiter}}円</p>
        <p>合計(税込): {{totalPriceWithTax | numberWithDelimiter}}円</p>
        <p v-show="!canBuy">
            {{1000 | numberWithDelimiter}}円以上からご購入いただけます
        </p>
        <button v-bind:disabled="!canBuy" v-on:click="doBuy">購入</button>
    </div>
</div>
<script>
var items = [
    {
        name:"鉛筆",
        price:100,
        quantity:2
    },
    {
        name:"消しゴム",
        price:80,
        quantity:2
    },
    {
        name:'ノート',
        price: 100,
        quantity: 1
    }
]
vm = new Vue({
    el: '#app',
    data: {
        items: items,
        arr: ['い', 'ろ', 'は']
    },
    filters: {
        numberWithDelimiter: function (value) {
            if (!value) {
                return '0'
            }
            return value.toString().replace(/(\d)(?=(\d{3})+$)/g, '$1,')
        }
    },
    methods: {
        doBuy: function () {
            //本来はここでサーバーと通信を行う
            alert(this.totalPriceWithTax + '円のお買い上げ!')
            this.items.forEach(function (item){
                item.quantity = 0
            })
        }
    },
    computed: {
        totalPrice: function () {
            return this.items.reduce(function (sum, item) {
                return sum + (item.price * item.quantity)
            }, 0)
        },
        totalPriceWithTax: function () {
            return Math.floor(this.totalPrice * 1.08)
        },
        canBuy: function () {
            return this.totalPrice >= 1000
        },
        errorMessageClass: function () {
           return {
                error: !this.canBuy
           }
        },
        errorMessageStyle: function () {
            return {
                border: this.canBuy ? '' : '1px solid red',
                color: this.canBuy ? '' : 'red'
            }
        }
    }
})
</script>
ライフサイクルフック
Vueインスタンスの生成から消滅までに至るまでのライフサイクルにおいて、特定のタイミングで呼ばれる処理を事前登録し自動で呼び出すことができる。これをライフサイクルフックという。
| フックの名前 | フックが呼ばれるタイミング | 
|---|---|
| beforeCreate | インスタンスが生成され、データが初期化される前 | 
| created | インスタンスが生成され、データが初期化された後 | 
| beforeMount | インスタンスがDOM要素にマウントされる前 | 
| mounted | インスタンスがDOM要素にマウントされた後 | 
| beforeUpdate | データが変更され、DOMに適用される前 | 
| updated | データが変更され、DOMに適用された後 | 
| beforeDestroy | Vueインスタンスが破棄される前 | 
| destroyed | Vueインスタンスが破棄された後 | 
createdフック
DOM要素はインスタンスに紐づいていない段階のため、DOM APIのgetElementByIdやquerySelectorAll等は使えない。
Web APIと通信してデータに関する処理を開始したり、setIntervalやsetTimeoutで繰り返し実行したりするタイマー処理を開始するポイントとして利用される。
mountedフック
DOM APIが利用できるようになるので、DOM操作やイベントリスナーの登録が必要な場合にはこのフックで行う。
beforeDestroyフック
イベントリスナーの破棄やタイマー処理のクリアといった後始末をここで行う。
コンポーネント
UIを構成する部品をコンポーネントという。
Googleのトップページでいうと、ヘッダー・検索フォーム・フッターという要素があり、ヘッダーにはGmailへのリンク、画像検索へのリンク・ログインボタンから構成されている。これらの部品をUIコンポーネントと呼ぶ。
複数ページで繰り返し使われるヘッダー等はコンポーネント化し再利用するべきである。
コンポーネント化のメリット
- 再利用性が高まり開発効率が上げられる
- 既に使用されているコンポーネントを再利用することで品質を保てる
- コンポーネントを適切に区切り疎結合にした場合保守性が高まる
- カプセル化されて開発で意識すべき範囲を弁体できるようになる
基本的な使い方
**Vue.component()**の第一引数にコンポーネント名を、第二引数にコンポーネントの内容などのオプションを与える。
Vue.component('lit-item', {
    templete: '<li>foo</li>'
})
このコンポーネントは下記のように使える。
<ul id="example">
    <list-item></list-item>
</ul>
~~
    
Vue.component('list-item',{
    template: '<li>foo</li>'
})
new Vue({ el: '#example' })
ルートVueインスタンス(ul以下)内では、コンポーネント名をタグ名として記載するだけでコンポーネントの内容を流し込める。
コンポーネントはVueインスタンスを生成する前に記述しなければいけない。
オプションとしては下記のものが使える。
| オプション名 | 用途 | 
|---|---|
| data | UIの状態・データ | 
| filters | データを整形 | 
| methods | イベントが発生した時などの振る舞い | 
| computed | データから派生して算出される値 | 
| template | コンポーネントのテンプレート | 
| props | 親から子へのデータの受け渡し | 
| created他 | ライフサイクルフック | 
Vueコンポーネントは再利用可能なVueインスタンス
Vueコンポーネント中ではVueインスタンスで学んだテンプレート構文も使える。
Vue.component('list-item',{
    template: '<li>foo {{contents}}</li>',
    data: function () {
        return {contents: 'bar'}
    }
})
ただし、コンポーネント内のdataオプションは関数でなければいけない。
再利用される度に独立した値を保持させるため。この仕組みがなければ、一つの箇所での変更によって他の箇所に影響が及んでしまう。
コンポーネントの親と子
コンポーネントには親子関係が存在する。
コンポーネントを利用する側が親、利用される側が子。
<main id="main">
    <fruits-list></fruits-list>
</main>
~~
    
Vue.component('fruits-list-title', {
    template: '<h1>フルーツ一覧</h1>'
})
Vue.component('fruits-list', {
    template: '<div><fruits-list-title></fruits-list-title></div>'
})
new Vue({el: '#main'})
コンストラクタベースの定義
**Vue.extend()**というグローバルなAPIを利用して、ベースのVueコンストラクタを継承したサブクラスコンストラクタを作成できる。
これを用いてコンポーネントを作ることが可能。
Vue.extend()で定義したコンポーネントを直接特定の要素にマウントするには**$mount**を使う。
var FruitsListTitle = Vue.extend({
    template: '<h1>フルーツ一覧</h1>',
})
Vue.component('fruits-list', FruitsListTitle)
new Vue({el: '#main'})
ということでテンプレートベースの方法と上記コンストラクタベースの方法があるらしい。
が、基本的にはカスタムタグを定義してVue.componentで定義するのがおすすめとのこと。
ローカルコンポーネントの定義
ある特定のコンポーネントの中でしか使えないローカルコンポーネントというものがある。
VueインスタンスやVueコンポーネントのオプションにcomponentsオブジェクトを定義し登録する。
<div id="fruits-list">
    <fruits-list-title></fruits-list-title>
</div>
~~
    
new Vue({
    el: "#fruits-list",
    components: {
        'fruits-list-title': {
            template: '<h1>フルーツ一覧</h1>'
        }
    }
})
コントラクタベースのテンプレートも指定可能。
var FruitsListTitle = Vue.extend({
    template: '<h1>フルーツ一覧</h1>',
})
new Vue({
    el: "#fruits-list",
    components: {
        'fruits-list-title': FruitsListTitle
    }
})
テンプレートを構築するその他の手段
上記で説明したもの以外にもあるらしい。
- text/x-templete
- インラインテンプレート
- 描画関数
- JSX
- 単一コンポーネント
サンプルだけご紹介
text/x-templete
<script type="text/x-template" id="fruits-list-title">
    <h1>フルーツ一覧</h1>
</script>
~~
    
    Vue.component('fruits-list-title', {
    template: '#fruits-list-title'
})
描画関数
<div id="app">
    <input-data-with-today></input-data-with-today>
~~
Vue.component('input-data-with-today', {
    render: function (createElement) {
        return createElement(
            'input',
            {
                attrs: {
                    type: 'data',
                    value: new Data().toIS0String().substring(0,10)
                }
            }
        )
    }
})
命名について
コンポーネントの命名はケバブケースで定義するのが推奨されてる。
//ケバブケース
kebab-fruits-list
//パスカルケース
PascalFruitsList
コンポーネント間の通信
親→子
親コンポーネントから子コンポーネントへデータを渡すには、propsオプションを利用する。
Vue.component(コンポーネント名,{
    props: {
        親から受け取る属性名:{
            type: StringやObjectなどのデータ型,
            default: デフォルト値,
            required: 必須かどうかの真偽値,
            validator: バリデーション用の関数
        }
    }
    ...templete内で「親から受け取る属性」が使える
})
propsにキャメルケースでitemNameと書いた場合、テンプレート側の属性名にはケバブケースでitem-nameと書く。
<div id="app">
    <item-desc v-bind:item-name='myItem'></item-desc>
</div>
~~
        
Vue.component('item-desc', {
    props: {
        itemName: {
            type: String
        }
    },
    template: '<p>{{itemName}}は便利です。</p>'
})
new Vue({
    el: '#app',
    data: {
        myItem: 'pen'
    }
})
子→親
カスタムイベントを使用する。
| 用途 | インターフェイス | 
|---|---|
| イベントのlisten | $on(eventName) | 
| イベントのtrigger | $emit(eventName) | 
ボタンを押すとコンポーネントのaddToCart()メソッドが呼ばれ、その中でincrementというカスタムイベントが発行される。
var counterButton = Vue.extend({
    template: '<span>{{counter}}個<button v-on:click="addToCart">追加</button></span>',
    data: function () {
        return {
            counter: 0
        }
    },
    methods: {
        addToCart: function () {
            this.counter += 1
            this.$emit('increment')
        }
    }
})
親コンポーネント側ではv-on:increment(increment)でincrementイベントをlistenしており、ボタンを押したときに親のincrementCartStatus()メソッドが呼ばれる。
new Vue({
    el: '#fruits-counter',
    components: {
        'counter-button': counterButton
    },
    data: {
        total: 0,
        fruits: [
            {name: '梨'},
            {name: 'イチゴ'}
        ]
    },
    methods: {
        incrementCartStatus: function () {
            this.total += 1
        }
    }
})
<div id="fruits-counter">
    <div v-for="fruit in fruits">
        {{fruit.name}}: <counter-button v-on:increment="incrementCartStatus()"><counter-button>
    </div>
    <p>合計: {{total}}</p>
</div>
子から親のネイティブDOMイベントを取得したい場合
子コンポーネントの中で**.native修飾子**を使う。
<my-component v-on:click.native="someMethod"></my-component>
propsの値に関して双方向バインディング
基本的にデータは親から子への一方通行であるが、.sync修飾子を使うことで実現可能。
サンプル
ログインフォーム
<script src="http://unpkg.com/vue"></script>
<div id="login-example">
    <user-login></user-login>
</div>
<script type="text/x-template" id="login-template">
    <div id="login-template">
        <div>
            <input type="text" placeholder="ログインID" v-model="userid">
        </div>
        <div>
            <input type="password" placeholder="パスワード" v-model="password">
        </div>
        <button @click="login">ログイン</button>
    </div>
</script>
<script>
Vue.component('user-login', {
    template: '#login-template',
    data: function () {
        return {
            userid: '',
            password: ''
        }
    },
    methods: {
        login: function () {
            auth.login(this.userid, this.password)
        }
    }
})
var auth = {
    login: function (id, pass) {
        window.alert("userid:" + id + "\n" + "password:" + pass)
    }
}
new Vue({
    el: "#login-example"
});
</script>
Vue Router
Vue.jsの公式プラグイン。シングルページアプリケーション(SPA)構築のためのルーティング(ページ遷移など)プラグイン。
導入
<script src="http://unpkg.com/vue"></script>
//バージョン未指定
<script src="http://unpkg.com/vue-router"></script>
//バージョン指定
<script src="http://unpkg.com/vue-router@3.0.1"></script>
コンストラクタ
//ルート定義
{
    path: 'someurl', //URLを指定 ファイル名#/someurlでアクセスできる
        component: {
            template: '...' //コンポーネント構文またはコンストラクタベースのコンポーネントを用いる
        }
}
//ルーターコンストラクタ これをnew Vue()に渡す
new VueRouter({
    routes: [ ] //ルート定義を配列で渡す
})
サンプル
script側
<!--Vue.jsとVue Routerを読み込んでおく -->
<script>
//ルートオプションを渡してルーターインスタンスを生成
    var router = new VueRouter({
        //コンポーネントをマッピングしたルート定義を配列で渡す
        routes: [
            {
                path: '/top',
                component: {
                    template: '<div>トップページです。</div>'
                }
            },
            {
                path: '/users',
                component: {
                    template: '<div>ユーザー一覧ページです。</div>'
                }
            }
        ]
    })
    //ルーターのインスタンスをrootとなるVueインスタンスに渡す
    var app = new Vue({
        router: router
    }).$mount('#app')
</script>
HTML側で、Vueインスタンスをマウントする要素の他に、ルート定義で書いたコンポーネントを実際に反映させるrouter-view要素を使用する。ルート内でマッピングしたコンポーネントが****の部分にレンダリングされる。
リンクの定義にはrouter-link要素を使用する。
<div id="app">
    <router-link to="/top">トップページ</router-link>
    <router-link to="/users">ユーザー一覧ページ</router-link>
    <router-view></router-view>
</div>
URLパラメーターとパターンマッチング
アクセスするURLのパターンマッチングによりパラーメータを受け渡したい場合、/user/:userIdという形式で記述する。
パラメーターはコンポーネント内の**$route.params.userId**で受け取ることができる。
var router = new VueRouter({
    router: [
        {
            path: '/user/:userId',
            component: {
                template: '<div>ユーザーIDは {{$route.params.userId}} です。</div>'
            }
        }
    ]
})
名前付きルート
Vue Routerではルートを定義した時に名前を付与して、その名前をHTML側()で指定してページ遷移を実行できる。
ユーザーIDを元にURLを組み立てる場合、静的にHTMLに記載することはできないが、ルートの名前を付けられれば問題は解決する。
var router = new VueRouter({
    routes: [
        {
            path: 'user/:userId',
            name: 'user',
            component: {
                template: '<div>ユーザーIDは {{$route.params.userId}} です。'
            }
        }
    ]
})
<router-link :to"{ name: 'user', params: { userId: 123}}>ユーザー詳細ページ</router-link>"
router.pushを使った遷移
router.push({ name: 'user', params: { userId: 123}})
フック関数
ページ遷移が実行される前後に処理を追加できる。
リダイレクトやページ遷移前の確認などはフック関数で実装する。
グローバルのフック関数
全てのページ遷移に対して設定できるフック関数。router.beforeEachに関数をセットすると、ページ遷移が起こる直前にその関数が実行される。
引数のtoとfromには現在遷移しようとしているルーティングの遷移先ルートと遷移元ルートの情報が入る。
このtoとfromに格納されるルートは、マッチしたルートのパスやコンポーネントの情報を持っている。
router.beforeEach(function (to, from next) {
    //ユーザー一覧ページにアクセスした時に/topへリダイレクトする例
    if (to.path === '/users') {
        next('/top')
    } else {
        //引数なしでnextを呼び出すと通常通りの遷移が行われる
        next()
    }
})
ルート単位のフック関数
特定のルート単位でフックを追加するには、Vue Router初期化時のルート定義の時個別に設定する。
ルート定義にbeforeEnterを記述することで、ルーティング前のフックを追加する。
var router = new VueRouter({
    router: [
        {
            path: '/users',
            component: UserList,
            beforeEnter: function (to, from, next) {
                // /users?redirect=true でアクセスされた時だけtopにリダイレクトする
                if (to.query.rediect === 'true') {
                    next('/top')
                } else {
                    next()
                }
            }
        }
    ]
})
コンポーネント内のフック関数
コンポーネントのオプションとしてbeforeRouteEnterを使ってデータを取得する例。
var UserList = {
    template: '#user-list',
    data: function () {
        return {
            users: function () {
                users: function () { return []},
                error: null
            }
        }
    },
    
    //ページ遷移が行われて、コンポーネントが初期化される前に呼び出される
    beforeRouteEnter: function (to, from, next) {
        getUsers((function (err, users) {
            if (err) {
                this.error = err.toString()
            } else {
                //nextに渡すcallbackでコンポーネント自身にアクセス可
                next(function (vm) {
                    vm.users = users
                })
            }
        }).bind(this))
    }
}
javascript使ったことなくて関数意味不明だったので参考に
単一ファイルコンポーネント
Vue.jsのコンポーネントを単独のファイルとして作成する機能。
.vue拡張子のファイル内に<template>,<script>,<style>のブロックで構成されたHTMLベースの構文で定義する。
<template>
    <div id="app">
        <h1 class="greeting">あいさつ: {{msg}}</h1>
        <Content/>
    </div>
</template>
<script>
import Content from './content.vue'
export default {
    components: {
        Content
    },
    data () {
        return {msg: 'こんにちは!'}
    }
}
</script>
<style>
.greetin {
    color: #42b983;
}
</style>
単一コンポーネントファイルのことをSFC(Single File Component)とかVueコンポーネントとか呼んだりする。
Vue.compnent/componentsオプションによるコンポーネント化に比べ、単一コンポーネントファイルではスタイルも含めてコンポーネントとして定義できる。
ライブラリや他のコンポーネントのインポートはこのブロック内で行う。
<script>
import MyModel from 'my-model'
...
</script>
Vue.jsアプリケーションで単一ファイルコンポーネントを利用するためにはエクスポートする必要がある。
<script>
...
export default {
    ...
}
</script>
styleブロック
単一コンポーネント内には複数のstyleブロックを含むことができる。
styleブロックは、単一ファイルコンポーネント毎にスタイルをカプセル化できる。
<!-- カプセル化されたスタイル -->
<style scoped>
.message {
    color: #42b983;
}
<!-- グローバルなスタイル -->
<style>
.theme {
    color: ##34495e;
}
単一ファイルコンポーネントのビルド
単一ファイルコンポーネントはVue.js独自の仕組みであるため、Webブラウザで動作可能なように変換する必要がある。
Vue CLIのインストール
Vue.js向けのアプリケーション開発環境をセットアップするなどの機能を提供する公式コマンドラインツール。
npmがインストールされてる前提で…
npm install -g @vue/cli @vue/cli-service-global
(参考書のまま書いてるだけなので詳しくは調べていただいて…。)
動作させてみる
Vue CLIのserveサブコマンドで動かす。vue serveは内部ではwebpackとVue Loaderを利用していてwebpackの設定なしでもVue.jsアプリケーションとしてビルドできる大変便利なコマンドらしい。
<template>
    <p class="message">メッセージ: {{msg}}</p>
</template>
<script>
export default {
    data () {
        return {msg: 'こんにちは!'}
    }
}
</script>
<style>
.message {
    color: #42b983;
}
</style>
コマンドラインで実行
$ vue serve hello.vue --open
--openでブラウザが自動で開いて結果が表示される。
外部ファイルのインポート
各ブロックごとにsrc属性で外部ファイルの内容をインポートできる。
<template src="./template.html"></template>
<script src="./script.js"></script>
<style src="./style.css"></style>
Vuex
Vue.jsアプリケーション向けの状態管理ライブラリ
導入
$ npm install vuex
import文を書いてVuexを読み込む。プロジェクトのVue.jsに対してVuexを登録する。
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
ストア
ストアは主にアプリケーションの状態を保持する役割を担う。Vuexの根幹。
ストアの作成と代入
const store = new Vuex.Store({ /* オプション */})
ストアはアプリケーションの中で一つのみ存在するようにする。
ストアの構成要素
- アプリケーションのステート(state)
- ステートの一部や、ステートから計算された値を返すゲッター(Getter)
- ステートを更新するミューテーション(Mutaion)
- 主にAjaxリクエストのような非同期処理や、localStorageへの読み書きのような外部APIとのやり取りを行うアクション(Action)
大規模アプリケーションでは、これらの構成要素をモジュール(Module)という単位で分割して見通しをよくする。
ステート
Vuexストアのステートはアプリケーション全体の状態を保持するオブジェクト。全てのステートは1つの木構造として表現される。
全てのデータをストアで管理するべきてはなく、あるコンポーネントでしか使わないデータはコンポーネントのdataオブジェクトで管理し、アプリケーション全体で使用されるデータはストア内で管理すべき。
stateオプションを指定して初期値を定義。ステートはstore.stateから参照可能。
import Vue from 'vue'
import vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
    //ステート
    state: {
        const: 0
    }
})
//ステートを参照
console.log(store.state.count)
ステートに適したデータ
- サーバーからデータを取得中かどうかを表すフラグ
- ログイン中のユーザーの情報など、アプリケーション全体で使用されるデータ
- ECサイトにおける商品の情報など、アプリケーションの複数の場所で使用される可能性のあるデータ
- コンポーネント側で持つべきデータ
- マウスポインタがある要素の上に存在するかどうかを表すフラグドラッグ中の要素の座標
- 入力中のフォームの値
ゲッター
ステートから別の値を算出するために用いられる。
gettersオプションに関数を持つオブジェクトを指定することでゲッターを定義する。
コンポーネントの算出プログラムと似た機能であるが、引数にステートと他のゲッターが渡され、それらを使って値を返す点が異なる。ゲッターはstore.gettersから参照可能。
import Vue from 'vue'
import vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
    //ステート
    state: {
        const: 0
    },
    getters: {
        squared: (state) => state.count * state.count,
        cubed: (state) => state.count * getters.squared
    }
})
console.log(store.getters.cubed)
ゲッターはcomputedオプションと同様にキャッシュされる。
ミューテーション
ステートを更新するために用いられる。
Vuexでは規約としてミューテーション以外がステートの更新を行うことを禁止している。
mutationsオプションにミューテーション名をキーに持ち、ハンドラー関数を値に持つオブジェクトを指定して定義する。
ハンドラー内では第一引数に渡されたステートを更新する。store.commtにミューテーション名を与えて呼び出す。
import Vue from 'vue'
import vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
    //ステート
    state: {
        const: 0
    },
    mutations: {
        increment (state) {
            state.count = state.count + 1
        }
    }
})
console.log(store.state.count)
store.commit('increment')
console.log(store.state.count)
store.commitの第二引数になんらかの値を与えると、それがハンドラーの第二引数に渡される。この値をペイロードと呼ぶ。
ペイロードを使用することで、同じミューテーションでも渡す値によって異なるステートに更新できる。
import Vue from 'vue'
import vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
    //ステート
    state: {
        const: 0
    },
    mutations: {
        //ペイロード内の'amount'を使ってステートを更新
        increment (state, payload) {
            state.count = state.count + payload.amount
        }
    }
})
console.log(store.state.count)
store.commit('increment', {amount: 5})
console.log(store.state.count)
ミューテーションは全て同期的にしなければいけない
アクション
非同期処理や外部APIとの通信を行い、最終的にミューテーションを呼び出すために用いられる。
actionsオプションにアクション名をキーに持ち。ハンドラー関数を値に持つオブジェクトを指定することでアクションを定義する。
store.dispatchにアクション名を与えて呼び出す。
import Vue from 'vue'
import vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
    //ステート
    state: {
        const: 0
    },
    mutations: {
        increment (state) {
            state.count = state.count + 1
        }
    },
    actions: {
        incrementAction (ctx) {
            ctx.commit('increment')
        }
    }
})
console.log(store.state.count)
store.dispatch('incrementAction')
console.log(store.state.count)
ハンドラーの第一引数にステートではなく、コンテキストと呼ばれる特別なオブジェクトを渡す。
コンテキストには以下が含まれる。
- state: 現在のステート
- getters: 定義されているゲッター
- dispatch: 他のアクションを実行するメソッド
- commit: ミューテーションを実行するメソッド
stateやgettersはデータのロード中にはアクションの処理を行わないというような現在の状態に応じてアクションの処理を切り替える時に使う。
vuexで力尽きた完。