iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 16
3

前言

在 Redux 中做非同步的請求其實是很麻煩的,這也是一開始讓我卡關的一個痛點,但好在有 StackOverflow 才沒讓這個痛持續很久。

其實一開始我是愛用 redux-thunk 的,但 Redux-Saga 提供了太多方便的 API,讓我可以處理複雜的操作,而且也不會讓程式碼變得難看,這篇會簡單的介紹一些基本的用法,如果有你也對它一見鍾情,那可以到 Redux-Saga 的文檔 查看更詳細的用法。


前置準備

  1. 文中的專案會以 Day14 的專案架構繼續講解,如果未跟到前一天的進度,可以從 GitHub 上 Clone 下來。
  2. 這篇會比較長,得先做好心理準備,然後帶著一顆擁有學習熱忱的心。

使用方法

配置 Redux Saga 使用環境

從 npm 中安裝 Redux-Saga:

npm install --save redux-saga

完成後,還要另外安裝 @babel/polyfill,因為在 Redux-Saga 中會出現 function*,它的名字叫做 Generator Function,像 Arrow Function 一樣,是 JavaScript 中的一種 function。

Generator Function 能夠配合 yield 在執行的過程中喊停。

例如下方是一般的 function,只要執行了就會直接從 0 輸出到 10:

function printNumber() {
  for (let i = 0; i <= 10; i += 1) {
    console.log(i);
  }
}
printNumber() // 0 1 2 3 ... 10

但是如果用 Generator Function 配合上 yield 使用,就能讓程式的運行停在標記 yield 的地方,等待下一次執行:

function* printNumber() {
  for (let i = 0; i <= 10; i += 1) {
    yield console.log(i);
  }
}

// 將 printNumber 的執行交給
const iteratorA = printNumber();

// 接著每一次執行 .next() 都會到 Generator Function 中的 yield,然後停住
iteratorA.next() // 0

// 下一次執行 .next() 時又會執行到 yield,然後停住
iteratorA.next() // 1

// 依此類推,一直到 10
iteratorA.next() // 10

// 再來就沒有了
iteratorA.next()

但是這麼方便的 function 支援度還不是那麼高,所以才要另外安裝 @babel/polyfill ,它能替我們處理使用到 Generator Function 或其他瀏覽器為支援語法的應對方式。

安裝 @babel/polyfill

npm install --save @babel/polyfill

接著打開 webpack.config.js,在 entry 中設置編譯時載入 @babel/polyfill

module.exports = {
  entry: ['@babel/polyfill', './src/index.jsx'],
  /* 其餘省略 */
};

流程說明

首先解釋一下使用了 Redux-Saga 後,原本流程的變化:

原 Redux 流程為先對 Component 做 connect,之後便可使用 Store 的 dispatch 觸發預先在 Reducer 中寫好的邏輯。

而 Redux-Saga 的流程同樣是先對 Component 做 connect,但 dispatch 觸發的事件為 React-Saga 預先訂閱的名稱,在該事件裡才依照流程去觸發需執行的 Reducer 邏輯。

原本被 dispatch 觸發的 Method 從 Reducer 變成 Redux-Saga。

換句話說

Redux-Saga 成為 Component 及 Reducer 之間溝通的橋樑。

如果對這個流程沒有問題,那就能繼續下去,如果還有點搞不清楚,那看完下方的操作說明後可以再回顧一次,應該會更清楚!

使用方法

以下的 API 請求會從 httpbin 這個網站拿現成的來用,大家無聊想玩 API 的時候也可以玩看看。

首先在 src/action/todolist.js 中定義 Action:

export const FETCH_DATA_BEGIN = 'FETCH_DATA_BEGIN';

export const fetchDataBegin = () => ({
  type: FETCH_DATA_BEGIN,
});

export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS';

const fetchDataSuccess = data => ({
  type: FETCH_DATA_SUCCESS,
  payload: {
    data,
  },
});

FETCH_DATA_BEGIN 用來觸發請求的事件,FETCH_DATA_SUCCESS 是請求完成後執行的事件。

接下來是執行的 function 內容,我們需要使用 Fetch 獲取資料,然後送到 Reducer 中更新 State,這個階段我們會使用到 Redux-Saga 中的 callput,這兩個被稱作 Effect,官網的說明如下:

當一個 middleware 透過 Saga 取得一個被 yield 的 Effect,Saga 會被暫停,直到 Effect 被完成。

簡單來說就是在獲得 Effect 執行完的結果前,都不會往下一行執行,而 call 用來執行 function,在此我們將請求的 Fetch 寫在 call 中,讓我們的 fetchData 可以等到拿到資料後再繼續執行:

import { call } from 'redux-saga/effects';

function* fetchData() {
  // 使用 data 接收請求的資料
  const data = yield call(
    () => fetch('https://httpbin.org/get')
      .then(response => response.json()),
  );
}

call 取到資料後,就輪到 put 登場了,put 可以作為 dispatch 觸發 Reducer,使用方式也和 dispatch 一模一樣,以下觸發 Reducer 中的 FETCH_DATA_SUCCESS,將 call 獲取到的資料送到 Reducer 中儲存:

