博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
记一次redux-saga的项目实践总结
阅读量:5746 次
发布时间:2019-06-18

本文共 15243 字,大约阅读时间需要 50 分钟。

前言

本文主要记录了在项目中使用redux-saga的一些总结,如有错误的地方欢迎指正互相学习。

redux中的action仅支持原始对象(plain object),处理有副作用的action,需要使用中间件。中间件可以在发出action,到reducer函数接受action之间,执行具有副作用的操作。

redux-thunk 和 redux-saga 是 redux 应用中最常用的两种异步流处理方式。

之前一直使用redux-thunk处理异步等副作用操作,在action中处理异步等副作用操作,此时的action是一个函数,以dispatch,getState作为形参,函数体内的部分可以执行异步。通过redux-thunk来处理异步,action可谓是多种多样,不利于维护。

redux-thunk

redux-thunk简单介绍

redux-thunk 的任务执行方式是从 UI 组件直接触发任务。

redux-thunk中间件可以让action创建函数先不返回一个action对象,而是返回一个函数,函数传递两个参数(dispatch,getState),在函数体内进行业务逻辑的封装

redux-thunk 的主要思想是扩展 action,使得 action 从一个对象变成一个函数。

redux-thunk使用

比如下面是一个获取礼品列表的异步操作所对应的action

