何謂 redux-saga?
- redux 的 middleware
- 用於管理 Redux 非同步資料流的處理
- 將 saga pattern 實踐在 react/redux 應用上
特性
- 集中處理 redux 副作用 (side effect, 一般泛指非同步) 問題
- 被實現為 generator (es6 feature)
- 類似 redux-thunk middleware
- watch/worker(監聽->執行) 的工作形式
優點
- 保持 action 和 reducer 的簡單純粹
- 可將商業邏輯從 React Component 中抽出來,保持 View 的乾淨
- 程式碼更容易被測試和閱讀
- redux-saga 提供了豐富的 API 實現 saga pattern
- 處理複雜非同步問題,如果商業邏輯複雜,快讓 redux-saga 來拯救你
- 使用 saga 處理資料流非常清晰,雖然一開始寫會比較繁瑣,但日後程式碼會更好維護
缺點
- 其他的 middleware 可能較難以和 redux-saga 搭配,使用時或許要花費一些代價,用 redux-saga 重構部分程式
- babel-loader 轉出來的 source map 會錯位,要手動加 debugger 比較好測試
用程式碼快速了解 redux-saga 特性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import {call, take, fork} from 'redux-saga/effects'; import * as ActionTypes from './actionTypes'; * worker * API 呼叫,並依據結果(成功、失敗) 更新redux state */ function* loadCourse() { yield put({ type: LOAD_DATA_REQUEST }); try { const result = yield call(api.getCourse, payload); yield put({ type: LOAD_DATA_SUCCESS, payload: result, }); } catch (error) { yield put({ type: LOAD_DATA_FAILURE, error }); } } *watcher */ function* watchGetCourse() { while (true) { yield take(ActionTypes.GET_COURSE) yield fork(loadCourse); ... } }
|
簡單說明一下上述的程式碼:
- function 後面要加上 *,這是 generator 的寫法
- watchGetCourse 會在 app 執行期間持續地監聽 GET_COURSE action
- 當GET_COURSE action 被執行時,調用 loadCourse 這個 saga function
- 若 api 呼叫成功,dispatch LOAD_DATA_SUCCESS action,將資料更新回 store
- 若 api 呼叫失敗,dispatch LOAD_DATA_FAILURE action,將 error log 寫進 store
Effects
Effect 指的是一個 javascript object,裡面包含描述副作用的訊息,redux-saga 實作了豐富的 Effect API 幫助我們實現 Saga Effect。在 redux-saga 世界裡,所有的 Effect 都必須被 yield,由 yield 傳遞给 sagaMiddleware 執行。原則上來說,所有的 yield 後面也只能跟 Effect。
常用 Effects 的功能介紹:
1、take
等待 redux dispatch 匹配某個 pattern 的 action。
在上面例子中,先等待一個取得課程的 action ,然後執行取得課程的 saga:
1 2 3 4 5 6 7 8 9
| while (true) { yield take(ActionTypes.GET_COURSE) yield fork(loadCourse); } while (true) { const {id} = yield take(ActionTypes.GET_COURSE) yield fork(loadCourse, id); }
|
2、takeEvery
作用和 take 一樣,不過不用寫 while 迴圈了,取得 action payload 的方式也不太一樣
1 2 3 4 5 6 7
| function* login({ username, password }) { } function* watchLogin() yield takeEvery('LOGIN_REQUEST', login); }
|
3、fork
調用 saga function,可以返回執行結果。
1 2
| yield fork(loadCourse); const { response } = yield fork(api.getCourse);
|
4、call
與 fork 作用相同,差別在 fork 是非阻塞(non-block) 型調用,call 是阻塞(block) 型調用,所以對有順序性的操作你可以像底下這樣子寫。
1 2
| const categories = yield call(api.getCourseCategories); const courses = yield call(api.getCourses, {categories});
|
5、put
作用和 redux 中的 dispatch 相同。
1 2 3 4
| yield put({ type: 'GET_COURSE' }); yield put({ type: 'GET_COURSE_REQUEST', id }); yield put({ type: 'GET_COURSE_SUCCESS', data }); yield put({ type: 'GET_COURSE_FAILURE', error });
|
6、select
作用和 redux thunk 中的 getState 相同,若 saga 中需要使用 redux state 時,可以透過 select 來取用。
1 2 3 4 5 6
| const id = yield select(state => state.id); if (id) { yield call(api.getCourse(id)); } esle { throw new Error('id is undefined'); }
|
7、all
如果我們同時要 call 好幾支不同的 api,你可以這樣子寫,作用和 Promise.all 效果一樣,會等所有 api call 執行完後才往下繼續執行
1 2 3 4 5 6 7 8 9
| const [ response: { course }, response: { user }, response: { order }, ] = yield all({ call(api.getCourse, courseId), call(api.getUser, userId), call(api.getOrder, orderId), });
|
如何在專案中導入?
1、安裝 redux-saga
1
| npm install redux-saga --save
|
或是透過 yarn
2、加入 Saga Middleware
在 store
中加入 sagaMiddleware
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import { createStore as _createStore, applyMiddleware, compose } from 'redux'; import { routerMiddleware } from 'react-router-redux'; import createSagaMiddleware, { END } from 'redux-saga'; import reducer from './reducer'; import sagaManager from './sagaManager'; export default function createStore(history, client, data) { const sagaMiddleware = createSagaMiddleware(); const middleware = [ reduxRouterMiddleware, sagaMiddleware ]; let enhancers = [applyMiddleware(...middleware)]; const finalCreateStore = compose(...enhancers)(_createStore); const store = finalCreateStore(reducer, data); sagaManager.runSagas(sagaMiddleware); if (__DEVELOPMENT__ && module.hot) { module.hot.accept('./reducer', () => { store.replaceReducer(require('./reducer')); }); } store.runSaga = sagaMiddleware.run; store.close = () => store.dispatch(END); return store; }
|
3、建立 sagaManager.js
將專案中所有的 saga
引用進來,並在 store
中透過該sagaManager.js
執行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { fork, cancel, take } from 'redux-saga/effects'; import authSaga from './modules/auth/authSaga'; export function* sagas() { yield [ ...authSaga, ]; } const rootSaga = [sagas]; export default { runSagas(sagaMiddleware) { rootSaga.forEach((saga) => sagaMiddleware.run(saga)); }, };
|
With Hot Reload
目前 redux-saga 官方尚未支援 Hot reload,必須要在專案中做些修改才行
Webpack React/Redux Hot Module Reloading (HMR) example
在 sagaManager.js
中修改以下內容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| ... const CANCEL_SAGAS_HMR = 'CANCEL_SAGAS_HMR'; function createAbortableSaga(saga) { if (process.env.NODE_ENV === 'development') { return function* main() { const sagaTask = yield fork(saga); yield take(CANCEL_SAGAS_HMR); yield cancel(sagaTask); }; } return saga; } export default { runSagas(sagaMiddleware) { rootSaga.map(createAbortableSaga).forEach((saga) => sagaMiddleware.run(saga)); }, cancelSagas(store) { store.dispatch({ type: CANCEL_SAGAS_HMR }); } };
|
在 store
加入 Saga
的 Hot-reload
1 2 3 4 5 6 7 8 9 10 11
| ... if (__DEVELOPMENT__ && module.hot) { module.hot.accept('./sagaManager', () => { sagaManager.cancelSagas(store); const nextSagas = require('./sagaManager'); nextSagas.runSagas(sagaMiddleware); }); } ...
|
參考
這篇概略的介紹 redux-saga 的特性和常用到的 Effects,以及怎麼在專案中套用,如果想進一步了解更多可以參考以下連結
github
gitbook