記一次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,由於它們真的頗有意思!!

以上~


參考

文章:

項目參考:

相關文章
相關標籤/搜索