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 层我们是不用动的。如此启动看看效果吧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容