Upgrade to Pro — share decks privately, control downloads, hide ads and more …

React-Redux-Redux-Saga-Workshop03

tashxii
August 05, 2019

 React-Redux-Redux-Saga-Workshop03

tashxii

August 05, 2019
Tweet

More Decks by tashxii

Other Decks in Programming

Transcript

  1. React / Redux / Redux-Saga • React … UIライブラリ •

    Redux … React の状態管理ライブラリ • Redux-Saga … 非同期処理を扱うReduxのミドルウェア 2 https://ja.reactjs.org/ https://Redux-Saga.js.org/ https://redux.js.org/ Skip可:前回と同じスライド
  2. 題材に使うアプリケーション • タスク管理アプリ • イメージ(gif) • 機能 • サインアップ •

    ログイン • ボード管理 • ユーザー管理 • タスク管理 • ドラッグ&ドロップ • Push通知 3 Skip可:前回と同じスライド
  3. 題材のアプリで使用しているライブラリ • Redux 状態管理ライブラリ • Redux-Saga 非同期処理用ライブラリ • styled-component コンポーネントのスタイル管理

    • Ant design コンポーネントライブラリ • react-beautiful-dnd ドラッグ&ドロップコンポーネント • react-router-dom URL遷移 • Font awesome アイコンライブラリ 5 Skip可:前回と同じスライド
  4. ソースコード • Front-end (React) https://github.com/tashxii/taskboard-react • Back-end (Go) https://github.com/tashxii/taskboard-api-go git

    clone https://github.com/tashxii/taskboard-react.git cd taskboard-react yarn install git clone https://github.com/tashxii/taskboard-api-go.git cd taskboard-api-go dep ensure go build 6 Skip可:前回と同じスライド
  5. Reactを使うアプリケーションの構成 https://ja.reactjs.org/ 8 • Reactは、Viewを提供するライブラリであり、それ以外の技術スタックを 組み合わせて使うことが前提 以下のような組み合わせで使う • Redux …

    アプリケーションの状態管理フレームワークを使用したり、 • Redux-Saga … 非同期処理を扱うライブラリを使ったり、 • Back-end サーバー(RESTやSOAPサーバー)と組み合わせて構成する ここを取り上げます
  6. 同期処理との違い • 同期処理では、「ログイン」は、ログインボタン押下⇒待ち⇒ログイン後の 処理の継続のような流れになる • 非同期処理は、「開始」、「成功」、「失敗」の三つに分岐される • 「開始」後にコントロールがユーザーに返されるため、二重サブミット防止や、処理 中が分かるような表示へのフィードバックなどをする必要がある 11

    export const UPDATE_LOGIN_USER_START_EVENT = "UPDATE_LOGIN_USER_START_EVENT" export const UPDATE_LOGIN_USER_SUCCESS_EVENT = "UPDATE_LOGIN_USER_SUCCESS_EVENT" export const UPDATE_LOGIN_USER_FAILURE_EVENT = "UPDATE_LOGIN_USER_FAILURE_EVENT“ export const LOGOUT_START_EVENT = "LOGOUT_START_EVENT" export const LOGOUT_SUCCESS_EVENT = "LOGOUT_SUCCESS_EVENT" export const LOGOUT_FAILURE_EVENT = "LOGOUT_FAILURE_EVENT"
  7. Saga Middlewareの登録 • createSagaMiddleware を使用して ReduxのMiddlewareに登録 12 import React from

    "react" import { render } from "react-dom" import { Provider } from "react-redux" import { createStore, applyMiddleware } from "redux" import createSagaMiddleware from "redux-saga" import App from "./components/App" import appState from "./reducers" import rootSaga from "./sagas/saga" const sagaMiddleware = createSagaMiddleware() const store = createStore( appState, applyMiddleware(sagaMiddleware) ) sagaMiddleware.run(rootSaga) render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") ) forkした 関数をrun
  8. forkを使用してハンドラを登録 • fork を使用してハンドラ 関数を登録 13 import { fork }

    from "redux-saga/effects" import UserSagas from "./userSagas" import BoardSagas from "./boardSaga" import TaskSagas from "./taskSaga" import WsSaga from "./wsSaga" export default function* rootSaga() { let sagaFunctions = [] sagaFunctions = sagaFunctions.concat(UserSagas.sagaFunctions()) sagaFunctions = sagaFunctions.concat(BoardSagas.sagaFunctions()) sagaFunctions = sagaFunctions.concat(TaskSagas.sagaFunctions()) sagaFunctions = sagaFunctions.concat(WsSaga.sagaFunctions()) for (let i = 0; i < sagaFunctions.length; i++) { yield fork(sagaFunctions[i]) } } function* handleListUsers() { yield takeEvery(LIST_USERS_START_EVENT, listUsers) } function* listUsers(/*action*/) { const { users, error } = yield call(UserService.listAsync) if (!error) { yield put(listUsersSuccessEvent(users)) } else { yield put(listUsersFailureEvent(error)) } } export default class UserSagas { static sagaFunctions = () => { return [ handleLogin, handleSignUp, handleLogout, handleUpdateLoginUser, handleListUsers, ] } } ハンドラ関数 fork
  9. 基本的な流れ • takeEveryでユーザーの操作から 呼ばれるイベントを待ち受ける • callで非同期処理を呼び出す • putで次のイベントを起こす 14 function*

    handleLogin() { yield takeEvery(LOGIN_START_EVENT, login) } function* login(action) { const payload = action.payload const { user, error } = yield call( UserService.loginAsync, payload.name, payload.password) if (!error) { yield put(loginSuccessEvent(user)) } else { yield put(loginFailureEvent(error)) } } イベント 待ち受け 非同期 呼び出し 次の イベント クリック イベントを 待つ
  10. 双方向通信(Push通知) • eventChannel を作成し、 • サーバーからのメッセージに 対応したイベントを emit する •

    クライアントのイベントは、takeで 監視する • サーバーのイベントはチャンネルを 作り、それを take で監視する 15 const createWebsocketChannel = async loginUser => { return eventChannel(emit => { const ws = ApiCommon.createWebsocket(loginUser) ws.onmessage = (msg) => { const params = msg.data.split(" ") if (params.length >= 1) { const type = params[0] let ids = params.slice(1) switch (type) { case "UPDATE_TASKBOARDS": { ids.forEach(boardId => { emit(listTasksStartEvent(boardId)) }) break } case "UPDATE_BOARDS": break default: break } } } return () => { // Nothing } }) } イベント チャンネル サーバーからのメッ セージに対応した イベントをemit
  11. 双方向通信(Push通知) • サーバーからのメッセージを eventChannel で受け付け、 • channel を take し、そのアクションを

    put する 16 function* handleWebsocketMessage() { yield takeEvery(LOGIN_SUCCESS_EVENT, watchWebsocketMessage) } function* watchWebsocketMessage(action) { const channel = yield call(createWebsocketChannel, action.payload.user) while (true) { const pushedAction = yield take(channel) yield put(pushedAction) } } Channelをcall で作成する channelをtakeして そのイベントをputする
  12. レイヤー設計 • Redux, Redux-Sagaを使う場合、ビジネスロジック=状態遷移はreducers, saga関数内で実装される • 何もしないでロジックを書いていくとreducers, sagaが肥大化していって しまう •

    以下のようなレイヤー設計を導入して対応する方法がある • 表示用のmodel層を作成する • ロジックを集約するservice層を設ける • API層を作成し、APIのリクエストとレスポンスをmodelと切り分ける • “Converter”を用いて、modelとAPIのリクエストとレスポンスを変換する 18
  13. View レイヤー設計(全体) 19 Reducer/Saga Service Layer API Layer Component Model

    Store Converter Model Request/ Response API以外の 全てから参照 Serviceを 呼び出す Modelと Request/Response を変換する Request/ Response
  14. レイヤー設計(Model) • ES2015のclassとして作成 • StoreやComponentで参照する 20 export default class Board

    { constructor( id, name, dispOrder, isSystem, isClosed, createdDate, version, tasks, ) { this.id = id this.name = name this.dispOrder = dispOrder this.isSystem = isSystem this.isClosed = isClosed this.createdDate = createdDate this.version = version this.tasks = tasks } } export default class Task { constructor( id, name, description, assigneeUserId, boardId, dispOrder, createdDate, isClosed, estimateSize, version, ) { this.id = id this.name = name this.description = description this.assigneeUserId = assigneeUserId this.boardId = boardId this.dispOrder = dispOrder this.createdDate = createdDate this.isClosed = isClosed this.estimateSize = estimateSize this.version = version } } export default class User { constructor(id, name, avatar, version) { this.id = id this.name = name this.avatar = avatar this.version = version this.newPassword = "" } } User Task Board
  15. レイヤー設計(Saga) • Serviceを呼び出すのみ 21 function* handleCreateTask() { yield takeEvery(CREATE_TASK_START_EVENT, createTask)

    } function* createTask(action) { const payload = action.payload const { task, error } = yield call(TaskService.createAsync, payload.task) if (!error) { yield put(createTaskSuccessEvent(task)) } else { yield put(createTaskFailureEvent(error)) } } Serviceの 呼び出し タスクの作成
  16. レイヤー設計(Service) • APIを呼び出す • Converterを使い、RequestとResponse ⇔ Model の変換を行う 22 export

    default class TaskService { static createAsync = async (taskCreateRequest) => { const request = TaskConverter.convertCreateRequest(taskCreateRequest) return await TaskApi.create(request) .then((res) => { if (res.ok) { return { task: TaskConverter.getTaskByTaskResponse(res.json) } } else { return { error: ApiErrorConverter.createByApiError(res, I18n.get("タスクの登録に失敗しました")) } } }) .catch((error) => { return { error: ApiErrorConverter.createSystemError(error) } }) } Task ⇒ TaskCreateRequest 変換 TaskService.js Response ⇒ Task 変換
  17. レイヤー設計(Converter) • ModelをResponseとRequestに変換 (REST API ならJSONをclassにする) • APIの変更を吸収する 23 export

    default class TaskConverter { static getTaskByTaskResponse = (response) => { return new Task( response.id, response.name, response.description, response.assigneeUserId, response.boardId, response.dispOrder, response.createdDate, response.isClosed, response.estimateSize, response.version, ) } static convertCreateRequest = (task) => { return { name: task.name, description: task.description, assigneeUserId: task.assigneeUserId, boardId: task.boardId, dispOrder: task.dispOrder, isClosed: task.isClosed, estimateSize: task.estimateSize, } } TaskConverter.js Response ⇒ Task 変換 Task ⇒ TaskCreateRequest 変換
  18. レイヤー設計(API) • Httpメソッドを呼び出す(fetch API使用) 24 export default class TaskApi {

    static create = async (request) => { return await ApiCommon.post("/tasks", request) } static list = async (boardId) => { return await ApiCommon.get(`/tasks?boardid=${boardId}`) } static get = async (taskId) => { return await ApiCommon.get(`/tasks/${taskId}`) } static update = async (taskId, request) => { return await ApiCommon.put(`/tasks/${taskId}`, request) } static delete = async (taskId) => { return await ApiCommon.delete(`/tasks/${taskId}`, {}) } } TaskAPI.js GET, POST, PUT, DELETE を呼び出す export default class ApiCommon { static async get(path) { return doFetch( getApiUrl(path), getOption() ) } static async post(path, request) { return doFetch( getApiUrl(path), getUpdateOption(ApiCommon.Method.POST, request) ) } static async put(path, request) { return doFetch( getApiUrl(path), getUpdateOption(ApiCommon.Method.PUT, request) ) } static async delete(path, request) { return doFetch( getApiUrl(path), getUpdateOption(ApiCommon.Method.DELETE, request) ) } } const doFetch = async (path, option) => { let ok = false let status = -1 return await fetch(path, option) .then(response => { ok = response.ok status = response.status return response.text() }) .then(text => { const json = text !== "" ? JSON.parse(text) : {} return { ok, status, json } }) .catch(error => { throw error }) } } APICommon.js
  19. 覚えていてほしいこと • Redux-Sagaでの基本的な流れ • takeEvery … イベントの待ち受け • call …

    非同期処理の呼び出し • put … イベントの発行 • Redux-SagaでのPush通知 • eventChannel と emit • eventChannel のtake • レイヤー化設計があること • Serviceにロジックを集約する • APIとビューを疎結合にする 25