“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情”
上一节我们学习了redux在实际项目的应用细节,这一节我们来学习redux中一个很重要的概念:中间件。我们会简单实现一个记录的中间件,
然后学习redux-saga这个异步请求中间件。
redux中的Middleware
redux中的中间件提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。
你可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。
记录日志
试想一下,如果我们的redux在每一次dispatch的时候都可以记录下此次发生的action以及dispatch结束后的store。那么在我们的应用
出现问题的时候,我们就可以轻松的查阅日志找出是哪个action导致了state不正确。那么我们怎样通过redux实现它呢?
手动记录
最直接的解决方案就是在每次调用 store.dispatch(action)
前后手动记录被发起的 action 和新的 state。假如你在创建一个action时这样调用:
store.dispatch(addTodo('use Redux'))
为了记录这个 action 以及产生的新的 state,你可以通过这种方式记录日志:
let action = addTodo('Use Redux')
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
那么很自然的就能想到可以封装为一个函数在各处调用:
function dispatchAndLog(store, action) {
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
}
dispatchAndLog(store, addTodo('Use Redux'))
但是这样我们还是需要每次导入一个外部方法,那么如果我们直接去替换store实例中的dispatch函数呢?
let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
其实到这里我们想要实现的功能已经完成了,但是距离Middleware实际使用的方法还是有不小的差距,
同时我们这里只能对dispatch的扩展时十分有限的,如果我想对其添加其他的功能,又该怎么实现呢?
首先可以确定的是我们需要将每一个功能分离开来,我们希望的时一个功能对应一个模块,那么当我们想添加其他的模块时,应该是这样的:
function patchStoreToAddLogging(store) {
let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
// 崩溃报告模块
function patchStoreToAddCrashReporting(store) {
let next = store.dispatch
store.dispatch = function dispatchAndReportErrors(action) {
try {
return next(action)
} catch (err) {
console.error('捕获一个异常!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}
}
然后我们可以在store中使用它们:
patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)
那么有没有一种更好的代码组织方式呢?此前,我们使用dispatchAndLog
替换了dispatch
, 如果我们不这样做,而是在函数中返回新的dispatch
呢?
function logger(store) {
let next = store.dispatch
// 我们之前的做法:
// store.dispatch = function dispatchAndLog(action) {
return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
然后我们在外部提供方法将它替换到store.dispatch
中。
function applyMiddlewareByMonkeypatching(store, middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()
// 在每一个 middleware 中变换 dispatch 方法。
middlewares.forEach(middleware =>
store.dispatch = middleware(store)
)
}
其实到了这里我们的中间件功能已经大体实现,如果想后续继续深入请参考redux官方文档
redux-saga
接下来我们来看管理应用程序副作用的中间件reudx-saga。他在redux中有很多使用场景,但是我们使用最多的还是用它来进行网络请求。
redux-saga使用了ES6的Generator功能,让异步的流程更易于读取,写入和测试。因此我们首先了解一下generator函数是什么?
Generator函数
形式上,Generator函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态.
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
const hw = helloWorldGenerator();
Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。
下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。
换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
实际使用场景
现在来看一个项目中的实际使用
//查询搜索列表
const requestLists = function*({ page, keyword, callback }) {
try {
appLoading(); // 展现加载框
const body = {
keyword: keyword,
page: page,
size: size,
};
const result = yield handleData.get(DataUrls.searchLists, body) // 发送网络请求
if (result && result.success) {
yield put(Action.fetchSearchListDone(lists)); // 请求成功保存数据
callback && callback();
} else {
showModal((result && result.message) || '系统繁忙,请稍后');
yield put(Action.fetchSearchListFailure(result)); //请求失败错误处理
}
} catch (err) {
yield put(Action.fetchSearchListFailure(err)); //错误处理
showModal('系统繁忙,请稍后');
} finally {
appFinish(); //关闭加载框
}
};
上面是一个比较完整的请求处理过程,从发送请求到成功或失败处理都有包含到。
对上面的代码做一个解释,在这个函数中我们首先使用yield发起一个异步请求,这时middleware 会暂停 Saga,直到请求完成。
一旦完成后,不管是成功或者失败,middleware 会恢复 Saga 接着执行,直到遇到下一个 yield。当 try 报错时, 会执行到catch去捕获异常,
在这里遇到下一个yield,调用请求失败的Action,传入失败原因。请求成功时遇到下一个 yield 对象,调用请求成功的Action,传入结果。
put 就是我们称作副作用的一个例子。副作用是一些简单 Javascript 对象,包含了要被 middleware 执行的指令。
当 middleware 拿到一个被 Saga yield 的副作用,它会暂停 Saga,直到副作用执行完成,然后 Saga 会再次被恢复。
接下来我们需要去启动这个saga,为了做到这一点,我们将添加一个 listSaga,负责启动其他的 Sagas。在同一个文件中:
const listSagas = function* listSagas() {
yield all([
takeEvery('LIST_REQUESTLIST', requestLists),
]);
};
export default listSagas;
其中的辅助函数takeEvery
用于监听所有的LIST_REQUESTLIST
action,在action执行的时候去启动相应的requestLists
任务。
定义一个listSagas
的原因就是我们这个文件中可能远不至这一个副作用函数,当定义了多个的时候,我们可以在all中添加一个takeEvery,
这样就会有两个Generators同时启动。在实际项目中因为项目所分的模块可能会有很多,因此对每个模块都定义一个sagas是很有必要的,
最终在sagas的最外层定义一个index.js
文件用来将我们的所有模块整合在一起定义一个root
,然后我们只有在 main.js
的 root Saga 中调用sagaMiddleware.run
。就可以启动所有的sagas。
对于其他更加详细的redux-saga学习可以参考文档