09-采用React Saga的心路历程

我们在之前的实现中,对于异步 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 层我们是不用动的。如此启动看看效果吧。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容