{ PH_Dev }

Published on

Redux 非同步資料流 - Redux saga 基本用法

Authors
  • avatar
    Name
    Penghua Chen(PH)
    Twitter

今天要學習的是在 Redux 中另一套可以用來處理非同步資料套件: Redux saga

這套與 Redux thunk 都是目前算是蠻主流的套件,在使用上可以依據自己的喜好選擇使用

往下學習怎麼用之前,我們先對於 Redux-saga 再來多一點認識吧!

Redux-saga 的特色

依據文件提到,這裡條列出幾個重點:

  1. 可以在 Redux-saga 中處理 side effect 的行為,例如像是 fetching data 等,可以更有效率、簡單的管理。
  2. 作為 Redux 的 middleware ,可以在 Redux 中調度 Redux 的操作,例如在 middleware 中 dispatch 一個 action。
  3. 使用 ES6 的生成器(Generators)來處理非同步流。
  4. 相較於 redux-thunk,不會陷入到 回呼地獄(callback hell)。

以上為文件中提到關於 redux-saga 的特色,其中也提到了一個很重要的核心概念:生成器(Generators)

所以接著我們要先學習一下關於生成器(Generators)的一些知識。

生成器(Generators)

生成器(Generators)是 ES6 出的功能,那為什麼 Redux-saga 會是透過它來實作出這個套件呢?

接著我們看一下關於生成器(Generators)的定義與一些關鍵字

什麼是生成器?

生成器函式會產生一連串的值,但並不是一次全部產生出來,而是對每次請求產生一個值

在產生一個值之後,生成器函式不會像正常函式那樣結束執行,它只是先暫停,然後再請求另一個值時,從暫停的地方恢復運作

生成器不同於標準函式,呼叫生成器不是執行生成器函式,而是建立出一個迭代器(iterator)的物件。

如何定義一個生成器函式?

定義一個生成器函式: 在 function 後面加上一個星號(*) 即可。

然後透過 yield 關鍵字來產生個別的值

如何取得生成器產生的值?

呼叫迭代器(iterator)的方法: next(),可以向迭代器請求一個值,當生成器遇到 yeild 關鍵字時,會產生一個物件的值作為結果回傳,然後就暫時暫停執行,讓頁面其他動作可以繼續,直到生成器再次透過 next() 方法呼叫時才會再次喚醒生成器。

而剛剛提到回傳的結果為一個物件,從這個物件的值我們可以判斷是否還有值可以產生出來。

例如物件的值長這樣的時候:

{value: "Katana", done: false}

由於 donefalse,代表還有值可以產生

如果得到的值是 {value: undefined, done: true}donetrue,代表已經沒有值可以產生了。

大致了解了生成器(Generators)的定義、設定方式和如何取值之後,透過一個簡單的程式碼來幫助理解吧

function* FruitGenerator() {
  yield "apple";
  yield "pineapple";
  yield "orange";
}

var fruitIterator = FruitGenerator();
console.log(fruitIterator.next());
console.log(fruitIterator.next());
console.log(fruitIterator.next());
console.log(fruitIterator.next());

上面程式碼建立了一個 FruitGenerator 的生成器,並且我們產生了一個 fruitIterator 的迭代器物件,而當我們一次次請求時,也就是透過連續呼叫 next() 方法時,會取得一連串的回傳值:

{value: "apple", done: false}
{value: "pineapple", done: false}
{value: "orange", done: false}
{value: undefined, done: true}

關於生成器(Generators)的學習大致先到這裡,接著我們看看怎麼使用 Redux-saga

Redux-saga 的使用

安裝 Redux-saga

安裝的部分相信大家都很熟悉了,可以透過 npm 或 yarn 的方式安裝

npm install --save redux-saga
yarn add redux-saga

然後再往下之前,我們先將流程順過一次

理解加入 Redux-saga 前後的流程差異

將 redux-saga 加入到 redux 之前,我們先把 redux 更新同步資料的流程再順過一次:

這裡把重點放在 React component dispatch 一個 action 到 reducer 中。

  1. 在 component 中透過 connect 連接到 redux store
  2. 透過 mapDispatchToProps dispatch 一個 actionreducer 中,並依照 action type 執行對應的 state 更新流程。

而加入 redux-saga 之後的流程:

  1. 在 component 中透過 connect 連接到 redux store
  2. 透過 mapDispatchToProps dispatch 一個 action 到 redux-saga 中,然後 在 redux-saga 中依照 action type 執行對應的非同步流程(例如: call api)後,再把資料往 reducer 中觸發對應的 state 更新流程。

上述流程的差異一定要先弄懂,才不會在實作的時候覺得卡住哦!

模擬一個非同步流程: 兩秒後變更卡片元件的內容

這裡為了可以更加了解完整整個 Redux 加入 redux-saga 的流程。

所以撰寫了一個範例,並且從建置 Redux 開始一一了解。

相關測試範例,點擊前往

首先是這個範例的資料結構:

  • components: 管理 Card 元件
  • store:
    • reducers: 管理 reducer,並依據 action type 更新 state
    • sagas: 管理非同步流程,從 React component 中先 dispatch 到 middleware 中,處理完非同步流程後再 dispatch 到 reducer 中。

建立一個 Card 元件

建立元件相信大家都很熟悉了,所以不再多做贅述:

稍微需要提的是這裡透過前面篇幅學到的 styled componet 方式設定樣式的部分。

import React from "react";
import styled from "styled-components";

const CardDiv = styled.div`
  width: 200px;
  border: 1px solid #8d8d8d;
  border-radius: 10px;
  margin: 0 auto;
`;

