LoginSignup
1
2

More than 5 years have passed since last update.

redux Vol1

Posted at

Reactと一緒にされることが多いReduxですが、まずはReactなしでいきます。

やること: API → 表示

Calil のAPI から図書館情報を取得して一覧で表示。
https://calil.jp/doc/api.html

同じ構成でIos(ReSwift)実装はこちら
http://qiita.com/nakadoribooks/items/c5f53071abdfafd47d4f

成果物

準備

環境

es2015, Babel

ライブラリ

Redux, superagent

環境設定

package.json
{
  "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

webpack.config.js
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

ファイル構成

スクリーンショット 2017-03-11 17.24.22.png

処理の流れ

0: globalにstoreを作る (Global)

main.js
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)

LibraryListViewController.js
_dispatchLoadData(){
  let action = ActionCreator.loadLibraries() /*← こっち*/
  store.dispatch(action)
}

1.5: 読み込む and Actionを作って返す (ActionCreator)

ActionCreator.js

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)

LibraryListViewController.js
_dispatchLoadData(){
  let action = ActionCreator.loadLibraries()
  store.dispatch(action) /*← こっち*/
}

3: loadActionを受け取ってStateのisLoading を trueに (Reducer)

CalilReducer.js
case LibraryListLoadAction.type:
      return Object.assign({}, state, {
        isLoading: true,
        errorMessage: null
      })

4: stateの変更を受け取ってViewを更新 (View)

LibraryListViewController.js

// 〜 略 〜  
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)

CalilReducer.js
    case LibraryListLoadedAction.type:
      return Object.assign({}, state, {
        isLoading: false,
        libraryList: action.libraryList,
        errorMessage: null
      })

6: stateの変更を受け取ってViewを更新 (View)

LibraryListViewController.js
_newState(state){
    // 〜 略 〜

    // loaded
    console.log("loaded")

    this._statusLabel.innerHTML = '<span style="color:blue">読み込み完了</span>'
    this._reloadListView(state.libraryList)
}

以下、コード全体

State 3つ

読み込み中フラグ
表示するデータ
エラー有無

AppState.js
export default {
  isLoading: false,
  libraryList: [],
  errorMessage: null
}

Action 3つ

読み込み開始
読み込み完了
エラー

Actions.js
let LibraryListLoadAction = {
  type: "LibraryListLoadAction"
}

let LibraryListFailLoadAction = {
  type: "LibraryListFailLoadAction"
}

let LibraryListLoadedAction = {
  type : "LibraryListLoadedAction"
  , libraryList: []
}

export {LibraryListLoadAction, LibraryListFailLoadAction, LibraryListLoadedAction};

Reducer: Actionを受け取ってStateを変更

CalilReducer.js
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するのはどうなんだろう。

ActionCreator.js
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の変更を受け取って表示を更新

LibraryListViewController.js
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;

その他

config.js

var API_ROOT = "https://api.calil.jp/"

export default {
  API_ROOT: API_ROOT,
  API_LIBRARY: API_ROOT + "library",
  API_KEY: "変更してね"
}

カーリルに開発者登録してAPI_KEYをゲットして書き換えてね。

main.js
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 てきなやつ

pubic/index.html
<!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群を開発者向けに提供します。
法人・個人を問わず無償で利用できます。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2