我们在之前的实现中,对于异步 Action 的调用使用了 redux-saga 中间件。thunk 中间件通过增强了返回可调用函数的功能,也就允许了我们可以实现如下所示的异步 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 层会随着项目需求增长而不断扩大。
- 异步 Action 不能很好的进行单元测试。
- 不能够更好的组织业务的流程控制,每个异步 Action 都是相对独立的,无关联的。
- Action 包含了副作用,不能保持为一个 Plain Object。
- ......(以后发现继续追加)
redux-saga 也是一个类似 redux-thunk 的增强 store 功能的中间件,它是可测试的,并提供了声明式指令。saga 使用了 ES6 的 Generator 功能,让异步的流程更易于读取,写入和测试,并且将流程控制从Action Creator 中抽出,简化了 Action 层,保持了 Action 层的纯净。
我们将原来定义在 Action 层里的副作用代码,转移到 saga 层来实现。function* 就是ES6 的 Generator 函数实现方式。saga 内部的业务流程控制都是通过一个一个的 yield 来完成的,你可以直接 yield 一个 promise(当然由于不利于单元测试,不推荐这样写),也可以直接使用 saga 所提供的一些申明式的命令,也就是 Effect。每一个 yield 的 Effect 都会传递到 redux-saga 中间件被解释执行,如果指令是 promise,saga 就会暂停等到 promise 返回。接着就执行下一个 yield 指令,你也可以通过 if,for 等控制语句来构建更复杂的流程。
saga 相关 基础 Effect :
-
put(Action)
创建 dispatch 的 Effect,告诉 middleware 发起 Action 操作。 -
call(method | generator, arg1, arg2, ...)
告诉 middleware 使用给定的参数 arg1, arg2, ... 调用给定的 method 或 generator 函数,另外一种写法可以允许我们调用指定对象的方法yield call([obj, obj.method], arg1, arg2, ...)
。适合调用返回Promise 结果的函数。 -
apply(obj, obj.method, [arg1, arg2, ...])
与 call 指令功能相同,就写法不一样。 -
select()
返回当前完整的 State 树,与 getState 类似,也可以指定对应的 selector 作为参数,来返回指定部分的 State。 -
take(Action | '*')
告诉 middleware 等待一个指定或者满足匹配符*
的 Action。使用take
就可以组织我们复杂的流程控制。 -
fork(method | generator, arg1, arg2, ...)
相对于take
,表示一个无阻塞调用,告诉 middleware 使用给定的参数 arg1, arg2, ... 调用给定的 method 或 generator 函数。 -
cancel(task)
命令 middleware 取消之前的一个 fork 任务。与之对应的cancelled
指令可以指定取消任务需要执行的操作。 -
race(effects)
类似Promise.race
功能,在多个 Effects 之间触发一个竞赛,谁先完成就结束整个 Effect,失败方自动取消。
saga 相关 Wraaper Effect :
-
takeEvery(Action | '*', function* do(){})
检测到指定或者满足匹配符*
的 Action 发起到 store 以后,触发后续 do 操作指令,允许多个 do 操作同时发生。这个和 redux-thunk 功能相似。 -
takeLatest(Action | '*', function* do(){})
只相应最新的 do 操作。
以上是比较常用的,还想了解更多请查阅 API 参考
。并且所有的 Effect 都是生成简单对象后,发送给 saga middleware,由 middleware 来根据effect 的类型来完成具体的调用。所以才保证了 saga 来实现相关的副作用是可测试的。
现在开始实际编码吧。首先执行命令安装yarn add redux-saga
在根目录新建文件夹 sagas,新定义 saga 层来定义我们的业务流程,新增 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() {
// 无限循环保证 saga 一直在后台运行监视
while (true) {
// 阻塞直到 fetchNovels Action发起
yield take(fetchNovels);
try {
// 异步调用
const data = yield call(queryNovels);
// 通知store发起fetchNovelsOK操作
yield put(fetchNovelsOK(data.novel));
} catch (error) {
// 通知store发起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();
// 创建store
export const store = createStore(
// 跟reducer
rootReducer,
// 应用中间件
applyMiddleware(
sagaMiddleware,
routerMiddleware(history),
loggerMiddleware,
reduxCatch((error: Error) => {
console.error("Redux Action 调用出错了");
console.error(error);
})
)
);
// 启动 saga
sagaMiddleware.run(watchSearchNovels);
reducer 层我们是不用动的。如此启动看看效果吧。
网友评论