const Card = (props) => (
  <CardDiv>
    <h3 className="name">{props.person.name}</h3>
    <p className="age">{props.person.age}</p>
    <p className="habbit">{props.person.habbit}</p>
  </CardDiv>
);

export default Card;

接著,安裝、引入 Redux、建立 reducer

安裝 Redux 的部分不多提,這邊直接看看引入與建立 reducer 檔案:

首先先建立 reducer:

const initialState = {
  person: {
    name: "Bill",
    age: 28,
    habbit: "Read comic books"
  }
};

export const rootReducer = (state = initialState, action) => {
  switch (action.type) {
    case "changeCardContent":
      return {
        ...state,
        person: action.payload
      };
    default:
      return state;
  }
};

接著引入 Redux,並將 reducer 傳入:

// store/index.js
import { rootReducer } from "./reducers/rootReducer";
import { createStore } from "redux";

export const store = createStore(rootReducer);

連接 Redux 與 React component,以 class-based component 為例:

import React, { Component } from "react";
import { connect } from "react-redux";
import "./styles.css";

import Card from "./components/Card";

class App extends Component {
  render() {
    return (
      <div className="App">
        <h2>使用 redux-saga 模擬非同步流程</h2>
        <p>點擊按鈕後兩秒變更卡片的內容。</p>
        <Card person={this.props.person} />
        <button>Click Me!</button>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    person: state.person
  };
};

export default connect(mapStateToProps)(App);

到這裡沒意外的話,應該可以成功將 Redux 中的 person 的資料渲染到畫面上囉!

重頭戲上場,加入 redux-saga middleware

完成前置作業後,接著我們將 redux-saga 這個 middleware 加入到 Redux 中了!

還記得剛剛提到的關於流程變更的那個部分嗎?

「透過 mapDispatchToProps dispatch 一個 action 到 redux-saga 中,然後 在 redux-saga 中依照 action type 執行對應的非同步流程(例如: call api)後,再把資料往 reducer 中觸發對應的 state 更新流程。」

這裡我們先在 sagas 資料夾中建立一個 index.js 檔案,並設定好讓 React component 可以 dispatch 到 saga middleware 中的 action 與要 dipatch 到 reducer 中的 action

// sagas/index.js
import { delay, put, takeEvery, all } from "redux-saga/effects";
function* changeCardContentHandler() {
  console.log("redux-saga works and execute the generator");
  // 模擬 call api 兩秒後資料回來
  yield delay(2000);

  // disptach an action to reducer
  yield put({
    type: "changeCardContent",
    payload: {
      name: "Charlie",
      age: 13,
      habbit: "Play ball"
    }
  });
}

// receive an action from react component
function* watchChangeCardContentHandlerSaga() {
  yield takeEvery("CHANGE_CARD_CONTENT", changeCardContentHandler);
}

export function* rootSaga() {
  yield all([watchChangeCardContentHandlerSaga()]);
}

這裡有一些關於 redux-saga 中的方法相向會讓人一頭霧水,這部分稍後會提到,我們先繼續把流程建置好。

接著我們要在 redux 中引入 redux-saga,並運行起來:

import { rootReducer } from "./reducers/rootReducer";
import { createStore, applyMiddleware } from "redux";

// 引入 redux-saga
import createSagaMiddleware from "redux-saga";
import { rootSaga } from "./sagas";

// 建立 sagaMiddleware 並引入到 redux 中
const sagaMiddleware = createSagaMiddleware();
export const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));

// 運行我們設定的 saga generator
sagaMiddleware.run(rootSaga);

到這裡已經完成 redux-saga 的建置,最後一步就是在 React component 中 dispatch 一個 action 到 middleware 中囉

import React, { Component } from "react";
import { connect } from "react-redux";
import "./styles.css";

import Card from "./components/Card";

class App extends Component {
  render() {
    return (
      <div className="App">
        <h2>使用 redux-saga 模擬非同步流程</h2>
        <p>點擊按鈕後兩秒變更卡片的內容。</p>
        <Card person={this.props.person} />
        // 觸發事件
        <button onClick={this.props.changeCardContentHandler}>Click Me!</button>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    person: state.person
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    changeCardContentHandler: () => {
      // dipatch an action to redux-saga middleware
      dispatch({
        type: "CHANGE_CARD_CONTENT"
      });
    }
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(App);

順利的應該可以看到成功模擬非同步流程的結果了!

不過還記得剛剛在過程中使用到的那些方法嗎?

  • Middleware API
    • createSagaMiddleware(options)
    • middleware.run(saga, ...args)
  • Effect creators
    • put(action)
    • takeEvery(pattern, saga, ...args)
    • delay(ms, [val])
  • Effect combinators
    • all([...effects])

這些是 redux-saga 提供的方法,透過這些方法我們才可以完成整個 middleware 的建置,接著一一來理解這些輔助函式的定義吧!

Middleware API: createSagaMiddleware(options)

建立 Redux middleware 並將 sagas 與 Redux Store 做連接。

Middleware API: middleware.run(saga, ...args)

運行 saga,只能在 applyMiddleware 後的階段運行才有效。

Effect creators: put(action)

如同 Redux 中的 dispatch,會 disptach 一個 action 到 reducer 中執行後續的 state 更新

Effect creators: takeEvery(pattern, saga, ...args)

讓 React component 可以 dispatch 一個 action 到 middleware 中,並直接符合 pattern 時的第二個參數,也就是要執行的函式。

Effect creators: delay(ms, [val])

用於延遲程式流程執行,單位為毫秒(ms)。

Effect combinators: all([...effects])

可同時運行多個 Effects,並等待這些 Effects 執行完成後才會繼續流程。

關於 redux-saga 的基礎使用學習就到這裡囉,明天見

鐵人賽文章與程式碼同步發佈於:

資源