跳到主要内容

初學者教程

本教程的目的

本教程試圖用一種(希望能)容易理解的方式介紹 redux-saga。

對於我們的入門教程,我們將使用 Redux 回購中的簡單計數器範例。此應用程式非常基本,但非常適合說明 redux-saga 的基本概念,而不會迷失在過多的細節中。

初期設定

開始之前,複製教程儲存庫

本教程的最終程式碼位於 sagas 分支中。

然後在命令列中執行

$ cd redux-saga-beginner-tutorial
$ npm install

若要啟動應用程式,請執行

$ npm start

編譯完成後,在瀏覽器中開啟 https://127.0.0.1:9966

我們從最基本的用例開始:2 個按鈕用於增加減少計數器。稍後,我們將介紹異步呼叫。

如果一切順利,你應該看到 2 個按鈕增加減少,以及下面顯示計數:0的訊息。

如果您在執行應用程式時遇到問題。歡迎在教學課程倉庫中建立問題。

你好,Sagas!

我們要建立第一個 Saga。秉持傳統,我們將撰寫 Sagas 的「Hello, world」版本。

建立一個檔案sagas.js,然後加入下列程式碼片段

export function* helloSaga() {
console.log('Hello Sagas!')
}

因此,沒有什麼好恐懼的,只是一個正常的功能(*除外)。它只會將一個問候訊息列印到主控台中。

為了執行我們的 Saga,我們需要

  • 建立一個 Saga 中介軟體,其中包含要執行的 Saga 清單(到目前為止,我們只有一個 helloSaga
  • 將 Saga 中介軟體連接到 Redux 儲存

我們將對main.js進行變更

// ...
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

// ...
import { helloSaga } from './sagas'

const sagaMiddleware = createSagaMiddleware()
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(helloSaga)

const action = type => store.dispatch({type})

// rest unchanged

首先,我們從./sagas模組匯入我們的 Saga。然後,我們使用redux-saga函式庫匯出的工廠函式createSagaMiddleware建立一個中介軟體。

在執行我們的helloSaga之前,我們必須使用applyMiddleware將我們的 中介軟體連接到 Store。然後,我們可以使用sagaMiddleware.run(helloSaga)來啟動我們的 Saga。

到目前為止,我們的 Saga 沒有做任何特別的事情。它只是記錄一則訊息,然後結束。

建立異步呼叫

現在,讓我們加入一些更接近原始計數器示範檔的東西。為了說明異步呼叫,我們將加入另一個按鈕,以便在按一下後 1 秒增加計數器。

首先,我們要為 UI 元件提供一個額外的按鈕和一個回呼onIncrementAsync

我們將變更Counter.js

const Counter = ({ value, onIncrement, onDecrement, onIncrementAsync }) =>
<div>
<button onClick={onIncrementAsync}>
Increment after 1 second
</button>
{' '}
<button onClick={onIncrement}>
Increment
</button>
{' '}
<button onClick={onDecrement}>
Decrement
</button>
<hr />
<div>
Clicked: {value} times
</div>
</div>

接著,我們應該將元件的onIncrementAsync連接到 Store 的動作。

我們將修改main.js模組,如下所示

function render() {
ReactDOM.render(
<Counter
value={store.getState()}
onIncrement={() => action('INCREMENT')}
onDecrement={() => action('DECREMENT')}
onIncrementAsync={() => action('INCREMENT_ASYNC')} />,
document.getElementById('root')
)
}

請注意,與 redux-thunk 不同,我們的元件會發送一個純粹的動作物件。

現在,我們將介紹另一個 Saga 來執行異步呼叫。我們的用例如下

在每個INCREMENT_ASYNC動作中,我們要啟動一項工作,它將執行下列動作

  • 等候 1 秒,然後增加計數器

將下列程式碼加入sagas.js模組

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

const delay = (ms) => new Promise(res => setTimeout(res, ms))

// ...

// Our worker Saga: will perform the async increment task
export function* incrementAsync() {
yield delay(1000)
yield put({ type: 'INCREMENT' })
}

// Our watcher Saga: spawn a new incrementAsync task on each INCREMENT_ASYNC
export function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}

是時候做一些說明了。

我們建立一個會傳回Promisedelay函式,這個 Promise 會在特定毫秒數後解析。我們將使用這個函式來封鎖生成器。

Sagas 實作為 產生器函數,對 redux-saga 中介軟體產生物件。產生的物件是一種由中介軟體詮釋的指令。當 Promise 產生的中介軟體時,中介軟體會暫停 Saga,直到 Promise 完成。在以上的範例中,incrementAsync Saga 會暫停,直到由 delay 回傳的 Promise 解析為止,而這會在 1 秒後發生。

一旦 Promise 解析,中介軟體會繼續執行 Saga,執行代碼,直到下一個產生。在這個範例中,下一個陳述式是另一個產生的物件:呼叫 put({type: 'INCREMENT'}) 的結果,它指示中介軟體分派一項 INCREMENT 動作。

put 是我們稱之為效果的一個範例。效果是包含中介軟體要執行的指示的純 JavaScript 物件。當中介軟體擷取 Saga 產生的效果時,Saga 會暫停,直到效果履行為止。

因此,總而言之,incrementAsync Saga 透過呼叫 delay(1000) 睡眠 1 秒,然後分派 INCREMENT 動作。

接著,我們產生另一個 Saga watchIncrementAsync。我們使用 redux-saga 提供的輔助函數 takeEvery,來偵聽分派的 INCREMENT_ASYNC 動作,並每次執行 incrementAsync

