React + Redux + JWT tutorial(2)
前回でサーバーを準備したので今回はフロント側を準備します。
準備
以下を参考に webpack 関係を準備します。
https://gist.github.com/okamuuu/831b03f1dee84dea8d3893b9c8afabe3
Frontend で実装する機能
Frontend で以下の機能を実装します。
/api/sessions
へPOST
すると access_token を取得できる(通常は username, password を送信する)- クライアント側でローカルストレージにトークンを保存する
/api/hello
へアクセスする/api/private/hello
にアクセスする
作業開始
% mkdir src www test % mkdir src{components,containers}
webpack 関係の設定は下記を参考にしてください。
https://gist.github.com/okamuuu/831b03f1dee84dea8d3893b9c8afabe3
Creating the Redux Store
npm install --save-dev axios
create src/actions.js
export const SET_STATUS_MESSAGE = 'SET_STATUS_MESSAGE' export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE' export const CLEAR_MESSAGE = 'CLEAR_MESSAGE' export const SET_HELLO = 'SET_HELLO' export const SET_PRIVATE_HELLO = 'SET_PRIVATE_HELLO' export const SET_USER = 'SET_USER' import axios from 'axios'; const BASE_URL = 'http://localhost:3000' export function fetchHello() { // cording rule: https://github.com/acdlite/redux-actions return (dispatch) => { dispatch({type: CLEAR_MESSAGE}) dispatch({type: SET_STATUS_MESSAGE, payload: 'start fetching hello'}) return axios.get(`${BASE_URL}/api/hello`).then((response) => { dispatch({type: SET_HELLO, payload: response.data }) dispatch({type: SET_STATUS_MESSAGE, payload: 'finish fetching hello'}) }).catch((err) => { console.error(err.message) dispatch({type: CLEAR_MESSAGE}) dispatch({type: SET_ERROR_MESSAGE, payload: 'fail fetching hello'}) }) }; } export function fetchPrivateHello() { let token = localStorage.getItem('id_token') || null let config = {}; if (token) { config = { headers: {'Authorization': `Bearer ${token}` }} } return (dispatch) => { dispatch({type: CLEAR_MESSAGE}) dispatch({type: SET_STATUS_MESSAGE, payload: 'start fetching private hello'}) return axios.get(`${BASE_URL}/api/private/hello`, config).then((response) => { dispatch({type: SET_PRIVATE_HELLO, payload: response.data }) dispatch({type: SET_STATUS_MESSAGE, payload: 'finish fetching private hello'}) }).catch((err) => { console.error(err.message) dispatch({type: CLEAR_MESSAGE}) dispatch({type: SET_ERROR_MESSAGE, payload: 'fail fetching private hello'}) }) }; } export function login() { return (dispatch) => { dispatch({type: CLEAR_MESSAGE}) dispatch({type: SET_STATUS_MESSAGE, payload: 'start login'}) return axios.post(`${BASE_URL}/api/sessions`).then((response) => { localStorage.setItem('id_token', response.data.id_token) // hard-cording dispatch({type: SET_USER, payload: { userName: 'Tarou', isAuthenticated: true }}) dispatch({type: SET_STATUS_MESSAGE, payload: 'finish login'}) }).catch((err) => { console.error(err.message) dispatch({type: CLEAR_MESSAGE}) dispatch({type: SET_ERROR_MESSAGE, payload: 'fail login'}) }) }; }
create src/reducers.js
import { combineReducers } from 'redux' import { SET_STATUS_MESSAGE, SET_ERROR_MESSAGE, CLEAR_MESSAGE, SET_HELLO, SET_PRIVATE_HELLO, SET_USER } from './actions' function messageState(state = { statusMessage: "", errorMessage: "" }, action) { switch (action.type) { case SET_STATUS_MESSAGE: return Object.assign({}, state, { statusMessage: action.payload }) case SET_ERROR_MESSAGE: return Object.assign({}, state, { errorMessage: action.payload }) case CLEAR_MESSAGE: return Object.assign({}, state, { statusMessage: "", errorMessage: "" }) default: return state } } function helloState(state = { hello: '', }, action) { switch (action.type) { case SET_HELLO: return Object.assign({}, state, { hello: action.payload }) default: return state } } function privateHelloState(state = { privateHello: '', }, action) { switch (action.type) { case SET_PRIVATE_HELLO: return Object.assign({}, state, { privateHello: action.payload }) default: return state } } function userState(state = { userName: '', isAuthenticated: '' }, action) { switch (action.type) { case SET_USER: return Object.assign({}, state, { userName: action.payload.userName, isAuthenticated: action.payload.isAuthenticated, }) default: return state } } const helloApp = combineReducers({ messageState, userState, helloState, privateHelloState, }) export default helloApp
Testing
テストで使用するモジュールをインストールする
npm install --save-dev mocha assert nock node-localstorage
create test/test_fetch_hello.js:
import assert from 'assert' import { createStore, applyMiddleware } from 'redux' import thunkMiddleware from 'redux-thunk' import api from '../src/middleware/api' import { fetchHello, login, fetchPrivateHello } from '../src/actions' import helloApp from '../src/reducers' import nock from 'nock' import {LocalStorage} from 'node-localstorage' // replace localStorage for testing on Node.js global.localStorage = new LocalStorage('./scratch') describe('fetchHello', () => { const createStoreWithMiddleware = applyMiddleware(thunkMiddleware)(createStore) const store = createStoreWithMiddleware(helloApp) beforeEach(() => { nock.cleanAll() global.localStorage.clear() }); it('fetching', (done) => { // fetching は再現が難しいので nock nock('http://localhost:3000').get('/api/hello').delay(100).reply(200, 'hello, public') store.dispatch(fetchHello()) setTimeout(() => { const { statusMessage, errorMessage } = store.getState().messageState const { hello } = store.getState().helloState assert.equal(statusMessage, 'start fetching hello') assert.equal(hello, '') done() }, 10) }); it('success', (done) => { // nock('http://localhost:3000').get('/api/hello').reply(200, 'hello, public') store.dispatch(fetchHello()) setTimeout(() => { const { statusMessage, errorMessage } = store.getState().messageState const { hello } = store.getState().helloState assert.equal(statusMessage, 'finish fetching hello') assert.equal(hello, 'hello, public') done() }, 50) }); it('failure', (done) => { // failure は再現が難しいので nock nock('http://localhost:3000').get('/api/hello').reply(404) store.dispatch(fetchHello()) setTimeout(() => { const { statusMessage, errorMessage } = store.getState().messageState const { hello } = store.getState().helloState assert.equal(errorMessage, 'fail fetching hello') done() }, 10) }); }); describe('login', () => { const createStoreWithMiddleware = applyMiddleware(thunkMiddleware)(createStore) const store = createStoreWithMiddleware(helloApp) beforeEach(() => { nock.cleanAll() global.localStorage.clear() }); it('success', (done) => { nock('http://localhost:3000').post('/api/sessions').reply(201, {id_token: 'dummy-token'}) store.dispatch(login()) setTimeout(() => { const { statusMessage, errorMessage } = store.getState().messageState const { userName, isAuthenticated } = store.getState().userState assert.equal(statusMessage, 'finish login') assert.ok(isAuthenticated) const token = localStorage.getItem('id_token') assert.ok(token) done() }, 10) }) }); describe('fetchPrivateHello', () => { const createStoreWithMiddleware = applyMiddleware(thunkMiddleware)(createStore) const store = createStoreWithMiddleware(helloApp) beforeEach(() => { nock.cleanAll() global.localStorage.clear() }); it('fetching', (done) => { // fetching は再現が難しいので nock nock('http://localhost:3000').get('/api/private/hello').delay(100).reply(200, 'hello, public') store.dispatch(fetchPrivateHello()) setTimeout(() => { const { statusMessage, errorMessage } = store.getState().messageState const { privateHello } = store.getState().privateHelloState assert.equal(statusMessage, 'start fetching private hello') assert.equal(privateHello, '') done() }, 10) }); it('failure because of no token', (done) => { // nock('http://localhost:3000').get('/api/private/hello').reply(401) store.dispatch(fetchPrivateHello()) setTimeout(() => { const { statusMessage, errorMessage } = store.getState().messageState assert.equal(errorMessage, 'fail fetching private hello') done() }, 10) }); it('success', (done) => { nock('http://localhost:3000').post('/api/sessions').reply(201, {id_token: 'dummy-token'}) store.dispatch(login()) setTimeout(() => { nock('http://localhost:3000').get('/api/private/hello').reply(200, 'hello, private') store.dispatch(fetchPrivateHello()) setTimeout(() => { const { statusMessage, errorMessage } = store.getState().messageState const { privateHello } = store.getState().privateHelloState assert.equal(statusMessage, 'finish fetching private hello') assert.equal(privateHello, 'hello, private') done() }, 10) }, 10) }); });
前回作成した server.js を起動してテストを実行。コマンドが長いので npm run
に登録する事をおすすめします。babel-polyfill
は Node.js でテストをする時に必要です。
% NODE_ENV=test $(npm bin)/mocha --compilers js:babel-register --require babel-polyfill
まとめ
今回は Action と Reducers を定義しました。 次回、実際に React に組み込んで動作を確認します。