リンク
前回は開発ツール周りを説明しました。
今回からは実際の実装部分についてです。
最初はJavaScript Frameworkについて説明します。
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を開きます。
<!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が3つあって、webpack dev serverで作られたjs読み込んでいるくらいです。
ルートファイル
style.ts
開発用のファイルになります。
SASSファイルをJSとして読み込んでいます。
import "../scss/index.scss"
main.ts
javascriptのルートファイルになります。
実際では、カテゴリーごとに複数のルートファイルを作ることになるかと思います。
/// <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のアンビエント宣言を記述しています。
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のアンビエント宣言はここを参考にさせていただきました。
拡張子はhtmlでもよかったのですが、vueのhtmlなのでvhtmlにしました。
別にhtmlでもいいんですがね。
ルーター(vue router)
今回はmainRouter.tsのみになります。
ルーターを設定するとURLとComponentをマッピングしてくれます。
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 モード
を参考にしてください。
データストア(Vuex)
データストアのライブラリはVuexを使います。
データストアのライブラリを使わなくてもWEBアプリは開発できますが、SPAで開発する場合に必須ではと思います。
iOSやandroidアプリにライフサイクルがあるようにWEBアプリをSPAで作る場合にもライフサイクルがでてきます。
その際にビュー側のライフサイクルによるコンポーネントの生存期間は画面遷移の度などに削除されますが、データの保存期間は画面がリロードするまでずっとメモリに残り続けたりします。
SPAで作らない場合は、画面遷移でデータが消失され、コンポーネントとデータの削除のタイミングが同じになりがちです。
その際に、便利にデータを管理するのがVuexとなるのです。
たぶん。
Vuex(Store)
MainStore.tsはVuexのクラスです。
/// <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()
今回は3つモジュールを読み込んでいるだけのクラスになっています。
あと、strictの設定をしています。
strictの設定とは厳格モードにするためです。
本番では厳格モードを有効してはいけないです。
詳しくはVuex 厳格モードを参考にしてください。
EnvironmentUtility.ts
開発用と本番用の設定を切り替えられるクラスです。
webpackでtrue falseの設定しています。
開発ツール編を参考にしてください。
/// <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
になります。
/// <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!!'
}
<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に簡単に描画したいと思った場合のサンプルになります。
/// <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 {
}
<div>
<h1>sample</h1>
</div>
vhtmlには1つのタグをルートとしたdomしか記述できません。
Vueイベント
今回はとりあえずクリックイベントのみ扱います。
http://localhost:3000/sample-button
/// <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");
}
}
<div>
<h1>sample-click</h1>
<a class="button is-light" @click="clickByAlertButton">test</a>
</div>
ポイントは@click
です。
Vueの双方向バインディング
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へのアクセッサーです。
/// <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を引数の値に変更しています。
stateの変数はmutation内でしか変更してはいけません。
これは大事です。
action内やgetter内、他では変更できないのです。
詳しくはVuex フォームの扱いを参考にしてください。
<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
/// <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;
}
}
<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
1つ目のURLはid、123が取得できます。
2つ目のURLはaaaを取得しています。
さらに2つ目のURLにはクエリパラメータ付きです。
キーはtest値はbbbにとなっています。
ネットワーク
ネットワーク周りをやる上で押さえておきたいのがPromiseとasync/awaitです。
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;
}
}
load 1
message:null
message loaded:sample message
load 2
load 3
consoleログには順番にログが出ています。
順番にログが出ているのがポイントです。
通信もしているのにです。
<div>
<h1>sample-load</h1>
message: {{message}}<br>
title: {{title}}<br>
</div>
/// <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()
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);
}
}
export interface Sample2Entity {
title
}
export interface Sample2Parameter {
abc
ddd?
}
abcは必須、ddd?はnullable変数になります。
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)
});
});
}
}
一番大事なところなのに説明が少ないところが多々ありますが、
とりあえずこんな感じでおわりにします。。