跳到主要內容

宣告式效果

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, ...])

callapply 非常適合回傳 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 參考 中找到。