宣告式效果
在 redux-saga
中,Saga 使用 Generator 函數實作。若要表達 Saga 的邏輯,我們可以從 Generator 產生純 JavaScript 物件。我們會將這些物件稱為 效果。效果是指包含部分資訊的物件供中間件詮釋。可以將效果視為中間件執行的指令(例如:呼叫部分非同步函數、派送動作到 store 等)。
要建立效果,請使用程式庫提供的函數,函數包含在 redux-saga/effects
套件中。
我們會在這部分和後面的部分介紹一些基本的效果。並說明如何使用這個概念輕鬆測試 Saga。
Saga 可以用多種形式產生效果。最簡單的方法是產生 Promise。
舉例來說,假設我們有一個 Saga 監控 PRODUCTS_REQUESTED
動作。針對每個符合條件的動作,它會開始一個任務來從伺服器擷取產品清單。
import { takeEvery } from 'redux-saga/effects'
import Api from './path/to/api'
function* watchFetchProducts() {
yield takeEvery('PRODUCTS_REQUESTED', fetchProducts)
}
function* fetchProducts() {
const products = yield Api.fetch('/products')
console.log(products)
}
在以上的範例中,我們在 Generator 中直接呼叫 Api.fetch
(在 Generator 函式中,位於 yield
右邊的所有表達式都會被評量,然後將結果傳遞給呼叫者)。
Api.fetch('/products')
觸發 AJAX 要求,然後傳回一個 Promise,它會使用已解析的回應來解析,AJAX 要求會立刻執行。方法簡單又直觀,但...
假設我們想要測試上面的 generator。
const iterator = fetchProducts()
assert.deepEqual(iterator.next().value, ??) // what do we expect ?
我們想要檢查 generator 傳遞的第一個值。在我們的案例中,這是執行 Api.fetch('/products')
的結果,它是一個 Promise。在測試期間,執行真正的服務既不可行又 impractical,所以我們必須「模擬」`Api.fetch` 函式,也就是說,我們必須將真正的函式取代為一個假的函式,它並不會實際執行 AJAX 要求,而只會檢查我們是否呼叫了 Api.fetch
並使用正確的參數(在我們的案例中為 '/products'
)。
模擬會讓測試更困難,而且可靠性更低。另一方面,傳回值的函式比較容易測試,因為我們可以使用簡單的 equal()
來檢查結果。這是編寫最可靠單元測試的方法。
還沒說服你嗎?我建議你閱讀 Eric Elliott 的文章
(...)
equal()
本質上會回答每一個單元測試都必須回答,但大多數都不會回答的兩個最重要的問題
- 真實的輸出是什麼?
- 預期的輸出是什麼?
如果你完成一個測試,卻沒有回答這兩個問題,那你就沒有進行真正的單元測試,你做的只是一個粗糙、半生不熟的測試。
我們實際上必須做的事情是確保 fetchProducts
任務使用正確的函式和正確的參數來傳遞呼叫。
與其在 Generator 中直接呼叫非同步函式,我們可以只傳遞函式呼叫的描述,也就是說,我們會傳遞一個看起來像這樣的物件
// Effect -> call the function Api.fetch with `./products` as argument
{
CALL: {
fn: Api.fetch,
args: ['./products']
}
}
換言之,Generator 會傳遞包含指示的純粹物件,而 redux-saga
中介軟體會負責執行這些指示,並將它們執行的結果傳遞給 Generator。這樣一來,在測試 Generator 時,我們只需使用已傳遞的物件來執行簡單的 deepEqual
,檢查它是否傳遞預期的指示即可。
因為這個原因,函式庫提供了執行非同步呼叫的不同方式。
import { call } from 'redux-saga/effects'
function* fetchProducts() {
const products = yield call(Api.fetch, '/products')
// ...
}
我們現在使用的是 call(fn, ...args)
函式。與前一個範例的不同處在於,我們現在並沒有立刻執行取得的要求,而 call
會建立 effect 的描述。就像你使用動作建立器在 Redux 中建立一個純粹的動作物件,讓 Store 執行,call
會建立一個純粹的物件,描述函式呼叫。redux-saga 中介軟體負責執行函式呼叫,然後使用已解析的回應回復 generator。
這讓我們可以輕鬆在 Redux 環境外測試 Generator。因為 call
只是一個回傳純粹物件的函式。
import { call } from 'redux-saga/effects'
import Api from '...'
const iterator = fetchProducts()
// expects a call instruction
assert.deepEqual(
iterator.next().value,
call(Api.fetch, '/products'),
"fetchProducts should yield an Effect call(Api.fetch, './products')"
)
現在我們不用模擬任何東西,只要做簡單的 equality 測試即可。
這些宣告式呼叫的優點是,我們可以透過重複運算 Generator 並對循序產生的值進行 deepEqual
測試,來測試 Saga 中的所有邏輯。這是一個真正的優點,因為您的複雜非同步操作不再是黑盒子,而且無論邏輯有多複雜,您都可以詳細測試它們的操作邏輯。
call
還支援呼叫物件方法,可以使用下列形式提供 this
內容給呼叫的函式
yield call([obj, obj.method], arg1, arg2, ...) // as if we did obj.method(arg1, arg2 ...)
apply
是方法呼叫形式的別名
yield apply(obj, obj.method, [arg1, arg2, ...])
call
和 apply
非常適合回傳 Promise 結果的函式。另一個函式 cps
可用於處理 Node 型式函式(例如 fn(...args, callback)
,其中 callback
的形式為 (error, result) => ()
)。cps
代表 Continuation Passing Style。
例如
import { cps } from 'redux-saga/effects'
const content = yield cps(readFile, '/path/to/file')
當然,您可以像測試 call
一樣測試它
import { cps } from 'redux-saga/effects'
const iterator = fetchSaga()
assert.deepEqual(iterator.next().value, cps(readFile, '/path/to/file') )
cps
也支援與 call
相同的方法呼叫形式。
宣告式效果的完整清單可以在 API 參考 中找到。