初學者教程
本教程的目的
本教程試圖用一種(希望能)容易理解的方式介紹 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)
}
是時候做一些說明了。
我們建立一個會傳回Promise的delay
函式,這個 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 helloSaga
和 watchIncrementAsync
的結果。這表示兩個產生 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 個值
yield delay(1000)
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
這應該會在控制台中呈報結果。