Redux-saga实践总结及其高级功能在DvaJS中的应用

选择 dvajs

最近一直忙于公司产品的前端重构,因为之前学习和接触的东西一直以后端为主,使用的语言又是以 PHP 为主,所以对于前端开发使用的语言 ES6 的代码规范和编程风格不太熟悉,对于前端工程化的经验也是几乎为零。虽然 leader 给了充足的学习时间和强大的技术支持,但写代码的就我自己。于是乎我就选择了 antd+dvajs,一来框架作者提供的 Demo 可以让我去学习前端的设计思想和代码的编写规范,二来我不需要耗费过多的精力去搭建整个项目的生态。站在巨人的肩上,才能更好更快地拥抱 react/redux 全家桶。

redux-saga

什么是 saga

Saga这个名词常被用在CQRS的讨论中,它是指一段在限定上下文(bounded contexts)和聚合(aggregates)之间起协作和路由(coordinates and routes)消息作用的代码。最早被定义在Hector Garcia-Molina和Kenneth Salem的论文”Sagas“中。这篇论文提出了一个saga机制来作为分布式事务的替代品以解决长时间运行的分布式事务(long-running process)的问题。这篇论文认为业务过程经常由很多步骤组成,每个步骤都涉及一个事务,如果将这些事务组成一个分布式事务,就可以实现总体一致(overall consistency)。

redux-saga 中几个抽象的概念:

Effect

Effect 是一个 javascript 对象,通过使用 redux-saga/effects 包里提供的函数来创建,里面包含描述异步操作(Side Effect)的信息,通过 yield 传达给 sagaMiddleware 执行, 我们可以把每一个 Effect 看成一道发送给 middleware 的指令。

Saga

所有的 Saga 都是由 Generator 函数实现,所做的实际上是通过组合 Effect,以实现所需的控制流。最简单的例子就是把 yield Effects 一个接一个地放置,就可以让 Effect 按顺序执行。

Task

一个 task 就像是一个在后台运行的进程。在基于 redux-saga 的应用程序中,可以同时运行多个 task。通过 fork 函数来创建 task,同时可以使用 cancel 函数来结束 task。

原生 redux-saga 的使用

鉴于文档中有详细的解释,这里只做介绍,查看文档建议阅读官方英文文档,中文文档一是没有及时更新,导致一些内容在中文文档里没有,另外翻译的水平也不太行,有些地方甚至出现错误的翻译。

Effect 创建器

put

作用和 redux 中的 dispatch 相同。

yield put({ type: 'CLICK_BTN' });

select

作用和 redux thunk 中的 getState 相同。

const id = yield select(state => state.id);

  • Ps:saga 最好是自主独立的,不应依赖 Store 的 state。这使得很容易修改 state 实现部分而不影响 Saga 代码。
take

监听(pulling)一个能匹配指定 pattern 的 action。

1
2
3
4
while (true) {
yield take('CLICK_BUTTON');
yield put({type: 'SHOW_CONGRATULATION'})
}

一个简单的日志记录器的例子:

1
2
3
4
5
6
7
8
9
10
11
import { select, take } from 'redux-saga/effects'

function* watchAndLog() {
while (true) {
const action = yield take('*')
const state = yield select()

console.log('action', action)
console.log('state after', state)
}
}
call

阻塞地调用 saga 或者返回 promise 的函数。

yield call(method, arg1, arg2, ...);

同样支持调用对象方法,可以用下面这种方式,为调用的函数提供一个 this 上下文:

yield call([obj, obj.method], arg1, arg2, ...) // 如同 obj.method(arg1, arg2 ...)

fork

无阻塞地调用 saga 或者返回 promise 的函数, 返回一个 Task 对象。

yield fork(authorize, username, password)

cancel

取消之前 fork 的 task。

下面的例子实现了在登陆过程中,一旦收到LOGOUT ,马上终止authorize ,然后在任务内部捕获取消错误,并执行内部的清理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { isCancelError } from 'redux-saga'
import { take, put, call, fork, cancel } from 'redux-saga/effects'
import Api from '...'

