Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >redux-saga

redux-saga

作者头像
ayqy贾杰
发布于 2019-06-12 04:41:35
发布于 2019-06-12 04:41:35
2K00
代码可运行
举报
文章被收录于专栏:黯羽轻扬黯羽轻扬
运行总次数:0
代码可运行

一.目标定位

redux-saga is a library that aims to make side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) in React/Redux applications easier and better.

作为一个Redux中间件,想让Redux应用中的副作用(即依赖/影响外部环境的不纯的部分)处理起来更优雅

二.设计理念

Saga像个独立线程一样,专门负责处理副作用,多个Saga可以串行/并行组合起来,redux-saga负责调度管理

Saga来头不小(1W star不是浪得的),是某篇论文中提出的一种分布式事务机制,用来管理长期运行的业务进程

P.S.关于Saga背景的更多信息,请查看Background on the Saga concept

三.核心实现

利用generator,让异步流程控制易读、优雅、易测试

In redux-saga, Sagas are implemented using Generator functions. To express the Saga logic we yield plain JavaScript Objects from the Generator.

实现上,关键点是:

  • 以generator形式组织逻辑序列(function\* + yield),把一系列的串行/并行操作通过yield拆分开
  • 利用iterator的可“暂停/恢复”特性(iter.next())分步执行
  • 通过iterator影响内部状态(iter.next(result)),注入异步操作结果
  • 利用iterator的错误捕获特性(iter.throw(error)),注入异步操作异常

generator/iterator实现是因为它非常适合流程控制的场景,体现在:

  • yield让描述串行/并行的异步操作变得很优雅
  • 以同步形式获取异步操作结果,更符合顺序执行的直觉
  • 以同步形式捕获异步错误,优雅地捕获异步错误

P.S.关于generator与iterator的关系及generator基础用法,可以参考generator(生成器)_ES6笔记2

例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const ts = Date.now();
function asyncFn(id) {
   return new Promise((resolve, reject) => {
       setTimeout(() => {
           console.log(`${id} at ${Date.now() - ts}`);
           resolve(id);
       }, 1000);
   });
}function* gen() {
   // 串行异步
   let A = yield asyncFn('A');
   console.log(A);
   let B = yield asyncFn('B');
   console.log(B);
   // 并行异步
   let C = yield Promise.all([asyncFn('C1'), asyncFn('C2')]);
   console.log(C);
   // 串行/并行组合异步
   let D = yield Promise.all([
       asyncFn('D1-1').then(() => {
           return asyncFn('D1-2');
       }),
       asyncFn('D2')
   ]);
   console.log(D);
}// test
let iter = gen();
// 尾触发顺序执行iter.next
let next = function(prevResult) {
   let {value: result, done} = iter.next(prevResult);
   if (result instanceof Promise) {
       result.then((res) => {
           if (!done) next(res);
       }, (err) => {
           iter.throw(err);
       });
   }
   else {
       if (!done) next(result);
   }
};
next();

实际结果符合预期:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
A at 1002
A
B at 2012
B
C1 at 3015
C2 at 3015
["C1", "C2"]
D1-1 at 4019
D2 at 4020
D1-2 at 5022
["D1-2", "D2"]

执行顺序为:A -> B -> C1,C2 -> D1-1 -> D2 -> D1-2

redux-saga的核心控制部分与上面示例类似(没错,就是这么像co),从实现上看,其异步控制的关键是尾触发顺序执行iter.next。示例没添Effect这一层描述对象,从功能上讲Effect并不重要(Effect的作用见下面术语概念部分)

Effect层要实现的东西包括2部分:

  • 业务操作 -> Effect 以Effect creator API形式提供,提供各种语义的用来生成Effect的工具函数,例如把dispatch action包装成put、把方法调用包装成call/apply
  • Effect -> 业务操作 在执行时内部进行转换,例如把[Effect1, Effect2]转换为并行调用