import { call, put } from 'redux-saga/effects';

function* fetchData() {
  // ...使用 call 獲取資料
  
  yield put(fetchDataSuccess(data));
}

接下來,記得上方說過「原本被 dispatch 觸發的 Method 從 Reducer 變成 Redux-Saga。」,因此我們必須在 Redux-Saga 中訂閱 FETCH_DATA_BEGIN 執行 fetchData 獲取資料,並寫到 Reducer 中。

通常我們會把訂閱事件統一到一個 Function 中,管理一種 Redcuer 的 State,在 Function 內可以使用 takeEvery 來訂閱相同類別的所有事件:

import { call, put, takeEvery } from 'redux-saga/effects';


function* mySaga() {
  yield takeEvery(FETCH_DATA_BEGIN, fetchData);
}

export default mySaga;

mySaga 這個 Generator Function 便是在訂閱事件, Function 內的每一項 takeEvery 都在訂閱一個事件,觸發的 action.typeFETCH_DATA_BEGIN,執行的事件是 fetchData

到此, src/action/todolist.js 的準備就先完成了,為了更好的管理散佈在不同檔案間的 Saga,筆者會傾向於在 src 下建立另一個目錄管理 sagas/index.js:

|-src
 |-action
 |-reducer
 |-sagas
  |-index.js
 |-store

剛好,這時候是個好時間,我把 position 資料夾移除,畢竟他在接下來的文章中不會再有登場的機會。

打開 src/sagas/index.js,將 src/action/todolist.js 中訂閱事件的 mySaga 給 import,並用另一個 Generator Function 包裝所有 Saga,雖然目前僅有一個但如果有多個的話,只需要再 all 的陣列中添加新 Saga 即可:

import { all } from 'redux-saga/effects';
import toodlistSaga from '../action/todolist';

function* rootSaga() {
  yield all([
    toodlistSaga(),
  ]);
}

export default rootSaga;

接下去講 Redux-Saga 前,先到 src/reducer/todolist.js 中加上 FETCH_DATA_SUCCESS 的 Action 動作:

/*其餘未更動的程式碼省略*/

const initState = {
  todoList: ['第一件事情', '第二件事情'],
  data: {},
};

const todoReducer = (state = initState, action) => {
  switch (action.type) {
      /*省略 ADD_TODO */
    case actions.FETCH_DATA_SUCCESS:
      return {
        ...state,
        data: action.payload.data,
      };
     /*省略 default */
  }
};

export default todoReducer;

最後一個階段,要把剛剛設置的 mySaga 創建成一個 Middleware,放進 Store 中,這樣子如果我們用 dispatch 觸發到用 takeEvery 訂閱的事件,那 Redux-Saga 就會替我們處理事情了。

打開 src/store/index.js 從 Redux-Saga 取出 createSagaMiddleware ,還有 src/sagas/index.js 中管理訂閱事件的 rootSaga

import createSagaMiddleware from 'redux-saga';
import rootSaga from '../sagas';

之後使用 createSagaMiddleware 創建一個 Saga 的 Middleware,並放入 applyMiddleware 中給 createStore 創建 store

const sagaMiddleware = createSagaMiddleware();

const store = createStore(
  todoReducer,
  applyMiddleware(sagaMiddleware, logger),
);

最後一步就是用 sagaMiddlewarerun,來執行上方用來訂閱事件的 rootSaga

sagaMiddleware.run(rootSaga);

一切準備就緒後,回到 src/index.jsx 中建立一個 Component 為 Content,在 Component 中放上一個按鈕用 dispatch 觸發 FETCH_DATA_BEGIN,並將資料顯示在畫面上:

src/index.jsx :

然後把這個 Content 放到 Main 裡面,Render 到畫面上:

運行指令 npm run start,點擊獲得資料按鈕後,就能看見資料出現了,因為我們有設定 redux-logger,所以也能看見事件的運行順序:

本文的範例程式碼會提供在 GitHub 上,歡迎各位參考:)


結尾

看完後應該有人會想說,不就是請求 API,幹嘛搞那麼複雜?

嗯對的,但是 Redux-Saga 擁有的是他的便利和優雅,它會在底層幫你處理 Promise,直接取得你要的資料,不但減少程式的複雜度,在 coding 的時候也不會被一堆 returnpromise 搞得混亂。

而文中介紹的只是基本的使用方法,除了 callputtakeEvery 外還有像是 selecttake 都是很好用的 Effect ,官方的文檔也寫得很完整,Redux-Saga 是在 Redux 中處理非同步資料流套件的一個好選擇。

如果文章中有任何問題,或是不理解的地方,都可以留言告訴我!謝謝大家!


上一篇
Day14 | Redux 的改變,Logger 看得見
下一篇
Day16 | SPA 的換頁不是你的換頁
系列文
在 React 生態圈內打滾的一年 feat. TypeScript31

尚未有邦友留言

立即登入留言