Open Xiongyibo opened 5 years ago
初级教程 - 《Redux-saga 中文文档》 - 书栈网(BookStack.CN)
Redux-saga 中文文档
首页 下载 阅读记录 书签管理
阅读 487 扫码阅读 分享给好友 更新 2018-02-17 09:56:57
本教程尝试用一种易于接受的方式(希望如此)来介绍 redux-saga。
我们将使用 Redux 仓库那个很小的计数器例子作为我们的入门教程。 这个应用比较简单,但是非常适合用来演示说明 redux-saga 的基本概念,不至于迷失在过多的细节里。
在我们开始前,需要先 clone 这个仓库:
https://github.com/yelouafi/redux-saga-beginner-tutorial
此教程最终的代码位于 sagas 分支
然后在命令行输入:
复制代码
cd redux-saga-beginner-tutorial
npm install
接着启动应用:
npm start
我们先从最简单的用例开始:2 个按钮 增加(Increment) 和 减少(Decrement) 计数。之后我们将介绍异步调用。
增加(Increment)
减少(Decrement)
不出意外的话,你应该能看到 2 个按钮 Increment 和 Decrement,以及按钮下方 Counter : 0 的文字。
Increment
Decrement
Counter : 0
如果你在运行这个应用的时候遇到问题,可随时在这个教程的仓库上创建 issue https://github.com/yelouafi/redux-saga-beginner-tutorial/issues
如果你在运行这个应用的时候遇到问题,可随时在这个教程的仓库上创建 issue
https://github.com/yelouafi/redux-saga-beginner-tutorial/issues
接下来将创建我们的第一个 Saga。按照传统,我们将编写一个 Sagas 版本的 ‘Hello, world’。
创建一个 sagas.js 的文件,然后添加以下代码片段:
sagas.js
export function* helloSaga() {
console.log('Hello Sagas!');
}
所以并没有什么吓人的东西,只是一个很普通的功能(好吧,除了 *)。这段代码的作用是打印一句问候消息到控制台。
*
为了运行我们的 Saga,我们需要:
helloSaga
我们修改一下 main.js:
main.js
// ...
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
//...
import { helloSaga } from './sagas'
const store = createStore(
reducer,
applyMiddleware(createSagaMiddleware(helloSaga))
)
// rest unchanged
首先我们引入 ./sagas 模块中的 Saga。然后使用 redux-saga 模块的 createSagaMiddleware 工厂函数来创建一个 Saga middleware。 createSagaMiddleware 接受 Sagas 列表,这些 Sagas 将会通过创建的 middleware 被立即执行。
./sagas
redux-saga
createSagaMiddleware
到目前为止,我们的 Saga 并没做什么特别的事情。它只是打印了一条消息,然后退出。
现在我们来添加一些更接近原始计数器例子的东西。为了演示异步调用,我们将添加另外一个按钮,用于点击后 1 秒增加计数。
首先,我们需要提供一个额外的回调 onIncrementAsync。
onIncrementAsync
const Counter = ({ value, onIncrement, onDecrement, onIncrementAsync }) =>
<div>
...
{' '}
<button onClick={onIncrementAsync}>Increment after 1 second</button>
<hr />
<div>Clicked: {value} times</div>
</div>
接下来我们需要使 onIncrementAsync 与 Store action 联系起来。
修改 main.js 模块:
function render() {
ReactDOM.render(
<Counter
onIncrementAsync={() => action('INCREMENT_ASYNC')}
/>,
document.getElementById('root')
注意,与 redux-thunk 不同,上面组件发起的是一个普通对象格式的 action。
现在我们将介绍另一种执行异步调用的 Saga。我们的用例如下:
在每个 INCREMENT_ASYNC action 发起后,我们需要启动一个做以下事情的任务: 等待 1 秒,然后增加计数
在每个 INCREMENT_ASYNC action 发起后,我们需要启动一个做以下事情的任务:
INCREMENT_ASYNC
添加以下代码到 sagas.js 模块:
import { takeEvery } from 'redux-saga'
import { put } from 'redux-saga/effects'
// 一个工具函数:返回一个 Promise,这个 Promise 将在 1 秒后 resolve
export const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
// Our worker Saga: 将异步执行 increment 任务
export function* incrementAsync() {
yield delay(1000)
yield put({ type: 'INCREMENT' })
// Our watcher Saga: 在每个 INCREMENT_ASYNC action 调用后,派生一个新的 incrementAsync 任务
export function* watchIncrementAsync() {
yield* takeEvery('INCREMENT_ASYNC', incrementAsync)
好吧,该解释一下了。首先我们创建一个工具函数 delay,用于返回一个延迟 1 秒再 resolve 的 Promise。 我们将使用这个函数去 阻塞 Generator。
delay
Sagas 被实现为 Generator 函数,它 yield 对象到 redux-saga middleware。 被 yield 的对象都是一类指令,指令可被 middleware 解释执行。当 middleware 取得一个 yield 后的 Promise,middleware 会暂停 Saga,直到 Promise 完成。 在上面的例子中,incrementAsync 这个 Saga 会暂停直到 delay 返回的 Promise 被 resolve,这个 Promise 将在 1 秒后 resolve。
incrementAsync
一旦 Promise 被 resolve,middleware 会恢复 Saga 去执行下一个语句(更准确地说是执行下面所有的语句,直到下一个 yield)。 在我们的情况里,下一个语句是另一个 yield 后的对象:调用 put({type: 'INCREMENT'}) 的结果。 意思是 Saga 指示 middleware 发起一个 INCREMENT 的 action。
put({type: 'INCREMENT'})
INCREMENT
put 就是我们所说的一个调用 Effect 的例子。Effect 是一些简单 Javascript 对象,对象包含了要被 middleware 执行的指令。 当 middleware 拿到一个被 Saga yield 后的 Effect,它会暂停 Saga,直到 Effect 执行完成,然后 Saga 会再次被恢复。
put
总结一下,incrementAsync Saga 通过 delay(1000) 延迟了 1 秒钟,然后发起了一个 INCREMENT 的 action。
delay(1000)
接下来,我们创建了另一个 Saga watchIncrementAsync。这个 Saga 将监听所有发起的 INCREMENT_ASYNC action,并在每次 action 被匹配时派生一个新的 incrementAsync 任务。 为了实现这个目的,我们使用一个辅助函数 takeEvery 来执行以上的处理过程。
watchIncrementAsync
takeEvery
在我们开始这个应用之前,我们需要将 watchIncrementAsync 这个 Saga 连接至 Store:
import { helloSaga, watchIncrementAsync } from './sagas'
applyMiddleware(createSagaMiddleware(helloSaga, watchIncrementAsync))
注意我们不需要连接 incrementAsync 这个 Saga,因为它会在每次 INCREMENT_ASYNC action 发起时被 watchIncrementAsync 动态启动。
我们希望测试 incrementAsync Saga,以此保证它执行期望的任务。
创建另一个文件 saga.spec.js:
saga.spec.js
import test from 'tape';
import { incrementAsync } from './sagas'
test('incrementAsync Saga test', (assert) => {
const gen = incrementAsync()
// now what ?
});
由于 incrementAsync 是一个 Generator 函数,当我们在 middleware 之外运行它,每次调用 generator 的 next,你将得到一个以下结构的对象:
next
gen.next() // => { done: boolean, value: any }
value 字段包含 yield 后的表达式,即 yield 后面那个表达式的结果。done 字段指示 generator 是结束了,还是有更多的 yield 表达式。
value
yield
done
在 incrementAsync 的例子中,generator 连续 yield 了两个值:
yield put({type: 'INCREMENT'})
所以,如果我们连续 3 次调用 generator 的 next 方法,我们会得到以下结果:
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 }
前两次调用返回了 yield 表达式的结果。第三次调用由于没有更多的 yield 了,所以 done 字段被设置为 true。 并且由于 incrementAsync Generator 未返回任何东西(没有 return 语句),所以 value 字段被设置为 undefined。
return
undefined
所以现在,为了测试 incrementAsync 里面的逻辑,我们需要对返回的 Generator 进行简单地迭代并检查 Generator yield 后的值。
import { incrementAsync } from '../src/sagas'
assert.deepEqual(
gen.next().value,
{ done: false, value: ??? },
'incrementAsync should return a Promise that will resolve after 1 second'
问题是我们如何测试 delay 的返回值?我们不能在 Promise 之间做简单的相等测试。如果 delay 返回的是一个 普通(normal) 的值, 事情将会变得很简单。
好吧,redux-saga 提供了一种方式,让上面的语句变得可能。与在 incrementAsync 中直接调用 delay(1000) 不同,我们叫它 间接(indirectly
import { put, call } from 'redux-saga/effects'
// use the call Effect
yield call(delay, 1000)
我们现在做的是 yield call(delay, 1000) 而不是 yield delay(1000),所以有何不同?
在 yield delay(1000) 的情况下,yield 后的表达式 delay(1000) 在被传递给 next 的调用者之前就被执行了(当运行我们的代码时,调用者可能是 middleware。 也有可能是运行 Generator 函数并对返回的 Generator 进行迭代的测试代码)。所以调用者得到的是一个 Promise,像在以上的测试代码里一样。
在 yield call(delay, 1000) 的情况下,yield 后的表达式 call(delay, 1000) 被传递给 next 的调用者。call 就像 put, 返回一个指示 middleware 以给定参数调用给定的函数的 Effect。
call(delay, 1000)
call
put({type: 'INCREMENT'}) // => { PUT: {type: 'INCREMENT'} }
call(delay, 1000) // => { CALL: {fn: delay, args: [1000]}}
这里发生的情况是:middleware 检查每个 yield Effect 的类型,然后决定如何实现那个 Effect。如果 Effect 类型是 PUT 那 middleware 会发起一个 action 到 Store。 如果 Effect 类型是 CALL 那么它会调用给定的函数。
PUT
CALL
这种把 Effect 创建和 Effect 执行之间分开的做法,使得我们以一种令人惊讶的简单方法去测试 Generator 成为可能。
import { incrementAsync, delay } from './sagas'
call(delay, 1000),
'incrementAsync Saga must call delay(1000)'
put({type: 'INCREMENT'}),
'incrementAsync Saga must dispatch an INCREMENT action'
gen.next(),
{ done: true, value: undefined },
'incrementAsync Saga must be done'
assert.end()
由于 put 和 call 返回文本对象,所以我们可以在测试代码中重复使用同样的函数。为了测试 incrementAsync 的逻辑, 我们可以简单地遍历 generator 并对它的值做 deepEqual 测试。
deepEqual
为了运行上面的测试代码,我们需要输入:
npm test
测试结果会显示在控制面板上。
上一篇:介绍
下一篇:Saga 的背景
自述
介绍
基本概念
高级
技巧
外部资源
问题解答
名词解释
API 参考
翻译进度
暂无相关搜索结果!
本文档使用 书栈(BookStack.CN) 构建
×
取消分享
手机扫一扫,轻松掌上读
关闭
请下载您需要的格式的文档,随时随地,享受汲取知识的乐趣!
PDF文档 EPUB文档 MOBI文档
关闭窗口
阅读进度: 0.00% ( 0/0 ) 重置阅读进度
初级教程 - 《Redux-saga 中文文档》 - 书栈网(BookStack.CN)
Redux-saga 中文文档
首页 下载 阅读记录 书签管理
初级教程
阅读 487 扫码阅读 分享给好友 更新 2018-02-17 09:56:57
初级教程
本教程的目标
本教程尝试用一种易于接受的方式(希望如此)来介绍 redux-saga。
我们将使用 Redux 仓库那个很小的计数器例子作为我们的入门教程。
这个应用比较简单,但是非常适合用来演示说明 redux-saga 的基本概念,不至于迷失在过多的细节里。
初始步骤
在我们开始前,需要先 clone 这个仓库:
https://github.com/yelouafi/redux-saga-beginner-tutorial
然后在命令行输入:
复制代码
cd redux-saga-beginner-tutorial
npm install
接着启动应用:
复制代码
npm start
我们先从最简单的用例开始:2 个按钮
增加(Increment)
和减少(Decrement)
计数。之后我们将介绍异步调用。不出意外的话,你应该能看到 2 个按钮
Increment
和Decrement
,以及按钮下方Counter : 0
的文字。你好,Sagas!
接下来将创建我们的第一个 Saga。按照传统,我们将编写一个 Sagas 版本的 ‘Hello, world’。
创建一个
sagas.js
的文件,然后添加以下代码片段:复制代码
export function* helloSaga() {
console.log('Hello Sagas!');
}
所以并没有什么吓人的东西,只是一个很普通的功能(好吧,除了
*
)。这段代码的作用是打印一句问候消息到控制台。为了运行我们的 Saga,我们需要:
helloSaga
)我们修改一下
main.js
:复制代码
// ...
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
//...
import { helloSaga } from './sagas'
const store = createStore(
reducer,
applyMiddleware(createSagaMiddleware(helloSaga))
)
// rest unchanged
首先我们引入
./sagas
模块中的 Saga。然后使用redux-saga
模块的createSagaMiddleware
工厂函数来创建一个 Saga middleware。createSagaMiddleware
接受 Sagas 列表,这些 Sagas 将会通过创建的 middleware 被立即执行。到目前为止,我们的 Saga 并没做什么特别的事情。它只是打印了一条消息,然后退出。
发起异步调用
现在我们来添加一些更接近原始计数器例子的东西。为了演示异步调用,我们将添加另外一个按钮,用于点击后 1 秒增加计数。
首先,我们需要提供一个额外的回调
onIncrementAsync
。复制代码
const Counter = ({ value, onIncrement, onDecrement, onIncrementAsync }) =>
<div>
...
{' '}
<button onClick={onIncrementAsync}>Increment after 1 second</button>
<hr />
<div>Clicked: {value} times</div>
</div>
接下来我们需要使
onIncrementAsync
与 Store action 联系起来。修改
main.js
模块:复制代码
function render() {
ReactDOM.render(
<Counter
...
onIncrementAsync={() => action('INCREMENT_ASYNC')}
/>,
document.getElementById('root')
)
}
注意,与 redux-thunk 不同,上面组件发起的是一个普通对象格式的 action。
现在我们将介绍另一种执行异步调用的 Saga。我们的用例如下:
添加以下代码到
sagas.js
模块:复制代码
import { takeEvery } from 'redux-saga'
import { put } from 'redux-saga/effects'
// 一个工具函数:返回一个 Promise,这个 Promise 将在 1 秒后 resolve
export const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
// Our worker Saga: 将异步执行 increment 任务
export function* incrementAsync() {
yield delay(1000)
yield put({ type: 'INCREMENT' })
}
// Our watcher Saga: 在每个 INCREMENT_ASYNC action 调用后,派生一个新的 incrementAsync 任务
export function* watchIncrementAsync() {
yield* takeEvery('INCREMENT_ASYNC', incrementAsync)
}
好吧,该解释一下了。首先我们创建一个工具函数
delay
,用于返回一个延迟 1 秒再 resolve 的 Promise。我们将使用这个函数去 阻塞 Generator。
Sagas 被实现为 Generator 函数,它 yield 对象到 redux-saga middleware。
被 yield 的对象都是一类指令,指令可被 middleware 解释执行。当 middleware 取得一个 yield 后的 Promise,middleware 会暂停 Saga,直到 Promise 完成。
在上面的例子中,
incrementAsync
这个 Saga 会暂停直到delay
返回的 Promise 被 resolve,这个 Promise 将在 1 秒后 resolve。一旦 Promise 被 resolve,middleware 会恢复 Saga 去执行下一个语句(更准确地说是执行下面所有的语句,直到下一个 yield)。
在我们的情况里,下一个语句是另一个 yield 后的对象:调用
put({type: 'INCREMENT'})
的结果。意思是 Saga 指示 middleware 发起一个
INCREMENT
的 action。put
就是我们所说的一个调用 Effect 的例子。Effect 是一些简单 Javascript 对象,对象包含了要被 middleware 执行的指令。当 middleware 拿到一个被 Saga yield 后的 Effect,它会暂停 Saga,直到 Effect 执行完成,然后 Saga 会再次被恢复。
总结一下,
incrementAsync
Saga 通过delay(1000)
延迟了 1 秒钟,然后发起了一个INCREMENT
的 action。接下来,我们创建了另一个 Saga
watchIncrementAsync
。这个 Saga 将监听所有发起的INCREMENT_ASYNC
action,并在每次 action 被匹配时派生一个新的incrementAsync
任务。为了实现这个目的,我们使用一个辅助函数
takeEvery
来执行以上的处理过程。在我们开始这个应用之前,我们需要将
watchIncrementAsync
这个 Saga 连接至 Store:复制代码
//...
import { helloSaga, watchIncrementAsync } from './sagas'
const store = createStore(
reducer,
applyMiddleware(createSagaMiddleware(helloSaga, watchIncrementAsync))
)
//...
注意我们不需要连接
incrementAsync
这个 Saga,因为它会在每次INCREMENT_ASYNC
action 发起时被watchIncrementAsync
动态启动。让我们的代码可测试
我们希望测试
incrementAsync
Saga,以此保证它执行期望的任务。创建另一个文件
saga.spec.js
:复制代码
import test from 'tape';
import { incrementAsync } from './sagas'
test('incrementAsync Saga test', (assert) => {
const gen = incrementAsync()
// now what ?
});
由于
incrementAsync
是一个 Generator 函数,当我们在 middleware 之外运行它,每次调用 generator 的next
,你将得到一个以下结构的对象:复制代码
gen.next() // => { done: boolean, value: any }
value
字段包含 yield 后的表达式,即yield
后面那个表达式的结果。done
字段指示 generator 是结束了,还是有更多的yield
表达式。在
incrementAsync
的例子中,generator 连续 yield 了两个值:yield delay(1000)
yield put({type: 'INCREMENT'})
所以,如果我们连续 3 次调用 generator 的 next 方法,我们会得到以下结果:
复制代码
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 }
前两次调用返回了 yield 表达式的结果。第三次调用由于没有更多的 yield 了,所以
done
字段被设置为 true。并且由于
incrementAsync
Generator 未返回任何东西(没有return
语句),所以value
字段被设置为undefined
。所以现在,为了测试
incrementAsync
里面的逻辑,我们需要对返回的 Generator 进行简单地迭代并检查 Generator yield 后的值。复制代码
import test from 'tape';
import { incrementAsync } from '../src/sagas'
test('incrementAsync Saga test', (assert) => {
const gen = incrementAsync()
assert.deepEqual(
gen.next().value,
{ done: false, value: ??? },
'incrementAsync should return a Promise that will resolve after 1 second'
)
});
问题是我们如何测试
delay
的返回值?我们不能在 Promise 之间做简单的相等测试。如果delay
返回的是一个 普通(normal) 的值,事情将会变得很简单。
好吧,
redux-saga
提供了一种方式,让上面的语句变得可能。与在incrementAsync
中直接调用delay(1000)
不同,我们叫它 间接(indirectly复制代码
//...
import { put, call } from 'redux-saga/effects'
export const delay = ms => new Promise(resolve => setTimeout(resolve, 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)
的情况下,yield 后的表达式delay(1000)
在被传递给next
的调用者之前就被执行了(当运行我们的代码时,调用者可能是 middleware。也有可能是运行 Generator 函数并对返回的 Generator 进行迭代的测试代码)。所以调用者得到的是一个 Promise,像在以上的测试代码里一样。
在
yield call(delay, 1000)
的情况下,yield 后的表达式call(delay, 1000)
被传递给next
的调用者。call
就像put
,返回一个指示 middleware 以给定参数调用给定的函数的 Effect。
复制代码
put({type: 'INCREMENT'}) // => { PUT: {type: 'INCREMENT'} }
call(delay, 1000) // => { CALL: {fn: delay, args: [1000]}}
这里发生的情况是:middleware 检查每个 yield Effect 的类型,然后决定如何实现那个 Effect。如果 Effect 类型是
PUT
那 middleware 会发起一个 action 到 Store。如果 Effect 类型是
CALL
那么它会调用给定的函数。这种把 Effect 创建和 Effect 执行之间分开的做法,使得我们以一种令人惊讶的简单方法去测试 Generator 成为可能。
复制代码
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
返回文本对象,所以我们可以在测试代码中重复使用同样的函数。为了测试incrementAsync
的逻辑,我们可以简单地遍历 generator 并对它的值做
deepEqual
测试。为了运行上面的测试代码,我们需要输入:
复制代码
npm test
测试结果会显示在控制面板上。
上一篇:介绍
下一篇:Saga 的背景
自述
介绍
基本概念
高级
技巧
外部资源
问题解答
名词解释
API 参考
翻译进度
暂无相关搜索结果!
本文档使用 书栈(BookStack.CN) 构建
×
分享,让知识传承更久远
取消分享
×
文章二维码
手机扫一扫,轻松掌上读
关闭
×
文档下载
请下载您需要的格式的文档,随时随地,享受汲取知识的乐趣!
PDF文档 EPUB文档 MOBI文档
关闭窗口
×
书签列表
关闭
×
阅读记录
阅读进度: 0.00% ( 0/0 ) 重置阅读进度
关闭