记一次redux-saga的项目实践总结

前言

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

redux中的action仅支持原始对象(plain object),处理有反作用的action,须要使用中间件。中间件能够在发出action,到reducer函数接受action之间,执行具备反作用的操做。react

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

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

redux-thunk

redux-thunk简单介绍

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

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

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

redux-thunk使用

好比下面是一个获取礼品列表的异步操做所对应的actionredux

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缺点有以下几点:api

  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.js

import { 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__ || compose
const loggerMiddleware = createLogger({ collapsed: true })
// 这是一个能够帮你运行saga的中间件
const sagaMiddleware = createSagaMiddleware()

const store = createStore(reducers,
  composeEnhancers(
  applyMiddleware(
  sagaMiddleware, middlewareRouter, loggerMiddleware
  )))

// 经过中间件执行或者说运行saga
sagaMiddleware.run(rootSaga, store)

window.store = store
export 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 saga

function* 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 saga

export 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.js
export 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的知识,参考:阮一峰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 parallel
const [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,由于它们真的颇有意思!!

以上~


参考

文章:

项目参考:

相关文章
相关标签/搜索