Reactと一緒にされることが多いReduxですが、まずはReactなしでいきます。
やること: API → 表示
Calil のAPI から図書館情報を取得して一覧で表示。
https://calil.jp/doc/api.html
同じ構成でIos(ReSwift)実装はこちら
http://qiita.com/nakadoribooks/items/c5f53071abdfafd47d4f
成果物
#準備
環境
es2015, Babel
ライブラリ
Redux, superagent
環境設定
{
"name": "CalilClient-web",
"version": "1.0.0",
"description": "CalilClient-web",
"main": "main.js",
"scripts": {},
"repository": {
"type": "git",
"url": "git+https://github.com/nakadoribooks/CalilClient-web"
},
"author": "nakadoribooks",
"license": "ISC",
"bugs": {
"url": "https://github.com/nakadoribooks/CalilClient-web/issues"
},
"homepage": "https://github.com/nakadoribooks/CalilClient-web#readme",
"devDependencies": {
"babel-core": "^6.23.1",
"babel-loader": "^6.2.10",
"babel-preset-es2015": "^6.22.0",
"webpack": "^2.2.1"
},
"dependencies": {
"redux": "^3.0.0",
"superagent": "^3.5.0",
"superagent-jsonp": "^0.1.1"
}
}
npm install
var webpack = require('webpack');
var path = require('path');
var config = {
devtool: "eval",
context: __dirname + '/source',
entry: {
'application': './main.js'
},
output: {
path: __dirname + '/public/javascript',
filename: '[name].js'
},
resolve: {
extensions: ['.js']
},
plugins: [],
module: {
loaders: [
{
test: /.js?$/,
loader: 'babel-loader',
exclude: /node_modules/,
query: {
presets: ['es2015']
}
}
]
}
};
module.exports = config;
node_modules/.bin/webpack --watch
処理の流れ
0: globalにstoreを作る (Global)
import { applyMiddleware, createStore } from 'redux';
import reducer from './model/reducer/CalilReducer.js'
import LibraryListViewController from './vc/list/LibraryListViewController.js'
// setup redux. put global
const store = createStore(reducer);
window.store = store;
// ↑ これ
// createView
var appDom = document.getElementById("app");
var libraryListViewController = new LibraryListViewController();
appDom.appendChild(libraryListViewController.view());
1: 読み込みイベントを作る (View)
_dispatchLoadData(){
let action = ActionCreator.loadLibraries() /*← こっち*/
store.dispatch(action)
}
1.5: 読み込む and Actionを作って返す (ActionCreator)
import {LibraryListLoadAction, LibraryListFailLoadAction, LibraryListLoadedAction} from './actions/Actions.js'
// 〜 略 〜
static loadLibraries(prefName="秋田県") {
var params = { appkey: config.API_KEY, pref: prefName, format: "json", callback: "f" };
superagent.get(config.API_LIBRARY).query(params).use(superagentJsonp({timeout: 1000}))
.end(function(err, res){
// 〜 略 〜
var libraryList = Library.createList(list)
/* 読み込んだら loadedActionをdispatchして 5 へ*/
var loadedAction = Object.assign(LibraryListLoadedAction)
loadedAction.libraryList = libraryList
store.dispatch(loadedAction)
})
// 読み込み開始アクションを返す 2 へ
return Object.assign(LibraryListLoadAction)
}
2: (from 1) 作ったイベントをdispatch (View)
_dispatchLoadData(){
let action = ActionCreator.loadLibraries()
store.dispatch(action) /*← こっち*/
}
3: loadActionを受け取ってStateのisLoading を trueに (Reducer)
case LibraryListLoadAction.type:
return Object.assign({}, state, {
isLoading: true,
errorMessage: null
})
4: stateの変更を受け取ってViewを更新 (View)
// 〜 略 〜
store.subscribe(() => {
this._newState(store.getState())
})
// 〜 略 〜
_newState(state){
// 〜 略 〜
// loading
if(state.isLoading){
console.log("loading")
this._button.setAttribute("disabled", "disabled")
this._statusLabel.innerHTML = '<span style="color:bray">読み込み中</span>'
return
}
// 〜 略 〜
5: (from 1.5) 読み込み完了のイベントを受け取ってStateを更新 (Reducer)
case LibraryListLoadedAction.type:
return Object.assign({}, state, {
isLoading: false,
libraryList: action.libraryList,
errorMessage: null
})
6: stateの変更を受け取ってViewを更新 (View)
_newState(state){
// 〜 略 〜
// loaded
console.log("loaded")
this._statusLabel.innerHTML = '<span style="color:blue">読み込み完了</span>'
this._reloadListView(state.libraryList)
}
以下、コード全体
##State 3つ
読み込み中フラグ
表示するデータ
エラー有無
export default {
isLoading: false,
libraryList: [],
errorMessage: null
}
##Action 3つ
読み込み開始
読み込み完了
エラー
let LibraryListLoadAction = {
type: "LibraryListLoadAction"
}
let LibraryListFailLoadAction = {
type: "LibraryListFailLoadAction"
}
let LibraryListLoadedAction = {
type : "LibraryListLoadedAction"
, libraryList: []
}
export {LibraryListLoadAction, LibraryListFailLoadAction, LibraryListLoadedAction};
Reducer: Actionを受け取ってStateを変更
import {LibraryListLoadAction, LibraryListFailLoadAction, LibraryListLoadedAction} from '../action/actions/Actions.js'
import initialState from '../state/AppState.js'
export default function reducer(state = initialState, action) {
switch (action.type) {
case LibraryListLoadAction.type:
return Object.assign({}, state, {
isLoading: true,
errorMessage: null
})
case LibraryListFailLoadAction.type:
return Object.assign({}, state, {
isLoading: true,
errorMessage: "通信に失敗しました"
})
case LibraryListLoadedAction.type:
return Object.assign({}, state, {
isLoading: false,
libraryList: action.libraryList,
errorMessage: null
})
default:
return state
}
}
ActionCreator: Action を作って返却 or 通信 → Action作って dispatch
この中でdispatchするのはどうなんだろう。
import {LibraryListLoadAction, LibraryListFailLoadAction, LibraryListLoadedAction} from './actions/Actions.js'
import Library from '../entity/Library.js'
import superagent from 'superagent'
import superagentJsonp from 'superagent-jsonp'
import config from '../../config.js'
class ActionCreators{
static loadLibraries(prefName="秋田県") {
var params = { appkey: config.API_KEY, pref: prefName, format: "json", callback: "f" };
superagent.get(config.API_LIBRARY).query(params).use(superagentJsonp({timeout: 1000}))
.end(function(err, res){
// error
if(err != null){
let errorAction = Object.assign(LibraryListFailLoadAction)
store.dispatch(errorAction)
return;
}
var list = res.body
// wrong dictionary
if (list == null){
let errorAction = Object.assign(LibraryListFailLoadAction)
store.dispatch(errorAction)
return;
}
// success
var libraryList = Library.createList(list)
var loadedAction = Object.assign(LibraryListLoadedAction)
loadedAction.libraryList = libraryList
// 時間かかっているてい
setTimeout(function(){
store.dispatch(loadedAction)
}, 1000)
})
return Object.assign(LibraryListLoadAction)
}
}
export default ActionCreators
View: Stateの変更を受け取って表示を更新
import LibraryCell from './LibraryCell.js'
import ActionCreator from '../../model/action/ActionCreator.js'
class LibraryListViewController {
constructor() {
var view = document.createElement("div")
var listView = document.createElement("ul")
var headerView = document.createElement("div")
var statusLabel = document.createElement("p")
var button = document.createElement("button")
button.appendChild(document.createTextNode("読み込む"))
button.addEventListener("click", (event)=>{
this._tapReload(event)
}, false);
listView.style.height = window.parent.screen.height + "px"
listView.style.overflow = "auto"
headerView.appendChild(statusLabel)
headerView.appendChild(button)
view.appendChild(headerView)
view.appendChild(listView)
// save property
this._libraryList = []
this._libraryCellList = []
this._view = view
this._listView = listView
this._button = button
this._statusLabel = statusLabel
// storeの変更通知を受け取る
store.subscribe(() => {
this._newState(store.getState())
})
this._dispatchLoadData();
}
_tapReload(event){
console.log(event)
console.log(this)
this._dispatchLoadData()
}
_dispatchLoadData(){
let action = ActionCreator.loadLibraries();
store.dispatch(action);
}
_newState(state){
console.log("onChangeStore")
this._button.removeAttribute("disabled")
// error
if (state.errorMessage != null){
console.log("error");
this._statusLabel.innerHTML = '<span style="color:red">読み込み失敗</span>'
return
}
// loading
if(state.isLoading){
console.log("loading")
this._button.setAttribute("disabled", "disabled")
this._statusLabel.innerHTML = '<span style="color:bray">読み込み中</span>'
return
}
// loaded
console.log("loaded")
this._statusLabel.innerHTML = '<span style="color:blue">読み込み完了</span>'
this._reloadListView(state.libraryList)
}
_reloadListView(libraryList){
this._libraryList = libraryList
// reloadListView
let listView = this._listView
let libraryCellList = this._libraryCellList
for(var i=0,max=libraryCellList.length;i<max;i++){
let cell = libraryCellList[i]
listView.removeChild(cell.view())
}
this._libraryCellList = []
for(var i=0,max=libraryList.length;i<max;i++){
var library = libraryList[i]
var cell = new LibraryCell()
cell.reload(library)
listView.appendChild(cell.view())
this._libraryCellList.push(cell)
}
}
// public
view(){
return this._view;
}
}
export default LibraryListViewController;
その他
var API_ROOT = "https://api.calil.jp/"
export default {
API_ROOT: API_ROOT,
API_LIBRARY: API_ROOT + "library",
API_KEY: "変更してね"
}
カーリルに開発者登録してAPI_KEYをゲットして書き換えてね。
import { applyMiddleware, createStore } from 'redux';
import reducer from './model/reducer/CalilReducer.js'
import LibraryListViewController from './vc/list/LibraryListViewController.js'
// setup redux. put global
const store = createStore(reducer);
window.store = store;
// createView
var appDom = document.getElementById("app");
var libraryListViewController = new LibraryListViewController();
appDom.appendChild(libraryListViewController.view());
AppDelegate てきなやつ
<!doctype html>
<html>
<head>
<title>CalilClient</title>
<meta charset="UTF-8">
</head>
<body style="margin:0px;padding:0px;">
<div id="app"></div>
<script src="javascript/application.js"></script>
</body>
</html>
課題
非同期のActionどうしよう。
ありがとう Calil APi
引用
未来の図書館を作るのはあなただ!
全国6000の図書館に簡単アクセス! カーリルの図書館APIを使ってみよう
全国の図書館を対象としたリアルタイム蔵書検索を可能にするAPI群を開発者向けに提供します。
法人・個人を問わず無償で利用できます。