export default () => dispatch => {  fetch('/api/goodList', {    // fecth返回的是一个promise    method: 'get', dataType: 'json' }).then(    json => {      var json = JSON.parse(json)      if (json.code === 200) {        dispatch({ type: 'init', data: json.data })      }    }, error => { console.log(error) }  )}复制代码

从这个具有副作用的action中,我们可以看出,函数内部极为复杂。如果需要为每一个异步操作都如此定义一个action,显然action不易维护。

redux-thunk缺点

总结一下redux-thunk缺点有如下几点:

  1. action 虽然扩展了,但因此变得复杂,后期可维护性降低;

  2. thunks 内部测试逻辑比较困难,需要mock所有的触发函数;

  3. 协调并发任务比较困难,当自己的 action 调用了别人的 action,别人的 action 发生改动,则需要自己主动修改;

  4. 业务逻辑会散布在不同的地方:启动的模块,组件以及thunks内部。


redux-saga

redux-saga简单介绍

redux-saga中是这样介绍的:

redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。

刚开始了解Saga时,看官方解释,并不是很清楚到底是什么?Saga的副作用(side effects)到底是什么?

通读了官方文档后,大概了解到,副作用就是在action触发reduser之后执行的一些动作, 这些动作包括但不限于,连接网络,io读写,触发其他action。并且,因为Sage的副作用是通过redux的action触发的,每一个action,sage都会像reduser一样接收到。并且通过触发不同的action, 我们可以控制这些副作用的状态, 例如,启动,停止,取消。

所以,我们可以理解为Sage是一个可以用来处理复杂的异步逻辑的模块,并且由redux的action触发。

saga特点:

1.saga的应用场景是复杂异步,如长时事务LLT(long live.transcation)等业务场景。2.方便测试,可以使用takeEvery打印logger。3.提供takeLatest/takeEvery/throttle方法,可以便利的实现对事件的仅关注最近事件、关注每一次、事件限频4.提供cancel/delay方法,可以便利的取消、延迟异步请求5.提供race(effects),[…effects]方法来支持竞态和并行场景6.提供channel机制支持外部事件复制代码

Redux Saga适用于对事件操作有细粒度需求的场景,同时他们也提供了更好的可测试性。

redux-saga使用

注意:⚠️redux-saga是通过ES6中的generator实现的(babel的基础版本不包含generator语法,因此需要在使用saga的地方import ‘babel-polyfill’)。

redux-saga本质是一个可以自执行的generator。

在 redux-saga 中,UI 组件自身从来不会触发任务,它们总是会 dispatch 一个 action 来通知在 UI 中哪些地方发生了改变,而不需要对 action 进行修改。redux-saga 将异步任务进行了集中处理,且方便测试。

所有的东西都必须被封装在 sagas 中。sagas 包含3个部分,用于联合执行任务:

worker saga

(1)做所有的工作,如调用 API,进行异步请求,并且获得返回结果

watcher saga

(2)监听被 dispatch 的 actions,当接收到 action 或者知道其被触发时,调用 worker saga 执行任务

(3)root saga

立即启动 sagas 的唯一入口

项目中我是这样用的,如果你有更好的实现方法请分享给我:

给redux添加中间件

在定义生成store的地方,引入并加入redux-sage中间件。

// store/index.jsimport { createStore, applyMiddleware, compose } from 'redux'import { routerMiddleware } from 'react-router-redux'import createSagaMiddleware from 'redux-saga'import createHistory from 'history/createHashHistory'import { createLogger } from 'redux-logger'import { rootSaga } from '../rootSaga'import reducers from '../reducers/saga-reducer'const history = createHistory()const middlewareRouter = routerMiddleware(history)const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || composeconst loggerMiddleware = createLogger({ collapsed: true })// 这是一个可以帮你运行saga的中间件const sagaMiddleware = createSagaMiddleware()const store = createStore(reducers,  composeEnhancers(  applyMiddleware(  sagaMiddleware, middlewareRouter, loggerMiddleware  )))// 通过中间件执行或者说运行sagasagaMiddleware.run(rootSaga, store)window.store = storeexport default store复制代码

说明:程序启动时,run(rootSaga) 会开启 sagaMiddleware 对某些 action 进行监听,当后续程序中有触发 dispatch(action) (比如:用户点击)的时候,由于数据流会经过 sagaMiddleware,所以 sagaMiddleware 能够判断当前 action 是否有被监听?如果有,就会进行相应的操作(比如:发送一个异步请求);如果没有,则什么都不做。

// rootSaga.js// 处理浏览器兼容问题import 'babel-polyfill'import { all,call } from 'redux-saga/effects'import { lotterySagaRoot } from './components'import { getchampionListFlow, getTabsListFlow } from './container'export function* rootSaga () {  yield all([call(getTabsListFlow),    call(getchampionListFlow),    call(lotterySagaRoot),  ])}复制代码

rootSaga是我们实际发送给Redux中间件的。

rootSaga在应用程序启动时被触发一次,可以被认为是在后台运行的进程,监视着所有的动作派发到仓库(store)。

我们单拿出一个 getTabsListFlow 这个saga来进行讲解究竟发生了什么?

写到这里有必要说一下业务逻辑了,getTabsListFlow这个函数是一个watcher saga,它 watch 的谁呢?getTabsList这个worker saga函数,废话不多说看代码:

// 处理浏览器兼容问题import 'babel-polyfill'import { call, put, take, fork } from 'redux-saga/effects'import * as types from '../../action_type'import { lists } from '../../actions/server'const { GETLIST, TABS_UPDATE, START_FETCH, FETCH_ERROR, FETCH_END } = types//----worker sagafunction* getTabsList (tabs, rule, env) {  yield put({ type: START_FETCH })  try {    return yield call(lists, tabs, rule, env)  } catch (err) {    yield put({ type: FETCH_ERROR,err})  } finally {    yield put({ type: FETCH_END })  }}//-----watcher sagaexport default function* getTabsListFlow() {  while (true) {    const { tabs, rule, env } = yield take(GETLIST)    const { code, data } = yield call(getTabsList, tabs, rule, env)    yield put({ type: TABS_UPDATE, data, code })  }}复制代码

上面的代码可以看到,getTabsListFlow这个函数响应一个action,“GETLIST”,获取tabs, rule, env这三个参数传给,getTabsList,这个函数,然后把获取到的结果通过响应一个TABS_UPDATE这个action.type给reducer去出更新数据到页面。

那么这些call, put, take, fork这些API后面会讲,总之就是让函数执行获取数据嘛。我们现在需要知道数据流是怎样实现的?

问题1:

“GETLIST”这个action.type代表的是哪个函数,这个函数怎么获取到tabs, rule, env这三个参数的?看代码,其实真的很简单。。。

// actions/index.jsexport function getList(tabs, rule, env) {  return {    type: GETLIST,    tabs,    rule,    env,  }}复制代码

看到没有我导出了这样一个函数,给了它一个action.type就是叫GETLIST, yield take(GETLIST)就是让这个函数执行了,这三个参数也是这样传递进来的,我只需要在页面上引入这个函数去让个函数执行并传递参数就行了。

import React, { Component } from 'react'import { bindActionCreators } from 'redux'import { Link } from 'react-router-dom'import PropTypes from 'prop-types'import { connect } from 'react-redux'import { getList } from '../../actions/index'class List extends Component {  state = {    tabs: 'anchor',    rule: 'hour',    active: 'anchor',    hover: 'allanchor',    visible: false,  }  componentDidMount() {    const { tabs, rule } = this.state    this.props.getList(tabs, rule, env)  }        ....省略一些代码        List.propTypes = {  getList: PropTypes.func}function mapStateToProps(state) {  return {    ...state,  }}function mapDispatchToProps(dispatch) {  return {    getList: bindActionCreators(getList, dispatch),  }}export default connect(  mapStateToProps,  mapDispatchToProps)(List)复制代码

这样的话"const { tabs, rule, env } = yield take(GETLIST)"这一段代码就获取到我传递的参数了。

这里设计到了redux的知识,参考:阮一峰。

问题2:

接下来yield call(getTabsList, tabs, rule, env),让getTabsList执行,里面发了一个请求lists执行并传递参数。

lists是什么?其实它就是一个异步请求。

/** * 排行榜 * * @param {String} type * @param {String} rule * @return {Promise} */export const lists = (type, rule) => req({  endpoint: `${APP_NAME}/data/${type}/${rule}/${env}`,  method: GET,})复制代码

这个是一个被封装好的fectch请求。类似于这样

// 通过fetch获取百度的错误提示页面fetch('https://www.baidu.com/search/error.html?a=1&b=2', { // 在URL中写上传递的参数    method: 'GET'  })  .then((res)=>{    return res.text()  })  .then((res)=>{    console.log(res)  })复制代码

接下来执行到这里 const { code, data } = yield call(getTabsList, tabs, rule, env)

yield put({ type: TABS_UPDATE, data, code }),到这里我们已经通过请求获取到我们想要的数据了,下一步就是去reducer里生成新的state了。

const userReducer = (state = defaultState, action = {}) => {  const { type} = action;  switch (type) {   case TABS_UPDATE:    return Object.assign({}, state, { list: action.data, loading: false })    default: return state;  }};复制代码

总结一下:

(1)引入的 redux-saga/effects 都是纯函数,每个函数构造一个特殊的对象,其中包含着中间件需要执行的指令,如:call(lists, tabs, rule, env) 返回一个类似于 {type: CALL, function: lists, args: [tabs, rule, env]} 的对象。

(2)在 watcher saga getTabsListFlow中:

首先 yield take(GETLIST) 来告诉中间件我们正在等待一个类型为 GETLIST 的 action,然后中间件会暂停执行 getTabsListFlow generator 函数,直到 GETLIST action(getList) 被 dispatch。一旦我们获得了匹配的 action,中间件就会恢复执行 generator 函数。

下一条指令 const { code, data } = yield call(getTabsList, tabs, rule, env) 告诉中间件去执行getTabsList,并把{tabs, rule, env} 作为 getTabsList 函数的参数传递。中间件会触发 getTabsList generator。

(3)在 worker saga getTabsList 中, yield call(lists, tabs, rule, env)指示中间件去调用 fetch 函数,同时,会阻塞getTabsList 的执行,中间件会停止 generator 函数,直到 fetch 返回的 Promiseresolved(或 rejected),然后才恢复执行 generator 函数。

借一张基于 redux-saga 的一次 完整单向数据流单项数据流的图:

到此为止就是我在项目中使用redux-saga针对于其中一个请求来实现的数据处理。

下面开始介绍一些API的使用了:

redux-saga API

安装啥的步骤直接略过....

Effects

前面说到,saga 是一个 generator function,这就意味着它的执行原理必然是下面这样:

function isPromise(value) {    return value && typeof value.then === 'function';}const iterator = saga(/* ...args */);// 方法一:// 一步一步,手动执行let result;result = iterator.next();result = iterator.next(result.value);result = iterator.next(result.value);// ...// done!!// 方法二:// 函数封装,自主执行function next(args) {  const result = iterator.next(args);  if (result.done) {    // 执行结束    console.log(result.value);  } else {    // 根据 yielded 的值,决定什么时候继续执行(resume)     if (isPromise(result.value)) {      result.value.then(next);    } else {      next(result.value)    }  }}next();复制代码

也就是说,generator function 在未执行完前(即:result.done === false),它的控制权始终掌握在 执行者(caller)手中,即:

  • caller 决定什么时候 恢复(resume)执行。

  • caller 决定每次 yield expression 的返回值。

而 caller 本身要实现上面上述功能需要依赖原生 API :iterator.next(value) ,value 就是 yield expression 的返回值。

举个例子:

function* hello() {  const value = yield Promise.reslove('hello saga');  console.log('value: ', value); // value??}复制代码

单纯的看 hello 函数,没人知道 value 的值会是多少?

这完全取决于 gen 的执行者(caller),如果使用上面的 next 方法来执行它,value 的值就是 'hello saga',因为 next 方法对 expression 为 promise 时,做了特殊处理(这不就是缩小版的 co 么~ wow~⊙o⊙)。

换句话说,expression 可以是任何值,关键是 caller 如何来解释 expression,并返回合理的值

以此结论,推理来看:

大家熟知的 co 可以认为是一个 caller,它解释的 expression 是:promise/thunk/generator function/iterator 等。

这里的 sagaMiddleware 也算是一个 caller,它主要解释的 expression 就是 effect(当然还可以是 promise/iterator) 。

讲了这么多,那么 effect 到底是什么呢?先来看看官方解释:

An effect is a plain JavaScript Object containing some instructions to be executed by the saga middleware.

意思是说:effect 本质上是一个普通对象,包含着一些指令信息,这些指令最终会被 saga middleware 解释并执行。

用一段代码​来解释上述这句话:

function* fetchData() {  // 1. 创建 effect  const effect = call(ajax.get, '/userLogin');  console.log('effect: ', effect);  // effect:  // {  //   CALL: {  //     context: null,  //     args: ['/userLogin'],  //     fn: ajax.get,  //   }  // }  // 2. 执行 effect,即:调用 ajax.get('/userLogin')  const value = yield effect;  console.log('value: ', value);}复制代码

可以明显的看出:

call 方法用来创建 effect 对象,被称作是 effect factory。

yield 语法将 effect 对象 传给 sagaMiddleware,被解释执行,并返回值。

这里的 call effect 表示执行 ajax.get('user/Login') ,又因为它的返回值是 promise, 为了等待异步结果返回,fetchData 函数会暂时处于 阻塞 状态。

除了上述所说的 call effect 之外,redux-saga 还提供了很多其他 effect 类型,它们都是由对应的 effect factory 生成,在 saga 中应用于不同的场景,比较常用的是:

takeEvery

允许多个请求同时执行,不管之前是否还有一个或多个请求尚未结束。

// 首先我们创建一个将执行异步 action 的任务:import { call, put,takeEvery } from 'redux-saga/effects'export function* fetchData(action) {   try {      const data = yield call(Api.fetchUser, action.payload.url);      yield put({
type: "FETCH_SUCCEEDED", data}); } catch (error) { yield put({
type: "FETCH_FAILED", error}); }}//然后在每次 FETCH_REQUESTED action 被发起时启动上面的任务。function* watchFetchData() { yield* takeEvery('FETCH_REQUESTED', fetchData)}复制代码

在上面的例子中,takeEvery 允许多个 fetchData 实例同时启动。在某个特定时刻,尽管之前还有一个或多个 fetchData 尚未结束,我们还是可以启动一个新的 fetchData 任务,

如果我们只想得到最新那个请求的响应(例如,始终显示最新版本的数据)。我们可以使用 takeLatest 辅助函数。

takeLatest

作用同takeEvery一样,唯一的区别是它只关注最后,也就是最近一次发起的异步请求,如果上次请求还未返回,则会被取消。

function* watchFetchData() {  yield takeLatest('FETCH_REQUESTED', fetchData)}复制代码

call

all用来调用异步函数,将异步函数和函数参数作为call函数的参数传入,返回一个js对象。saga引入他的主要作用是方便测试,同时也能让我们的代码更加规范化。

同js原生的call一样,call函数也可以指定this对象,只要把this对象当第一个参数传入call方法就好了

saga同样提供apply函数,作用同call一样,参数形式同js原生apply方法。

// 模拟数据异步获取function fn() {  return new Promise((resolve, reject) => {    setTimeout(() => {      resolve('hello saga');    }, 2000);  });}function* fetchData() {  // 等待 2 秒后,打印欢迎语(阻塞)  const greeting = yield call(fn);  console.log('greeting: ', greeting);    }复制代码

fork

非阻塞任务调用机制:上面我们介绍过call可以用来发起异步操作,但是相对于 generator 函数来说,call 操作是阻塞的,只有等 promise 回来后才能继续执行,而fork是非阻塞的 ,当调用 fork 启动一个任务时,该任务在后台继续执行,从而使得我们的执行流能继续往下执行而不必一定要等待返回。

还是上面的栗子:

// 模拟数据异步获取function fn() {  return new Promise((resolve, reject) => {    setTimeout(() => {      resolve('hello saga');    }, 2000);  });}function* fetchData() {  // 立即打印 task 对象(非阻塞)  const task = yield fork(fn);  console.log('task: ', task);}复制代码

显然,fork 的异步非阻塞特性更适合于在后台运行一些不影响主流程的代码(比如:后台打点/开启监听),这往往是加快页面渲染的一种方式。

put

作用和 redux 中的 dispatch 相同。

yield put({ type: 'CLICK_BTN' });复制代码

select

作用和 redux thunk 中的 getState 相同。

const id = yield select(state => state.id);复制代码

take

take(pattern) 用以下规则来解释 pattern:

1.如果调用 take 时参数为空,或者传入 '*',那将会匹配所有发起的 action(例如,take() 会匹配所有的 action)。2.如果是一个函数,action 会在 pattern(action) 返回为 true 时被匹配(例如,take(action => action.entities) 会匹配那些 entities 字段为真的 action)。3.如果是一个字符串,action 会在 action.type === pattern 时被匹配(例如,take(INCREMENT_ASYNC))。4.如果参数是一个数组,会针对数组所有项,匹配与 action.type 相等的 action(例如,take([INCREMENT, DECREMENT]) 会匹配 INCREMENT 或 DECREMENT 类型的 action)。复制代码

当在generator中使用 take语句等待 action 时, generator被阻塞,等待 action被分发,然后继续往下执行,有种 Event.once() 事件监听的感觉。

export function* getAdDataFlow() {    while (true){        let request = yield take(homeActionTypes.GET_AD);        let response = yield call(getAdData,request.url);        yield put({
type:homeActionTypes.GET_AD_RESULT_DATA,data:response.data}) }}复制代码

take VS tackEvery

takeEvery 只是监听每个 action ,然后执行处理函数。对于合适响应 action 和如何响应 action, tackEvery 没有权限。

最大的区别:

take 只有在执行流达到时才回响应 action ,而 takeEvery 则一经注册,都会响应action

all

all提供了一种并行执行异步请求的方式。之前介绍过执行异步请求的api中,大都是阻塞执行,只有当一个call操作放回后,才能执行下一个call操作,call提供了一种类似Promise中的all操作,可以将多个异步操作作为参数参入all函数中, 如果有一个call操作失败或者所有call操作都成功返回,则本次all操作执行完毕。

import { all, call } from 'redux-saga/effects' // correct, effects will get executed in parallelconst [users, repos]  = yield all([  call(fetch, '/users'),  call(fetch, '/repos')])复制代码

race

有时候当我们并行的发起多个异步操作时,我们并不一定需要等待所有操作完成,而只需要有一个操作完成就可以继续执行流。这就是race的用处。

他可以并行的启动多个异步请求,只要有一个 请求返回(resolved或者reject),race操作接受正常返回的请求,并且将剩余的请求取消。

import { race, take, put } from 'redux-saga/effects' function* backgroundTask() {  while (true) { ... }} function* watchStartBackgroundTask() {  while (true) {    yield take('START_BACKGROUND_TASK')    yield race({      task: call(backgroundTask),      cancel: take('CANCEL_TASK')    })  }}复制代码

actionChannel  

在之前的操作中,所有的action分发是顺序的,但是对action的响应是由异步任务来完成,也即是说对action的处理是无序的。

如果需要对action的有序处理的话,可以使用actionChannel函数来创建一个action的缓存队列,但一个action的任务流程处理完成后,才可是执行下一个任务流。

import { take, actionChannel, call, ... } from 'redux-saga/effects' function* watchRequests() {  // 1- Create a channel for request actions  const requestChan = yield actionChannel('REQUEST')  while (true) {    // 2- take from the channel    const {payload} = yield take(requestChan)    // 3- Note that we're using a blocking call    yield call(handleRequest, payload)  }} function* handleRequest(payload) { ... }复制代码

Error Handling

在 saga 中,无论是请求失败,还是代码异常,均可以通过 try catch 来捕获。

倘若访问一个接口出现代码异常,可能是网络请求问题,也可能是后端数据格式问题,但不管怎样,给予日志上报或友好的错误提示是不可缺少的,这也往往体现了代码的健壮性,一般会这么做:

function* saga() { try {   const data = yield call(fetch, '/someEndpoint');   return data; }  catch (error) {    yield put(onError(error));  }}复制代码

Watcher/Worker

指的是一种使用两个单独的 Saga 来组织控制流的方式。

Watcher: 监听发起的 action 并在每次接收到 actionfork 一个 worker

Worker: 处理 action 并结束它。

function* watcher() {  while(true) {    const action = yield take(ACTION)    yield fork(worker, action.payload)  }}function* worker(payload) {  // ... do some stuff}复制代码

事实上因为项目的局限性很多API并没有用上,可以根据项目的实际需求使用这些API,因为它们真的很有意思!!

以上~


参考

文章:

  • -

项目参考:

转载地址:http://fiazx.baihongyu.com/

你可能感兴趣的文章
实现Hyper-V 虚拟机在不同架构的处理器间迁移
查看>>
简单使用saltstack
查看>>
针对web服务器容灾自动切换方案
查看>>
突破媒体转码效率壁垒 阿里云首推倍速转码
查看>>
容器存储中那些潜在的挑战和机遇
查看>>
R语言的三种聚类方法
查看>>
深入理解Python中的ThreadLocal变量(上)
查看>>
如果一切即服务,为什么需要数据中心?
查看>>
《游戏开发物理学(第2版)》一导读
查看>>
Erlang简史(翻译)
查看>>
深入实践Spring Boot2.4.2 节点和关系实体建模
查看>>
10个巨大的科学难题需要大数据解决方案
查看>>
Setting Up a Kerberos server (with Debian/Ubuntu)
查看>>
用 ThreadLocal 管理用户session
查看>>
setprecision后是要四舍五入吗?
查看>>
shiro初步 shiro授权
查看>>
上云就是这么简单——阿里云10分钟快速入门
查看>>
MFC多线程的创建,包括工作线程和用户界面线程
查看>>
我的友情链接
查看>>
FreeNAS8 ISCSI target & initiator for linux/windows
查看>>