“我报名参加金石计划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_REQUESTLISTaction,在action执行的时候去启动相应的requestLists任务。
定义一个listSagas的原因就是我们这个文件中可能远不至这一个副作用函数,当定义了多个的时候,我们可以在all中添加一个takeEvery,
这样就会有两个Generators同时启动。在实际项目中因为项目所分的模块可能会有很多,因此对每个模块都定义一个sagas是很有必要的,
最终在sagas的最外层定义一个index.js文件用来将我们的所有模块整合在一起定义一个root,然后我们只有在 main.js 的 root Saga 中调用sagaMiddleware.run。就可以启动所有的sagas。

对于其他更加详细的redux-saga学习可以参考文档