function* authorize(user, password) {
try {
const token = yield call(Api.authorize, user, password)
yield put({type: 'LOGIN_SUCCESS', token})
return token
} catch(error) {
if(!isCancelError(error))
yield put({type: 'LOGIN_ERROR', error})
}
}

function* loginFlow() {
while(true) {
const {user, password} = yield take('LOGIN_REQUEST')
// fork return a Task object
const task = yield fork(authorize, user, password)
const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
if(action.type === 'LOGOUT')
yield cancel(task)
yield call(Api.clearItem('token'))
}
}
  • Ps-1:在取消任务中出现嵌套的 saga 时,所有的 saga 都会被取消,即取消会向下传播,但未被捕获的错误不会向上冒泡。

  • Ps-2:取消任务时,saga 中的try...finally 代码块,finally 里的代码会照常执行。

  • Ps-3:yield cancel(task)是非阻塞的,因此任务一旦取消,就应当尽快完成其清理逻辑。

Effect 辅助函数

takeEvery

监听(pulling)每个能匹配指定 pattern 的 action。

1
2
3
4
5
import { takeEvery } from 'redux-saga'

function* watchFetchData() {
yield* takeEvery('FETCH_REQUESTED', fetchData)
}
takeLatest

监听(pulling)最后一个能匹配指定 pattern 的 action,之前启动的任务如果正在执行中,则会被自动取消。

1
2
3
4
5
import { takeLatest } from 'redux-saga'

function* watchFetchData() {
yield* takeLatest('FETCH_REQUESTED', fetchData)
}
throttle

节流器。监听到一个匹配的 action 后,忽略一段时间内匹配到的其他 action,但是保留这段时间中最后一个匹配到的 action。
yield throttle(1000, 'FETCH_AUTOCOMPLETE', fetchAutocomplete)

Effect 组合器(combinators)

race

在多个 Effect 之间执行一个 race(类似 Promise.race([...]) 的行为)。

下面的示例演示了触发一个远程的获取请求,并且限制了 1 秒内响应,否则作超时处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { race, take, put } from 'redux-saga/effects'
import { delay } from 'redux-saga'

function* fetchPostsWithTimeout() {
const {posts, timeout} = yield race({
posts: call(fetchApi, '/posts'),
timeout: call(delay, 1000)
})

if (posts)
put({type: 'POSTS_RECEIVED', posts})
else
put({type: 'TIMEOUT_ERROR'})
}
[...effects]

并行执行多个 Effect,然后等待所有 Effect 完成(类似 Promise.all([...]) 的行为)。

下面的实例并行执行了 2 个阻塞调用:

1
2
3
4
5
6
7
8
import { fetchCustomers, fetchProducts } from '...'

function* mySaga() {
const [customers, products] = yield [
call(fetchCustomers),
call(fetchProducts)
]
}

redux-saga 高级功能在 dvajs 中的应用

在 dvajs 中,作者选择了 redux-saga 处理异步操作。而对于异步处理的问题,目前比较流行的通用解决方案如下:

前两个方案一个是支持函数形式的action,另一个是支持 promise 形式的 action,但是有一个共同的问题就是:都改变了action的含义,使得action的含义不那么纯粹了。

redux-saga则以优雅而强大的方式,把业务逻辑都放在saga中,通过监听 action 来执行有副作用的 task,并且引入了 Sagas 的机制和 generator 的特性,让 reducer, action 和 component 都很纯粹地干他们原本需要干的事情。

但是,在目前的 dvajs 中的 effects 默认是 takeEvery dva@1.2.1),虽然文档中没有关于其他类型 effects 的说明,但在 源码 中我们可以看到,作者其实是提供了使用方法的。

通过下面的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const watcher = fn => [fn, { type: 'watcher' }];
const takeLatest = fn => [fn, { type: 'takeLatest' }];
const throttle = fn => [fn, { type: 'throttle' }];

app.model({
effects: {
addRemote: watcher(function*(action, { take, put, call }) {
while (true) {
const { payload } = yield take('add');
yield call(delay, 100);
yield put({ type: 'add', payload });
}
}
}
});

就可以使用所有原生 redux-saga 中的控制方式了。