現在我們有 2 個 Saga,而且需要同一時間啟動它們。為了做到這一點,我們將新增一個 rootSaga,負責啟動其他 Saga。在同一檔案 sagas.js 中,重新編排檔案,如下所示

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

export const delay = (ms) => new Promise(res => setTimeout(res, ms))

export function* helloSaga() {
console.log('Hello Sagas!')
}

export function* incrementAsync() {
yield delay(1000)
yield put({ type: 'INCREMENT' })
}

export function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}

// notice how we now only export the rootSaga
// single entry point to start all Sagas at once
export default function* rootSaga() {
yield all([
helloSaga(),
watchIncrementAsync()
])
}

這個 Saga 產生一個陣列,其中包含呼叫我們的兩個 saga helloSagawatchIncrementAsync 的結果。這表示兩個產生 Generators 會同時啟動。現在我們只需要在 main.js 中對 root Saga 呼叫 sagaMiddleware.run

// ...
import rootSaga from './sagas'

const sagaMiddleware = createSagaMiddleware()
const store = ...
sagaMiddleware.run(rootSaga)

// ...

讓我們的程式可測試

我們想要測試我們的 incrementAsync Saga,以確定它執行所需的任務。

產生另一個檔案 sagas.spec.js

import test from 'tape'

import { incrementAsync } from './sagas'

test('incrementAsync Saga test', (assert) => {
const gen = incrementAsync()

// now what ?
})

incrementAsync 是產生器函數。執行時,它會傳回一個反覆運算子物件,而反覆運算子的 next 方法會傳回一個具有以下形態的物件

gen.next() // => { done: boolean, value: any }

value 欄位包含產生的表示式,也就是 yield 之後表示式的結果。done 欄位指出產生器是否已終止,或者是否有更多「yield」表示式。

incrementAsync 的狀況下,產生器會連續產生 2 個值

  1. yield delay(1000)
  2. yield put({type: 'INCREMENT'})

因此,如果我們連續 3 次呼叫產生器的下一個方法,就會得到以下結果

gen.next() // => { done: false, value: <result of calling delay(1000)> }
gen.next() // => { done: false, value: <result of calling put({type: 'INCREMENT'})> }
gen.next() // => { done: true, value: undefined }

最初 2 次呼叫會傳回 yield 表達式的結果。在第 3 次呼叫中,由於沒有更多 yield,

done

欄位會設定為 true。由於

incrementAsync

產生器不會傳回任何內容(沒有

return

陳述式),因此

value

欄位會設定為

undefined

因此,我們現在必須反覆運算傳回的產生器並檢查產生器產生的值,才能測試

incrementAsync

內部的邏輯。

import test from 'tape'

import { incrementAsync } from './sagas'

test('incrementAsync Saga test', (assert) => {
const gen = incrementAsync()

assert.deepEqual(
gen.next(),
{ done: false, value: ??? },
'incrementAsync should return a Promise that will resolve after 1 second'
)
})

問題是我們該如何測試

delay

的傳回值?我們無法對 Promise 執行簡單的等號測試。如果

delay

傳回*正常*的值,測試應該會更容易。

嗯,

redux-saga

提供一個方法,可以實現上述陳述式。我們會在

incrementAsync

內部間接呼叫

delay(1000)

,而非直接呼叫,並將其匯出以執行後續的深層比較

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

export const delay = (ms) => new Promise(res => setTimeout(res, ms))

// ...

export function* incrementAsync() {
// use the call Effect
yield call(delay, 1000)
yield put({ type: 'INCREMENT' })
}

我們現在執行

yield call(delay, 1000)

,而非執行

yield delay(1000)

。兩者有什麼不同?

在第一個案例中,yield 表達式

delay(1000)

在傳遞給

next

的呼叫者之前會進行評估(呼叫者可能是執行我們程式碼時的 middleware。它也可能是反覆運算傳回的產生器並執行產生器函數的測試程式碼)。因此,呼叫者取得的是 Promise,就像上述的測試程式碼。

在第二個案例中,yield 表達式

call(delay, 1000)

就是傳遞給

next

的呼叫者。就像

put

call

會傳回一個 Effect,用來指示 middleware 呼叫特定函數並帶有指定的引數。事實上,

put

call

都不會自行執行任何 dispatch 或非同步呼叫,它們會傳回純 JavaScript 物件。

put({type: 'INCREMENT'}) // => { PUT: {type: 'INCREMENT'} }
call(delay, 1000) // => { CALL: {fn: delay, args: [1000]}}

middleware 會檢查每個產生的 Effect 類型,然後決定如何達成該 Effect。如果 Effect 類型是

PUT

,它會向 Store dispatch 一個動作。如果 Effect 是

CALL

,它會呼叫指定的函數。

這種 Effect 建立和 Effect 執行之間的分離,讓我們可以輕鬆地測試我們的產生器

import test from 'tape'

import { put, call } from 'redux-saga/effects'
import { incrementAsync, delay } from './sagas'

test('incrementAsync Saga test', (assert) => {
const gen = incrementAsync()

assert.deepEqual(
gen.next().value,
call(delay, 1000),
'incrementAsync Saga must call delay(1000)'
)

assert.deepEqual(
gen.next().value,
put({type: 'INCREMENT'}),
'incrementAsync Saga must dispatch an INCREMENT action'
)

assert.deepEqual(
gen.next(),
{ done: true, value: undefined },
'incrementAsync Saga must be done'
)

assert.end()
})

由於

put

call

會傳回純物件,因此我們可以在測試程式碼中重複使用同樣函數。我們會反覆運算產生器並對其值執行

deepEqual

測試,以測試

incrementAsync

的邏輯。

為了執行上述測試,請執行

$ npm test

這應該會在控制台中呈報結果。