我們在之前的實(shí)現(xiàn)中家浇,對于異步 Action 的調(diào)用使用了 redux-saga 中間件妈嘹。thunk 中間件通過增強(qiáng)了返回可調(diào)用函數(shù)的功能成黄,也就允許了我們可以實(shí)現(xiàn)如下所示的異步 Action 休弃。
actions/novel.ts
// 普通 Action
const fetchNovels = createAction(ACTION_TYPES.FETCH_NOVELS);
const fetchNovelsOK = createAction(ACTION_TYPES.FETCH_NOVELS_OK);
const fetchNovelsNG = createAction(ACTION_TYPES.FETCH_NOVELS_NG);
// 異步 Action
export const searchNovels = () => (dispatch: Dispatch) => {
dispatch(fetchNovels()); // {type: 'FETCH_NOVELS'}
queryNovels().then(resp => {
if (resp.isAxiosError) {
dispatch(fetchNovelsNG(resp)); // {type: 'FETCH_NOVELS_NG', payload: error, error: true}
} else {
dispatch(fetchNovelsOK(resp.novel)); // {type: 'FETCH_NOVELS_OK', payload: json}
}
});
};
使用 thunk 可以完成我們正常需求,但是它存在一些問題银受。
- Action 層會隨著項(xiàng)目需求增長而不斷擴(kuò)大践盼。
- 異步 Action 不能很好的進(jìn)行單元測試。
- 不能夠更好的組織業(yè)務(wù)的流程控制宾巍,每個(gè)異步 Action 都是相對獨(dú)立的宏侍,無關(guān)聯(lián)的。
- Action 包含了副作用蜀漆,不能保持為一個(gè) Plain Object。
- ......(以后發(fā)現(xiàn)繼續(xù)追加)
redux-saga 也是一個(gè)類似 redux-thunk 的增強(qiáng) store 功能的中間件咱旱,它是可測試的确丢,并提供了聲明式指令。saga 使用了 ES6 的 Generator 功能吐限,讓異步的流程更易于讀取鲜侥,寫入和測試,并且將流程控制從Action Creator 中抽出诸典,簡化了 Action 層描函,保持了 Action 層的純凈。
我們將原來定義在 Action 層里的副作用代碼狐粱,轉(zhuǎn)移到 saga 層來實(shí)現(xiàn)舀寓。function* 就是ES6 的 Generator 函數(shù)實(shí)現(xiàn)方式。saga 內(nèi)部的業(yè)務(wù)流程控制都是通過一個(gè)一個(gè)的 yield 來完成的肌蜻,你可以直接 yield 一個(gè) promise(當(dāng)然由于不利于單元測試互墓,不推薦這樣寫),也可以直接使用 saga 所提供的一些申明式的命令蒋搜,也就是 Effect篡撵。每一個(gè) yield 的 Effect 都會傳遞到 redux-saga 中間件被解釋執(zhí)行,如果指令是 promise豆挽,saga 就會暫停等到 promise 返回育谬。接著就執(zhí)行下一個(gè) yield 指令,你也可以通過 if帮哈,for 等控制語句來構(gòu)建更復(fù)雜的流程膛檀。
saga 相關(guān) 基礎(chǔ) Effect :
-
put(Action)
創(chuàng)建 dispatch 的 Effect,告訴 middleware 發(fā)起 Action 操作。 -
call(method | generator, arg1, arg2, ...)
告訴 middleware 使用給定的參數(shù) arg1, arg2, ... 調(diào)用給定的 method 或 generator 函數(shù)宿刮,另外一種寫法可以允許我們調(diào)用指定對象的方法yield call([obj, obj.method], arg1, arg2, ...)
互站。適合調(diào)用返回Promise 結(jié)果的函數(shù)。 -
apply(obj, obj.method, [arg1, arg2, ...])
與 call 指令功能相同僵缺,就寫法不一樣胡桃。 -
select()
返回當(dāng)前完整的 State 樹,與 getState 類似磕潮,也可以指定對應(yīng)的 selector 作為參數(shù)翠胰,來返回指定部分的 State。 -
take(Action | '*')
告訴 middleware 等待一個(gè)指定或者滿足匹配符*
的 Action自脯。使用take
就可以組織我們復(fù)雜的流程控制之景。 -
fork(method | generator, arg1, arg2, ...)
相對于take
,表示一個(gè)無阻塞調(diào)用膏潮,告訴 middleware 使用給定的參數(shù) arg1, arg2, ... 調(diào)用給定的 method 或 generator 函數(shù)锻狗。 -
cancel(task)
命令 middleware 取消之前的一個(gè) fork 任務(wù)。與之對應(yīng)的cancelled
指令可以指定取消任務(wù)需要執(zhí)行的操作焕参。 -
race(effects)
類似Promise.race
功能轻纪,在多個(gè) Effects 之間觸發(fā)一個(gè)競賽,誰先完成就結(jié)束整個(gè) Effect叠纷,失敗方自動取消刻帚。
saga 相關(guān) Wraaper Effect :
-
takeEvery(Action | '*', function* do(){})
檢測到指定或者滿足匹配符*
的 Action 發(fā)起到 store 以后,觸發(fā)后續(xù) do 操作指令涩嚣,允許多個(gè) do 操作同時(shí)發(fā)生崇众。這個(gè)和 redux-thunk 功能相似。 -
takeLatest(Action | '*', function* do(){})
只相應(yīng)最新的 do 操作航厚。
以上是比較常用的顷歌,還想了解更多請查閱 API 參考
。并且所有的 Effect 都是生成簡單對象后幔睬,發(fā)送給 saga middleware衙吩,由 middleware 來根據(jù)effect 的類型來完成具體的調(diào)用。所以才保證了 saga 來實(shí)現(xiàn)相關(guān)的副作用是可測試的溪窒。
現(xiàn)在開始實(shí)際編碼吧坤塞。首先執(zhí)行命令安裝yarn add redux-saga
在根目錄新建文件夾 sagas,新定義 saga 層來定義我們的業(yè)務(wù)流程澈蚌,新增 novel.ts
import { call, put, take } from "redux-saga/effects";
import { queryNovels } from "../services/novelapi";
import { fetchNovels, fetchNovelsOK, fetchNovelsNG } from "../actions/novel";
export function* watchSearchNovels() {
// 無限循環(huán)保證 saga 一直在后臺運(yùn)行監(jiān)視
while (true) {
// 阻塞直到 fetchNovels Action發(fā)起
yield take(fetchNovels);
try {
// 異步調(diào)用
const data = yield call(queryNovels);
// 通知store發(fā)起fetchNovelsOK操作
yield put(fetchNovelsOK(data.novel));
} catch (error) {
// 通知store發(fā)起fetchNovelsNG操作
yield put(fetchNovelsNG(error));
}
}
}
刪除原來 actions/novel.ts 中定義的異步 Action 代碼
import { ACTION_TYPES } from "../constants";
import { createAction } from "redux-actions";
// 普通 Action
export const fetchNovels = createAction(ACTION_TYPES.FETCH_NOVELS); // {type: 'FETCH_NOVELS'}
export const fetchNovelsOK = createAction(ACTION_TYPES.FETCH_NOVELS_OK); // {type: 'FETCH_NOVELS_OK', payload: json}
export const fetchNovelsNG = createAction(ACTION_TYPES.FETCH_NOVELS_NG); // {type: 'FETCH_NOVELS_NG', payload: error, error: true}
configure-store.ts 里移除 thunk 中間件的代碼摹芙,替換為 saga 。
// saga 中間件
const sagaMiddleware = createSagaMiddleware();
// 創(chuàng)建store
export const store = createStore(
// 跟reducer
rootReducer,
// 應(yīng)用中間件
applyMiddleware(
sagaMiddleware,
routerMiddleware(history),
loggerMiddleware,
reduxCatch((error: Error) => {
console.error("Redux Action 調(diào)用出錯(cuò)了");
console.error(error);
})
)
);
// 啟動 saga
sagaMiddleware.run(watchSearchNovels);
reducer 層我們是不用動的宛瞄。如此啟動看看效果吧浮禾。