Vue + TypeScript + Babel + Bulma + webpackで作るSPAでレスポンシブなフロント開発3(JavaScript Framework編)

# リンク
- [開発環境構築編](https://qiita.com/va034600/items/a62238666149bcd9e719)
- [開発ツール編](https://qiita.com/va034600/items/c968b9f524f5d3127b93)
- [JavaScript Framework編](https://qiita.com/va034600/items/f203ea7a423967d5023c)
- [CSS Framework編](https://qiita.com/va034600/items/8541a076a9d3d140162f)
- [ビルド編](https://qiita.com/va034600/items/919ce3fa330f87f2a277)

[前回](https://qiita.com/va034600/items/c968b9f524f5d3127b93)は開発ツール周りを説明しました。
今回からは実際の実装部分についてです。
最初はJavaScript Frameworkについて説明します。

[github](https://github.com/va034600/sample-html/tree/master/project-base3)

JavaScript FrameworkはVue.jsを使います。
Vue.jsはReact.jsにかなり似ています。
あくまで個人的な感想ですが、React.jsの微妙な部分をよくしたのがVue.jsみたいな認識です。
（jsx、reduxとvuex、formまわりなどなど）
Vue.jsは単独でも動作しますが、ライブラリとしてVue-Router.jsやVuexを使うとさらに複雑なことができます。
今回はルーター、Vuex、イベント、双方向バインディング、ネットワークのサンプルソースを作りました。

# 開発用のhtml

開発用のhtmlは`webRoot/index.html`です。
`webRootLive/index.html`は本番用になる想定です。

http://localhost:3000/ にアクセスするとindex.htmlを開きます。

```javascript:webRoot/index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>

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

<script src="/public/main.min.js" type="text/javascript"></script>
<script src="/public/style.min.js" type="text/javascript"></script>

</body>
</html>
```

head　にはレスポンシブの設定viewportがあるのと、divが３つあって、webpack dev serverで作られたjs読み込んでいるくらいです。

# ルートファイル

## style.ts

開発用のファイルになります。
SASSファイルをJSとして読み込んでいます。

```typescript:style.ts
import "../scss/index.scss"
```

## main.ts

javascriptのルートファイルになります。
実際では、カテゴリーごとに複数のルートファイルを作ることになるかと思います。

```typescript:main.ts
/// <reference path="./index.d.ts"/>
import Vue from 'vue'
import Vuex from 'vuex'
import VueRouter from 'vue-router'

import {MainComponent} from "./view/component/MainComponent";
import {FooterComponent} from "./view/component/FooterComponent";
import {NavigationComponent} from "./view/component/NavigationComponent";

Vue.use(Vuex)
Vue.use(VueRouter)

function start() {
    const navigation = new NavigationComponent()
    navigation.$mount('#navigation')

    const footer = new FooterComponent()
    footer.$mount('#footer')

    const app = new MainComponent()
    app.$mount('#app')
}

start()
```

index.htmlの3つのdivにDOMを設定するようにしています。

# TypeScriptの型宣言

index.tsはTypeScriptの型宣言を設定するクラスです。
TypeScriptのアンビエント宣言を記述しています。


```typescript:index.ts
declare var process: any

declare module '*.vhtml' {
	import Vue = require('vue')
	interface WithRender {
		<V extends Vue>(options: Vue.ComponentOptions<V>): Vue.ComponentOptions<V>
		<V extends typeof Vue>(component: V): V
	}
	const withRender: WithRender
	export = withRender
}
```

htmlのLoaderのアンビエント宣言は[ここ](https://qiita.com/kaorun343/items/425fdfe55b81437cdb6a)を参考にさせていただきました。
拡張子はhtmlでもよかったのですが、vueのhtmlなのでvhtmlにしました。
別にhtmlでもいいんですがね。

# ルーター(vue router)

今回はmainRouter.tsのみになります。
ルーターを設定するとURLとComponentをマッピングしてくれます。

```typescript:mainRouter.ts
import VueRouter from 'vue-router'

import {SampleComponent} from "../component/SampleComponent";
import {PageNotFoundComponent} from "../component/PageNotFoundComponent";
import {SampleButtonComponent} from "../component/SampleButtonComponent";
import {SampleParameterComponent} from "../component/SampleParameterComponent";
import {SampleBindComponent} from "../component/SampleBindComponent";
import {SampleLoadComponent} from "../component/SampleLoadComponent";
import {SampleModalButtonComponent} from "../component/SampleModalButtonComponent";
import {SampleColumnComponent} from "../component/SampleColumnComponent";

export default new VueRouter({
    mode: 'history',
    routes:[
        { path: "/", component:  SampleComponent},
        { name: 'sample', path: '/sample', component: SampleComponent},
        { name: 'sample-button', path: '/sample-button', component: SampleButtonComponent},
        { name: 'sample-parameter', path: '/sample-parameter/:id', component: SampleParameterComponent},
        { name: 'sample-bind', path: '/sample-bind', component: SampleBindComponent},
        { name: 'sample-load', path: '/sample-load', component: SampleLoadComponent},
        { name: 'sample-modal-button', path: '/sample-modal-button', component: SampleModalButtonComponent},
        { name: 'sample-column', path: '/sample-column', component: SampleColumnComponent},
        { path: '*', component: PageNotFoundComponent},
    ]
})
```

URLとクラスのマッピングは以下のようになります。

`http://localhost:3000/`の場合は`SampleColumnComponent.ts`
`http://localhost:3000/sample-button`の場合は`SampleButtonComponent.ts`

どれにもマッチングしなかった場合は、404としてPageNotFoundComponentにマッピングするようになっています。

詳しくは[Vue Router HTML5 History モード
](https://router.vuejs.org/ja/essentials/history-mode.html)を参考にしてください。

# データストア(Vuex)

データストアのライブラリはVuexを使います。
データストアのライブラリを使わなくてもWEBアプリは開発できますが、SPAで開発する場合に必須ではと思います。
iOSやandroidアプリにライフサイクルがあるようにWEBアプリをSPAで作る場合にもライフサイクルがでてきます。
その際にビュー側のライフサイクルによるコンポーネントの生存期間は画面遷移の度などに削除されますが、データの保存期間は画面がリロードするまでずっとメモリに残り続けたりします。
SPAで作らない場合は、画面遷移でデータが消失され、コンポーネントとデータの削除のタイミングが同じになりがちです。
その際に、便利にデータを管理するのがVuexとなるのです。
たぶん。

## Vuex(Store)

MainStore.tsはVuexのクラスです。

```typescript:MainStore.ts
/// <reference path="../../index.d.ts"/>

import Vue from 'vue'
import Vuex, {MutationTree} from 'vuex'
import EnvironmentUtility from "../../utility/EnvironmentUtility";
import sampleLoadModule from '../module/SampleLoadModule'
import sampleBindModule from '../module/SampleBindModule'
import sampleModalModule from '../module/SampleModalModule'

const NAMESPACE = 'main';
const NAMESPACE_ACTION = `${NAMESPACE}/a/`
const NAMESPACE_GETTER = `${NAMESPACE}/g/`
const NAMESPACE_MUTATION = `${NAMESPACE}/m/`

Vue.use(Vuex)

export const ActionKey = {
}

export const GetterKey = {
}

export const MutationKey = {
}

export class MainState {
    public sampleLoad
    public sampleBind
    public sampleModal
}

function createStore():any{
    const mutations = {
    } as MutationTree<MainState>

    return new Vuex.Store<MainState>({
        state: new MainState(),
        mutations,
        strict: EnvironmentUtility.isDevelopment(),
        modules:{
            sampleLoad:sampleLoadModule,
            sampleBind:sampleBindModule,
            sampleModal:sampleModalModule,
        }
    })
}

export default createStore()
```

今回は３つモジュールを読み込んでいるだけのクラスになっています。
あと、strictの設定をしています。
strictの設定とは厳格モードにするためです。
本番では厳格モードを有効してはいけないです。
詳しくは[Vuex 厳格モード](https://vuex.vuejs.org/ja/strict.html)を参考にしてください。

### EnvironmentUtility.ts

開発用と本番用の設定を切り替えられるクラスです。
webpackでtrue falseの設定しています。
[開発ツール編]()を参考にしてください。

```typescript:EnvironmentUtility.ts
/// <reference path="../index.d.ts"/>

export default class EnvironmentUtility {
    static isDevelopment(){
        return !this.isProduction();
    }

    static isProduction(){
        return process.env.NODE_ENV === 'production';
    }
}
```

## Vue(コンポーネント)

Vueのコンポーネントは表示をコントロールするためのクラスです。

### 枠のコンポーネント

今回の枠のコンポーネントは`MainComponent.ts`になります。

```typescript:MainComponent.ts
/// <reference path="../../index.d.ts"/>
import Vue from 'vue'
import Component from 'vue-class-component'
import HtmlTemplate from '../../../vhtml/main.vhtml'

import store from '../store/MainStore';
import router from '../router/mainRouter'

@HtmlTemplate
@Component({
  router,
  store,
})
export class MainComponent extends Vue {
  message: string = 'Hello Vue.js typescript!!'
}
```

```vue:main.vhtml
<section class="section">
    <div class="container">
        <h1 class="title">
            {{message}}
        </h1>

        <router-view></router-view>
    </div>
</section>
```

MainComponent.tsのmesseageプロパティに文字列が定義されています。
main.vhtmlの{{message}}で出力されます。

あと、今回、vue-routerを使っているので、
パスに対してマッチしたコンポーネントをrouter-viewに描画してくれます。

### Vue Hello World

Vue Hello Worldって自分で書いておきながらよくわからないですが、
SampleComponent.tsはrouter-viewに簡単に描画したいと思った場合のサンプルになります。

```typescript:SampleComponent.ts
/// <reference path="../../index.d.ts"/>
import Vue from 'vue'
import Component from 'vue-class-component'
import HtmlTemplate from '../../../vhtml/sample.vhtml'

@HtmlTemplate
@Component({
})
export class SampleComponent extends Vue {
}
```

```vue:sample.vhtml
<div>
    <h1>sample</h1>
</div>
```

vhtmlには１つのタグをルートとしたdomしか記述できません。

### Vueイベント

今回はとりあえずクリックイベントのみ扱います。
http://localhost:3000/sample-button

```typescript:SampleButtonComponent.ts
/// <reference path="../../index.d.ts"/>
import Vue from 'vue'
import Component from 'vue-class-component'
import HtmlTemplate from '../../../vhtml/sample-button.vhtml'

@HtmlTemplate
@Component({
})
export class SampleButtonComponent extends Vue {
    clickByAlertButton() {
        alert("clicked test");
    }
}
```

```vue:sample-button.vhtml
<div>
    <h1>sample-click</h1>
    <a class="button is-light" @click="clickByAlertButton">test</a>
</div>
```

ポイントは`@click`です。

### Vueの双方向バインディング

SampleBindComponent.ts

http://localhost:3000/sample-bind

```typescript:SampleBindComponent.ts
/// <reference path="../../index.d.ts"/>
import Vue from "vue";
import Component from "vue-class-component";
import HtmlTemplate from "../../../vhtml/sample-bind.vhtml";
import {MutationKey} from "../module/SampleBindModule";

@HtmlTemplate
@Component({
})
export class SampleBindComponent extends Vue {
  get comment() {
    return this.$store.state.sampleBind.comment
  }

  set comment(value) {
    this.$store.commit(MutationKey.SET_COMMENT, value)
  }
}
```

SampleBindModuleへのアクセッサーです。

```typescript:SampleBindModule.ts
/// <reference path="../../index.d.ts"/>

import {ActionTree, GetterTree, ModuleTree, MutationTree} from "vuex";
import {MainState} from "../store/MainStore";

const NAMESPACE = 'sample-bind';
const NAMESPACE_ACTION = `${NAMESPACE}/a/`
const NAMESPACE_GETTER = `${NAMESPACE}/g/`
const NAMESPACE_MUTATION = `${NAMESPACE}/m/`

export const ActionKey = {
}

export const GetterKey = {
}

export const MutationKey = {
    SET_COMMENT: `${NAMESPACE_MUTATION}SET_COMMENT`,
}

function createStore(){
    class State {
        constructor(
            public comment: String = null,
        ) { }
    }

    const getters = {
    } as GetterTree<State, MainState>

    const actions = {
    } as ActionTree<State, MainState>

    const mutations = {
        [MutationKey.SET_COMMENT] (state, comment:String) {
            state.comment = comment;
        },
    } as MutationTree<State>

    return {
        state: new State(),
        getters,
        actions,
        mutations
    } as ModuleTree<State>
}

export default createStore()
```

SET_COMMENTというメソッドがあります。
stateのcommentを引数の値に変更しています。
<font color="Red">stateの変数はmutation内でしか変更してはいけません。
</font>これは大事です。
action内やgetter内、他では変更できないのです。
詳しくは[Vuex フォームの扱い](https://vuex.vuejs.org/ja/forms.html)を参考にしてください。

```vue:sample-bind.vhtml
<div>
    <h1>sample-bind</h1>
    comment: <input type="text" v-model="comment"><br>
    comment: {{comment}}<br>
</div>
```

ポイントはv-modelです。
これでSampleBindComponent.tsのcommentへアクセスしています。

### VueのパスパラメータとQueryパラメータ

SampleParameterComponent.ts

http://localhost:3000/sample-parameter/123
http://localhost:3000/sample-parameter/aaa?test=bbb

```typescript:SampleParameterComponent.ts
/// <reference path="../../index.d.ts"/>
import Vue from 'vue'
import Component from 'vue-class-component'
import HtmlTemplate from '../../../vhtml/sample-parameter.vhtml'

@HtmlTemplate
@Component({
})
export class SampleParameterComponent extends Vue {
    get pathVariable() {
        return this.$route.params.id;
    }

    get getVariable() {
        return this.$route.query.test;
    }
}
```

```vue:sample-parameter.vhtml
<div>
    <h1>sample-parameter</h1>
    <div>path variable:{{pathVariable}}</div>
    <div>get variable:{{getVariable}}</div>
</div>
```

http://localhost:3000/sample-parameter/123
http://localhost:3000/sample-parameter/aaa?test=bbb

１つ目のURLはid、１２３が取得できます。
2つ目のURLはaaaを取得しています。
さらに２つ目のURLにはクエリパラメータ付きです。
キーはtest値はbbbにとなっています。

# ネットワーク

ネットワーク周りをやる上で押さえておきたいのがPromiseとasync/awaitです。
SampleLoadComponent.tsではちょっと複雑に作ってしますが、引き続きよろしくお願いします。

```typescript:SampleLoadComponent.ts
/// <reference path="../../index.d.ts"/>
import Vue from 'vue'
import Component from 'vue-class-component'
import HtmlTemplate from '../../../vhtml/sample-load.vhtml'
import {ActionKey} from "../module/SampleLoadModule";

@HtmlTemplate
@Component({
})
export class SampleLoadComponent extends Vue {
  /**
   * ライフサイクル
   */
  async created(){
    console.log("load 1");
    console.log(`message:${this.$store.state.sampleLoad.message}`);
    await this.$store.dispatch(ActionKey.LOAD_MESSAGE)
    console.log(`message loaded:${this.$store.state.sampleLoad.message}`);
    console.log("load 2");
    await this.$store.dispatch(ActionKey.LOAD_TITLE, "fff")
    console.log("load 3");
  }

  /**
   * プロパティ
   */
  get message(){
    return this.$store.state.sampleLoad.message;
  }

  get title(){
    return this.$store.state.sampleLoad.title;
  }
}
```

```:ブラウザ実行時のconsoleログ
load 1
message:null
message loaded:sample message
load 2
load 3
```

consoleログには順番にログが出ています。
順番にログが出ているのがポイントです。
通信もしているのにです。

```vue:sample-load.vhtml
<div>
    <h1>sample-load</h1>
    message: {{message}}<br>
    title: {{title}}<br>
</div>
```

```typescript:SampleLoadModule.ts
/// <reference path="../../index.d.ts"/>

import {ActionTree, GetterTree, ModuleTree, MutationTree} from "vuex";
import SampleAPI from "../../network/api/SampleAPI";
import {SampleEntity} from "../../network/entity/SampleEntity";
import {MainState} from "../store/MainStore";
import {Sample2Parameter} from "../../network/parameter/Sample2Parameter";
import {Sample2Entity} from "../../network/entity/Sample2Entity";

const NAMESPACE = 'sample-load';
const NAMESPACE_ACTION = `${NAMESPACE}/a/`
const NAMESPACE_GETTER = `${NAMESPACE}/g/`
const NAMESPACE_MUTATION = `${NAMESPACE}/m/`

export const ActionKey = {
    LOAD_MESSAGE: `${NAMESPACE_ACTION}LOAD_MESSAGE`,
    LOAD_TITLE: `${NAMESPACE_ACTION}LOAD_TITLE`,
}

export const GetterKey = {
}

export const MutationKey = {
    SET_LOAD_MESSAGE: `${NAMESPACE_MUTATION}SET_LOAD_MESSAGE`,
    SET_LOAD_TITLE: `${NAMESPACE_MUTATION}SET_LOAD_TITLE`,
}

function createStore(){
    class State {
        constructor(
            public message: String = null,
            public title: String = null,
        ) { }
    }

    const getters = {
    } as GetterTree<State, MainState>

    const actions = {
        [ActionKey.LOAD_MESSAGE]: ({ commit }) => {
            return SampleAPI.getSample()
                .then((res) =>{
                    commit(MutationKey.SET_LOAD_MESSAGE, res)
                })
                .catch(function(error) {
                    alert(error);
                })
        },
        [ActionKey.LOAD_TITLE]: ({ commit }, p1) => {
            const parameter:Sample2Parameter = {
                abc:p1
            };
            return SampleAPI.getSample2(parameter)
                .then((res) =>{
                    commit(MutationKey.SET_LOAD_TITLE, res)
                })
                .catch(function(error) {
                    alert(error);
                })
        },
    } as ActionTree<State, MainState>

    const mutations = {
        [MutationKey.SET_LOAD_MESSAGE] (state, payload:SampleEntity) {
            state.message = payload.message;
        },
        [MutationKey.SET_LOAD_TITLE] (state, payload:Sample2Entity) {
            state.title = payload.title;
        },
    } as MutationTree<State>

    return {
        state: new State(),
        getters,
        actions,
        mutations
    } as ModuleTree<State>
}

export default createStore()
```

```typescript:SampleAPI.ts
import BaseAPI from "./BaseAPI";

import {SampleEntity} from "../entity/SampleEntity";
import {Sample2Entity} from "../entity/Sample2Entity";
import {Sample2Parameter} from "../parameter/Sample2Parameter";
import {WebUrlParams} from "../WebUrlParams";

export default class SampleAPI extends BaseAPI {
    static getSample() {
        const apiUrl = `/data1.json`;

        return this.get<SampleEntity>(apiUrl);
    }

    static getSample2(parameter:Sample2Parameter) {
        const apiUrl = `/data2.json`;

        var params = new WebUrlParams();
        params.append("abc", parameter.abc);

        return this.get<Sample2Entity>(apiUrl, params);
    }
}
```

```typescript:Sample2Entity.ts
export interface Sample2Entity {
    title
}
```

```typescript:Sample2Parameter.ts
export interface Sample2Parameter {
    abc
    ddd?
}
```

abcは必須、ddd?はnullable変数になります。

```typescript:BaseAPI.ts
import {WebParams} from "../WebParams";
import {WebUrlParams} from "../WebUrlParams";

export default class BaseAPI {
    static post<T>(apiUrl:string, params:WebParams = null){
        let body = params ? params.data : null;
        return this.send<T>(apiUrl, {
            credentials: 'include',
            method: "POST",
            body: body
        })
    }

    static get<T>(apiUrl:string, params:WebUrlParams = null){
        var url = apiUrl;
        if(params){
            url += "?" + params.data;
        }
        return this.send<T>(url, {
            credentials: 'include'
        })
    }

    private static send<T>(apiUrl, config){
        return new Promise<T>((resolve, reject) => {
            fetch(apiUrl, config).then(res => {
                res.json().then(json => {
                    if(res.status >= 400 && res.status < 500){
                        reject(res)
                        return
                    }

                    resolve(json);
                });
            }).catch(e => {
                reject(e)
            });
        });
    }
}
```

一番大事なところなのに説明が少ないところが多々ありますが、
とりあえずこんな感じでおわりにします。。

[github](https://github.com/va034600/sample-html/tree/master/project-base3)