【若川】koa 洋葱模型实现:https://juejin.cn/post/7005375860509245471 【函数式编程指北】:https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ch5.html https://www.yuque.com/docs/share/0268760e-60bf-4278-871e-c1e83a68be7a

1. 前言

这周看的是 koa-compose 源码,虽然没用过 koa,但是用过 egg.js 的中间件,也实现过一个简易的 redux-middleware,对 compose 是有了解的。compose 不是实现中间件的必要实现,它是函数式编程的一种思想,主要作用是将多个函数结合在一起, 产生一个新的函数,那么我们来看看 koa-compose 利用 compose 做了什么。

2. 学习目标

看别人笔记的时候看到一句话,“源码其实并不难读,但怎么读,读什么,是想要读源码的人,最窘迫的问题”,深有同感。单单读源码可能是比较简单的,但要理解它为什么这么写,这么写的好处是什么,是比较难的。

我这一期的学习目标:
1)解读 koa-compose 源码,手写一个 compose
2)对比 compose 和 co 的实现

3. koa 中间件的使用

如果我们不了解一个东西,先学习它的使用方法,在实际工作中,我们开发 koa 中间件大概是这样的:

  1. // middleware/logger.js
  2. module.exports = function () {
  3. return async function logger(ctx, next) {
  4. console.log("logger中间件开始执行")
  5. await next()
  6. console.log("logger中间件执行完毕")
  7. }
  8. }
  9. // middleware/date.js
  10. module.exports = function () {
  11. return async function date(ctx, next) {
  12. console.log("date中间件开始执行")
  13. await next()
  14. console.log("date中间件执行完毕")
  15. }
  16. }
  17. // app.js
  18. import loggerMiddleware from "../middleware/logger.js"
  19. import dateMiddleware from "../middleware/date.js"
  20. app.use(loggerMiddleware())
  21. app.use(dateMiddleware())
  22. // 运行起来后,控制台会输出:
  23. // logger中间件开始执行
  24. // date中间件开始执行
  25. // date中间件执行完毕
  26. // logger中间件开始执行

看完上面的输出,是不是觉得有点疑惑呢,为什么执行 loggerMiddleware 的时候不是一次性执行完整个函数,看起来像一段一段地执行。反正我第一次开发 egg.js 中间件的时候很懵逼,不知道中间件的执行顺序,为什么要写 await next(),如何在中间件执行完成之后做一些操作,虽然后面看文档解决了,但疑惑却挥之不去。那么让我们来看看 koa 中间件是如何实现上面例子的执行顺序吧。

4. 解读 koa-compose

从源码实现的结构来看,compose 是一个闭包,它接收一个中间件函数数组,返回一个函数。这个函数接收 context, next 两个参数,返回一个 Promise。
在这个 dispatch 函数中,中间件从数组中取出并顺次执行,后面每调用一次 dispatch,index 会加 1。函数默认会执行 dispatch(0) ,这时从中间件数组 middleware 中取出第 0 个中间件并执行,同时将 dispatch(i+1) 作为 next 传递到下一次执行。

  1. function compose (middleware) {
  2. // 校验 middleware 是数组,且校验数组中的每一项是函数
  3. if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  4. for (const fn of middleware) {
  5. if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  6. }
  7. return function (context, next) {
  8. let index = -1
  9. function dispatch (i) {
  10. // 一个函数不能多次调用
  11. if (i <= index) return Promise.reject(new Error('next() called multiple times'))
  12. index = i
  13. // 获取中间件函数
  14. let fn = middleware[i]
  15. // 如果是最后一个中间件
  16. if (i === middleware.length) fn = next
  17. // 最后一个中间件已经执行完毕,fn 是没有值的,所以直接 resolve
  18. if (!fn) return Promise.resolve()
  19. try {
  20. // fn(context, dispatch.bind(null, i + 1)),首先执行了 fn 函数,同时将 dispatch(i+1) 作为 next 传递到下一次执行
  21. return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
  22. } catch (err) {
  23. return Promise.reject(err)
  24. }
  25. }
  26. return dispatch(0)
  27. }
  28. }

fn(context, dispatch.bind(null, i + 1)) 这句代码不容易看懂,我们可以结合上面的例子一起看看:

  1. // 以logger和date中间件为例
  2. fn(context, dispatch.bind(null, i + 1))
  3. // 第一次调用dispatch执行:
  4. // 此时 fn 是 logger:
  5. function logger(ctx, next) {
  6. console.log("logger中间件开始执行")
  7. // 此时next是什么,是 dispatch.bind(null, i + 1)
  8. // 第二次调用了 dispatch 函数,并使用bind把i+1传递给dispatch函数
  9. // 所以await next() 相当于执行第二个中间件date
  10. await next()
  11. // date中间件执行完毕结束之后再执行这句代码
  12. console.log("logger中间件执行完毕")
  13. }
  14. // 第二次调用dispatch执行:
  15. function date(ctx, next) {
  16. console.log("date中间件开始执行")
  17. // 此时next是什么,也是 dispatch.bind(null, i + 1)
  18. // 但此时i=2, 继续执行dispatch,middleware[2]是undefind, 返回了Promise.resolve()
  19. // 所以这句代码实际上是 await Promise.resolve()
  20. await next()
  21. // 接着执行这句代码
  22. console.log("date中间件执行完毕")
  23. }

如果看起来比较难懂的话,我们可以把 date 中间件嵌到 logger 中间件去看:

  1. logger(context, function next(){
  2. console.log("logger中间件开始执行")
  3. await Promise.resolve(
  4. date(context, function next(){
  5. console.log("date中间件开始执行")
  6. await Promise.resolve()
  7. console.log("date中间件执行完毕")
  8. })
  9. )
  10. console.log("logger中间件执行完毕")
  11. })

看到这里,我们就不难懂得,为什么在开发 koa 中间件的时候需要写 await next() 了吧,它是串行执行下一个中间件的“开关”,如果不写就无法执行到下一个中间件了。

5. compose 解决了什么问题

看完了 koa-compose,心里美滋滋的。但不可否认的是,我们看源码的过程中经常会有这样的疑惑:为什么要这样写?读源码,到底读到什么程度才算读懂?这也是我每次写笔记定下学习目标的原因。

我们在工作中有时候会这样调用多层函数:

  1. function getId(id){
  2. // 一些处理
  3. return id
  4. }
  5. function getData(id){
  6. // 一些处理
  7. return data
  8. }
  9. function formatData(data){
  10. // 一些处理
  11. return formatdata
  12. }
  13. const result = formatData(getData(getId(5)))

这种方法实现中间件的串联执行也是可以的,其实就是我们拆解 koa-compose 最后的嵌套代码。这种写法既不优雅,健壮性也不好(比如发生错误的情况,需要在每一个函数里做catch)。为了解决函数多层调用的嵌套问题,我们需要用到函数合成 compose。

那么 compose 的就是将这三个函数组合起来:

  1. // 函数合成
  2. const compose = function () {
  3. // TODO: 写一个compose
  4. }

6. 对比 compose 和 co 的实现

待续…

7. 感想

待续…