类似于装箱(把业务操作用Effect包起来)拆箱(执行Effect里的业务操作),此外,完整的redux-saga还要实现:

  • 作为middleware接入到Redux
  • 提供读/写Redux state的接口(select/put)
  • 提供监听action的接口(take/takeEvery/takeLatest)
  • Sagas组合、通信
  • task顺序控制、取消
  • action并发控制

差不多是一个大而全的异步流程控制库了,从实现上看,相当于一个增强版的co

四.术语概念

Effect

Effect指的是描述对象,相当于redux-saga中间件可识别的操作指令,例如调用指定的业务方法(call(myFn))、dispatch指定action(put(action)

An Effect is simply an object which contains some information to be interpreted by the middleware.

Effect层存在的主要意义是为了易测试性,所以用简单的描述对象来表示操作,多这样一层指令

虽然可以直接yield Promise(比如上面核心实现里的示例),但测试case中无法比较两个promise是否等价。所以添一层描述对象来解决这个问题,测试case中可以简单比较描述对象,实际起作用的Promise由redux-saga内部生成

这样做的好处是单测中不用mock异步方法(一般单测中会把所有异步方法替换掉,只比较传入参数是否相同,而不做实际操作),可以简单比较操作指令(Effect)是否等价。从单元测试的角度来看,Effect相当于把参数提出去了,让“比较传入参数是否相同”这一步可以在外面统一进行,而不用逐个mock替换

P.S.关于易测试性的更多信息,请查看Testing Sagas

另外,mock测试不但比较麻烦,还不可靠,毕竟与真实场景/流程有差异。通过框架约束,多一层描述对象来避免mock

这样做并不十分完美,还存在2个问题:

  • 业务代码稍显麻烦(不直接yield promise/dispatch action,而都要用框架提供的creator(call, put)包起来)
  • 有额外的学习成本(理解各个creator的语义,适应先包一层的玩法)

例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 直接
const userInfo = yield API.fetch('user/info', userId);// 包一层creator
const userInfo = yield call(API.fetch, 'user/info', userId);
// 并指定context,默认是null
const userInfo = yield call([myContext, API.fetch], 'user/info', userId);

形式上与fn.call类似(实际上也提供了一个apply creator,形式与fn.apply类似),内部处理也是类似的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// call返回的描述对象(Effect)
{
   @@redux-saga/IO: true,
   CALL: {
       args: ["user/info", userId],
       context: myContext,
       fn: fetch
   }
}// 实际执行
result = fn.apply(context, args)

写起来不那么直接,但比起易测试性带来的好处(不用mock异步函数),这不很过分

注意,不需要mock异步函数只是简化了单元测试的一个环节,即便使用这种对比描述对象的方式,仍然需要提供预期的数据,例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 测试场景直接执行
const iterator = fetchProducts()// expects a call instruction
assert.deepEqual(
 iterator.next().value,
 call(Api.fetch, '/products'),
 "fetchProducts should yield an Effect call(Api.fetch, './products')"
)// 预期接口返回数据
const products = {}// expects a dispatch instruction
assert.deepEqual(
 iterator.next(products).value,
 put({ type: 'PRODUCTS_RECEIVED', products }),
 "fetchProducts should yield an Effect put({ type: 'PRODUCTS_RECEIVED', products })"
)

P.S.这种描述对象的套路,和Flux/Redux的action如出一辙:Effect相当于Action,Effect creator相当于Action Creator。区别是Flux用action描述消息(发生了什么),而redux-saga用Effect描述操作指令(要做什么)

Effect creator

redux-saga/effects提供了很多用来生成Effect的工具方法。常用的Effect creator如下:

  • 阻塞型方法调用:call/apply 详见Declarative Effects
  • 非阻塞型方法调用:fork/spawn 详见redux-saga’s fork model
  • 并行执行task:all/race 详见Running Tasks In Parallel,Starting a race between multiple Effects
  • 读写state:select/put 详见Pulling future actions
  • task控制:join/cancel/cancelled 详见Task cancellation

大多creator语义都很直白,只有一个需要额外说明下:

  • join用来获取非阻塞的task的返回结果

其中forkspawn都是非阻塞型方法调用,二者的区别是:

  • 通过spawn执行的task完全独立,与当前saga无关 当前saga不管它执行完了没,发生cancel/error也不会影响当前saga 效果相当于让指定task独立在顶层执行,与middleware.run(rootSaga)类似
  • 通过fork执行的task与当前saga有关 fork所在的saga会等待forked task,只有在所有forked task都执行结束后,当前saga才会结束 fork的执行机制与all完全一致,包括cancel和error的传递方式,所以如果任一task有未捕获的error,当前saga也会结束

另外,cancel机制比较有意思:

对于执行中的task序列,所有task自然完成时,把结果向上传递到队首,作为上层某个yield的返回值。如果task序列在处理过程中被cancel掉了,会把cancel信号向下传递,取消执行所有pending task。另外,还会把cancel信号沿着join链向上传递,取消执行所有依赖该task的task

简言之:complete信号沿调用链反向传递,而cancel信号沿task链正向传递,沿join链反向传递

注意:yield cancel(task)也是非阻塞的(与fork类似),而被cancel掉的任务在完成善后逻辑后会立即返回

P.S.通过join建立依赖关系(取task结果),例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function* rootSaga() {
 // Returns immediately with a Task object
 const task = yield spawn(serverHello, 'world'); // Perform an effect in the meantime
 yield call(console.log, "waiting on server result..."); // Block on the result of serverHello
 const result = yield join(task);
}

Saga

术语Saga指的是一系列操作的集合,是个运行时的抽象概念

redux-saga里的Saga形式上是generator,用来描述一组操作,而generator是个具体的静态概念

P.S.redux-saga里所说的Saga大多数情况下指的都是generator形式的一组操作,而不是指redux-saga自身。简单理解的话:在redux-saga里,Saga就是generator,Sagas就是多个generator

Sagas有2种顺序组合方式:

  • yield* saga()
  • call(saga)

同样,直接yield* iterator运行时展开也面临不便测试的问题,所以通过call包一层Effect。另外,yield*只接受一个iterator,组合起来不很方便,例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function* saga1() {
   yield 1;
   yield 2;
}
function* saga2() {
   yield 3;
   yield 4;
}
function* rootSaga() {
   yield 0;
   // 组合多个generator不方便
   yield* (function*() {
       yield* saga1();
       yield* saga2();
   })();
   yield 5;
}// test
for (let val of rootSaga()) {
   console.log(val);   // 0 1 2 3 4 5
}

注意:实际上,call(saga)返回的Effect与其它类型的Effect没什么本质差异,也可以通过all/race进行组合

Saga Helpers

Saga Helper用来监听action,API形式是takeXXX,其语义相当于addActionListener:

  • take:语义相当于once
  • takeEvery:语义相当于on,允许并发action(上一个没完成也立即开始下一个)
  • takeLatest:限制版的on,不允许并发action(pending时又来一个就cancel掉pending的,只做最新的)

takeEvery, takeLatest是在take之上的封装,take才是底层API,灵活性最大,能手动满足各种场景

P.S.关于3者关系的更多信息,请查看Concurrency

pull action与push action

从控制方式上讲,take是pull的方式,takeEvery, takeLatest是push的方式

pull与push是指:

  • pull action:要求业务方主动去取action(yeild take()会返回action)
  • push action:由框架从外部注入action(takeEvery/takeLatest注册的Saga会被注入action参数)

pull方式的优势在于:

  • 允许更精细的控制 比如可以手动实现takeN的效果(只关注某几次action,用完就释放掉)
  • 以同步形式描述控制流 takeEvery, takeLatest只支持单action,如果是action序列的话要拆开,用take能保留关联逻辑块的完整性,比如登录/注销
  • 别人更容易理解 控制逻辑在业务代码里,而不是藏在框架内部机制里,一定程度上降低了维护成本

P.S.关于pull/push的更多信息,请查看Pulling future actions

五.场景示例

有几个印象比较深的场景,充分体现出了redux-saga的优雅

接口访问

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function* fetchProducts() {
 try {
   const products = yield call(Api.fetch, '/products')
   yield put({ type: 'PRODUCTS_RECEIVED', products })
 }
 catch(error) {
   yield put({ type: 'PRODUCTS_REQUEST_FAILED', error })
 }
}

除了需要知道put表示dispatch action外,几乎不需要什么注释,实际情况就是你想的那样

登录/注销

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function* loginFlow() {
 while (true) {
   yield take('LOGIN')
   // ... perform the login logic
   yield take('LOGOUT')
   // ... perform the logout logic
 }
}

pull action能保持关联action的处理顺序,而不需要额外外部状态控制。这样保证了LOGOUT总是在执行过LOGIN之后的某个时刻发生的,代码看起来相当漂亮

特定操作提示

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 在创建第3条todo的时候,给出提示消息
function* watchFirstThreeTodosCreation() {
 for (let i = 0; i < 3; i++) {
   const action = yield take('TODO_CREATED')
 }
 yield put({type: 'SHOW_CONGRATULATION'})
}// 接口访问异常重试
function* updateApi(data) {
 for(let i = 0; i < 5; i++) {
   try {
     const apiResponse = yield call(apiRequest, { data });
     return apiResponse;
   } catch(err) {
     if(i < 4) {
       yield call(delay, 2000);
     }
   }
 }
 // attempts failed after 5 attempts
 throw new Error('API request failed');
}

即takeN的示例,这样就把本应该存在于reducer中的副作用提到了外面,保证了reducer的纯度

六.优缺点

优点:

  • 易测试,提供了各种case的测试方案,包括mock task,分支覆盖等等
  • 大而全的异步控制库,从异步流程控制到并发控制应有尽有
  • 完备的错误捕获机制,阻塞型错误可try-catch,非阻塞型会通知所属Saga
  • 优雅的流程控制,可读性/精炼程度不比async&await差多少,很容易描述并行操作

缺点:

  • 体积略大,1700行,min版24KB,实际上并发控制等功能很难用到
  • 依赖ES6 generator特性,可能需要polyfill

P.S.redux-saga也可以接入其它环境(不与Redux绑定),详细见Connecting Sagas to external Input/Output

参考资料

  • JavaScript Power Tools Part II: Composition Patterns In Redux-Saga
  • API Reference
  • Reference 6: A Saga on Sagas
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2017-10-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
安全研究 | 实现账户劫持的越权漏洞(IDOR)分析
不安全的直接对象引用(IDOR也称越权)是一种非常常见的Web授权逻辑型漏洞,其漏洞利用危害有时很小,有时也非常严重,今天我们就来讨论一种严重型的IDOR漏洞利用方式-即利用IDOR实现账户或项目的劫持。作者以该漏洞利用方式在多个众测项目中获得了多达$25,000的奖励赏金。
FB客服
2020/11/06
1.3K0
安全研究 | 实现账户劫持的越权漏洞(IDOR)分析
详解越权漏洞
越权漏洞是指应用程序未对当前用户操作的身份权限进行严格校验,导致用户可以操作超出自己管理权限范围的功能,从而操作一些非该用户可以操作的行为。**简单来说,就是攻击者可以做一些本来不该他们做的事情(增删改查)**。
用户1709153
2023/05/22
1K0
越权漏洞(IDOR)测试技巧「建议收藏」
IDOR,Insecure Direct Object reference,即”不安全的直接对象引用”,场景为基于用户提供的输入对象进行访问时,未进行权限验证。IDOR漏洞其实在越权(Broken Access Control)漏洞的范畴之内,也可以说是逻辑漏洞,或是访问控制漏洞,国内通常被称为越权漏洞。
全栈程序员站长
2022/07/01
1.1K0
逻辑漏洞之越权、支付漏洞
因为程序本身逻辑不严或逻辑太复杂,导致一些逻辑分支不能够正常处理或处理错误,造成的一系列漏洞
宸寰客
2020/09/08
2.6K0
如何发现更多的IDOR漏洞(越权漏洞)
IDOR,Insecure Direct Object reference,即”不安全的直接对象引用”,场景为基于用户提供的输入对象进行访问时,未进行权限验证。IDOR漏洞其实在越权(Broken Access Control)漏洞的范畴之内,也可以说是逻辑漏洞,或是访问控制漏洞,国内通常被称为越权漏洞。具体可点此参考。
FB客服
2019/12/31
1.9K0
账户接管(Account Takeover)漏洞挖掘及实战案例全汇总
身份验证(Authentication):验证某人是特定用户,是否正确提供其安全凭据(密码,安全问题答案,指纹扫描等)。
Jayway
2019/09/29
5.1K0
账户接管(Account Takeover)漏洞挖掘及实战案例全汇总
网站漏洞挖掘思路
未授权访问漏洞,是在攻击者没有获取到登录权限或未授权的情况下,不需要输入密码,即可通过输入网站控制台主页面地址或者不允许查看的连接便可进行访问,同时进行操作。
R0A1NG
2022/06/15
1.6K0
如何发现并处置越权漏洞? | FreeBuf甲方群话题讨论
越权访问是Web应用程序中的一种常见漏洞, 在OWASP发布的2021年十大Web应用安全风险榜单中排列第一。在攻防实战中,越权漏洞也常常是红方进行渗透的重要手段之一,不仅运用范围广,危害也较大,本次话题讨论将围绕如何防范越权漏洞,就相关问题展开讨论。 现在越权漏洞有哪些检测方法?比较好的思路是什么? A1: 目前只知道越权漏洞需要通过人工来检测,如果公司能力有限,比较好的思路就是请经验丰富的人来测试。 A2: 越权漏洞算是逻辑漏洞里面比较容易自动化发现的了吧,我记得看过某公司的DevSecOps材料,他
FB客服
2023/03/30
1.4K0
如何发现并处置越权漏洞? | FreeBuf甲方群话题讨论
SRC逻辑漏洞挖掘详解以及思路和技巧
由于程序逻辑不严谨或逻辑太过复杂,导致一些逻辑分支不能正常处理或处理错误,统称为业务逻辑漏洞。常见的逻辑漏洞有交易支付、密码修改、密码找回、越权修改、越权查询、突破限制等,下图是简单的逻辑漏洞总结,在挖掘的过程中更多的时候需要脑洞大开:
HACK学习
2020/02/26
5.8K0
用BurpSuite实现越权漏洞(IDOR)的自动发现识别
这里分享一个自动化发现IDOR(越权)漏洞的方法,那就是在BurpSuite中利用Autozie和Autorepeater插件实现IDOR漏洞的探测识别,而无需针对每个请求手动去变化参数或请求。
FB客服
2020/02/20
2.3K0
用BurpSuite实现越权漏洞(IDOR)的自动发现识别
信息泄露(Information Exposure)挖掘及实战案例全汇总
信息泄露(InformationExposure)漏洞是有意或无意地向未明确授权访问该信息的行为者披露信息。信息泄露是最为常见和普遍的漏洞之一,漏洞出现的位置、造成的危害有很大的差异性,以下列出的是Acunetix在扫描时列为范围内的漏洞,同一漏洞评级差距极大。
Jayway
2019/09/29
3.1K0
信息泄露(Information Exposure)挖掘及实战案例全汇总
记录一些逻辑漏洞与越权的姿势
最近在看逻辑漏洞与越权相关书籍,记录一些常用的方法,每次检测的时候按照不同业务类型一个一个的去测试业务处
HACK学习
2019/12/25
2.4K0
水平越权挖掘技巧与自动化越权漏洞检测
自从2022.5入职字节无恒实验室以来,我一直从事SDLC相关工作,在SDLC中自动化安全能力卡点之一就是DAST(Dynamic Application Security Test),也是非常重要的用于实现安全左移的手段,我本人近两个月也在从事DAST产品的开发,目标自然也是希望能投入商用。我开发的DAST不会开源,但是最近恰逢一个契机需要写一款安全工具,于是随便用python简单写了一个水平越权的检测工具,误报率、漏水率这些数据不关心,反正能有个代码然后会做PPT扯淡就行了。
Y1ng
2023/03/08
4.9K2
水平越权挖掘技巧与自动化越权漏洞检测
十大漏洞之逻辑漏洞
在十大漏洞中,逻辑漏洞被称为“不安全的对象引用,和功能级访问控制缺失”。现如今,越权和逻辑漏洞占用比例比较高,包括任意查询用户信息,重置任意用户密码,验证码爆破等。
全栈程序员站长
2022/09/03
1.2K0
十大漏洞之逻辑漏洞
SRC挖掘—web不安全的直接对象引用 (IDOR)漏洞-3day
当应用程序根据用户提供的输入提供对对象的直接访问时,就会发生不安全的直接对象引用 (IDOR)。由于此漏洞,攻击者可以绕过授权并直接访问系统中的资源,例如数据库记录或文件。不安全的直接对象引用允许攻击者通过修改用于直接指向对象的参数值来绕过授权并直接访问资源。这些资源可以是属于其他用户的数据库条目、系统中的文件等等。这是因为应用程序接受用户提供的输入并使用它来检索对象而没有执行足够的授权检查。(来源: OWASP)
全栈程序员站长
2022/09/05
6150
何为越权、如何解决?
小编最近在公司送审的后台接口发现了严重的越权漏洞,今天就和大家谈下为何越权漏洞。
BUG弄潮儿
2020/06/12
3.9K0
何为越权、如何解决?
[红日安全]Web安全Day7 - 越权/非授权访问实战攻防
大家好,我们是红日安全-Web安全攻防小组。此项目是关于Web安全的系列文章分享,还包含一个HTB靶场供大家练习,我们给这个项目起了一个名字叫 Web安全实战 ,希望对想要学习Web安全的朋友们有所帮助。每一篇文章都是于基于漏洞简介-漏洞原理-漏洞危害-测试方法(手工测试,工具测试)-靶场测试(分为PHP靶场、JAVA靶场、Python靶场基本上三种靶场全部涵盖)-实战演练(主要选择相应CMS或者是Vulnhub进行实战演练),如果对大家有帮助请Star鼓励我们创作更好文章。如果你愿意加入我们,一起完善这个项目,欢迎通过邮件形式(sec-redclub@qq.com)联系我们。
红日安全
2020/02/24
2.7K0
Web攻防作业 | 越权访问漏洞全解析
通常情况下,一个 Web 程序功能流程是登录 - 提交请求 - 验证权限 - 数据库查询 - 返回结果。在验证权限阶段逻辑不够缜密,便会导致越权。(常见的程序都会认为通过登录后即可验证用户的身份,从而不会做下一步验证,最后导致越权。)
Ms08067安全实验室
2022/12/22
2.6K0
Web攻防作业 | 越权访问漏洞全解析
Web安全系列——越权访问(权限控制失效)
本文将介绍越权访问的原理、风险以及典型攻击场景,并为开发者提供有效的防范措施,帮助构建安全的Web应用。
windealli
2023/10/13
2.6K0
Web安全系列——越权访问(权限控制失效)
浅谈逻辑漏洞
整理下逻辑漏洞:程序本身逻辑不严或逻辑太复杂,导致一些逻辑分支不能够正常处理或处理错误,造成的一系列漏洞
中龙技术
2022/09/28
9400
浅谈逻辑漏洞
推荐阅读
相关推荐
安全研究 | 实现账户劫持的越权漏洞(IDOR)分析
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验