redux中间件Middleware
文奇 人气:0引言
上一节我们学习了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学习可以参考文档
加载全部内容