声明式效果
在 redux-saga
中,Sagas 是使用生成器函数实现的。为了表达 Saga 逻辑,我们从生成器中产生纯 JavaScript 对象。我们称这些对象为效果。效果是一个包含一些由中间件解释的信息的对象。你可以将效果视为对中间件执行某些操作(例如,调用某个异步函数,向存储库派发一个动作等)的指令。
要创建效果,你需要使用 redux-saga/effects
包提供的函数。
在本节和以下部分,我们将介绍一些基本的效果。并看看这个概念如何使 Sagas 易于测试。
Sagas 可以以多种形式产生效果。最简单的方式是产生一个 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)
}
在上面的例子中,我们直接从生成器内部调用 Api.fetch
(在生成器函数中,yield
右边的任何表达式都会被评估,然后结果会被产生给调用者)。
Api.fetch('/products')
触发一 个 AJAX 请求,并返回一个将解析为解析响应的 Promise,AJAX 请求将立即执行。简单且符合习惯,但是...
假设我们想要测试上面的生成器:
const iterator = fetchProducts()
assert.deepEqual(iterator.next().value, ??) // 我们期望什么?
我们想要检查生成器产生的第一个值的结果。在我们的情况下,它是运行 Api.fetch('/products')
的结果,这是一个 Promise。在测试期间执行真实的服务既不可行也不实用,所以我们必须模拟 Api.fetch
函数,即我们必须用一个假的函数替换真实的函数,这个假的函数实际上并不运行 AJAX 请求,而只是检查我们是否用正确的参数(在我们的情况下是 '/products'
)调用了 Api.fetch
。
模拟使测试更加困难和不可靠。另一方面,返回值的函数更容易测试,因为我们可以使用一个简单的 equal()
来检查结果。这是编写最可靠测试的方式。
还不信服?我鼓励你阅读 Eric Elliott 的文章:
(...)
equal()
,本质上回答了每个单元测试必须回答的两个最重要的问题, 但大多数没有:
- 实际的输出是什么?
- 预期的输出是什么?
如果你在没有回答这两个问题的情况下完成了一个测试,那么你并没有一个真正的单元测试。你有的只是一个马虎、半成品的测试。
我们实际需要做的是确保 fetchProducts
任务产生了正确的函数和正确的参数的调用。
与其直接从生成器内部调用异步函数,我们可以只产生函数调用的描述。也就是说,我们将产生一个看起来像这样的对象:
// 效果 -> 用 `./products` 作为参数调用函数 Api.fetch
{
CALL: {
fn: Api.fetch,
args: ['./products']
}
}
换句话说,生成器将产生包含指令的纯对象,redux-saga
中间件将负责执行这些指令,并将它们的执行结果返回给生成器。这样,当测试生成器时,我们只需要检查它是否产生了预期的指令,通过对产生的对象进行简单的 deepEqual
。
出于这个原因,库提供了 一种不同的方式来执行异步调用。
import { call } from 'redux-saga/effects'
function* fetchProducts() {
const products = yield call(Api.fetch, '/products')
// ...
}
我们现在使用的是 call(fn, ...args)
函数。与前面的例子的区别在于,现在我们并没有立即执行 fetch 调用,相反,call
创建了一个效果的描述。就像在 Redux 中你使用动作创建器来创建一个描述将由 Store 执行的动作的纯对象一样,call
创建了一个描述函数调用的纯对象。redux-saga 中间件负责执行函数调用,并将解析的响应恢复到生成器。
这使我们可以轻松地在 Redux 环境之外测试生成器。因为 call
只是一个返回纯对象的函数。
import { call } from 'redux-saga/effects'
import Api from '...'
const iterator = fetchProducts()
// 期望一个调用指令
assert.deepEqual(
iterator.next().value,
call(Api.fetch, '/products'),
"fetchProducts 应该产生一个 Effect call(Api.fetch, './products')"
)
现在我们不需要模拟任何东西,一个基本的等式测试就足够了。
这些声明式调用的优点是,我们可以通过迭代生成器并对连续产生的值进行 deepEqual
测试,来测试 Saga 中的所有逻辑。这是一个真正的好处,因为你的复杂异步操作不再是黑箱,无论它们有多复杂,你都可以详细地测试它们的操作逻辑。
call
还支持调用对象方法,你可以使用以下形式为被调用的函数提供一个 this
上下文:
yield call([obj, obj.method], arg1, arg2, ...) // 就像我们做的 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 参考 中找到。