【源码共读】洋葱模型 koa-compose

简介: 【源码共读】洋葱模型 koa-compose


koa-compose是一个非常简单的函数,它接受一个中间件数组,返回一个函数,这个函数就是一个洋葱模型的核心。


洋葱模型


网上一搜一大把图,我就不贴图了,代码也不上,因为等会源码就是,这里只是介绍一下概念。


洋葱模型是一个非常简单的概念,它的核心是一个函数,这个函数接受一个函数数组,返回一个函数,这个函数就是洋葱模型的核心。


这个返回的函数就是聚合了所有中间件的函数,它的执行顺序是从外到内,从内到外。


例如:


  1. 传入一个中间件数组,数组中有三个中间件,分别是abc
  2. 返回的函数执行时,会先执行a,然后执行b,最后执行c
  3. 执行完c后,会从内到外依次执行ba
  4. 执行完a后,返回执行结果。


这样说的可能还是不太清楚,来看一下流程图:

image.png

这里是两步操作,第一步是传入中间件数组,第二步是执行返回的函数,而我们今天要解析就是第一步。


源码


源码并不多,只有不到 50 行,我们来看一下:

'use strict'
/**
 * Expose compositor.
 */
module.exports = compose
/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */
function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }
  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */
  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

虽然说不到 50 行,其实注释都快占了一半,直接看源码,先看两个部分,第一个导出,第二个是返回的函数。

module.exports = compose
function compose (middleware) {
  // ...
  return function (context, next) {
      return dispatch(0)
      function dispatch (i) {
        // ...
      }
  }
}

这里确实是第一次见这样玩变量提升的,所以先给大家讲一下变量提升的规则:


  1. 变量提升是在函数执行前,函数内部的变量和函数声明会被提升到函数顶部。
  2. 变量提升只会提升变量声明,不会提升赋值。
  3. 函数提升会提升函数声明和函数表达式。
  4. 函数提升会把函数声明提升到函数顶部,函数表达式会被提升到变量声明的位置。


这里的module.exports = compose是变量提升,function compose是函数提升,所以compose函数会被提升到module.exports之前。


下面的return dispatch(0)是函数内部的变量提升,dispatch函数会被提升到return之前。


虽然这样可行,但是不建议这样写,因为这样写会让代码变得难以阅读,不多说了,继续吧:

function compose (middleware) {
    if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
    for (const fn of middleware) {
        if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
    }
    // ...
}

最开始就是洋葱模型的要求判断了,中间件必须是数组,数组里面的每一项必须是函数。


继续看:

return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    // ...
}

这个函数是返回的函数,这个函数接收两个参数,contextnextcontext是上下文,next是下一个中间件,这里的nextcompose函数的第二个参数,也就是app.callback()的第二个参数。


index注释写的很清楚,是最后一个调用的中间件的索引,这里初始化为-1,因为数组的索引是从0开始的。


dispatch函数是用来执行中间件的,这里传入0,也就是从第一个中间件开始执行。

function dispatch (i) {
    if (i <= index) return Promise.reject(new Error('next() called multiple times'))
    index = i
}

可以看到,dispatch函数接收一个参数,这个参数是中间件的索引,这里的i就是dispatch(0)传入的0


这里的判断是为了防止next被调用多次,如果i小于等于index,就会抛出一个错误,这里的index-1,所以这个判断是不会执行的。


后面就赋值了indexi,这样就可以防止next被调用多次了,继续看:

let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()

这里的fn是中间件,也是当前要执行的中间件,通过索引直接从最开始初始化的middleware数组里面取出来。


如果是到了最后一个中间件,这里的next指的是下一个中间件,也就是app.callback()的第二个参数。


如果fn不存在,就返回一个成功的Promise,表示所有的中间件都执行完了。


继续看:

try {
    return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
    return Promise.reject(err)
}

这里就是执行中间件的地方了,fn是刚才取到的中间件,直接执行。


然后传入contextdispatch.bind(null, i + 1),这里的dispatch.bind(null, i + 1)就是next,也就是下一个中间件。


这里就有点递归的感觉了,但是并没有直接调用,而是通过外部手动调用next来执行下一个中间件。


这里的try...catch是为了捕获中间件执行过程中的错误,如果有错误,就返回一个失败的Promise


动手


老规矩,还是用class来实现一下这个compose函数。

class Compose {
    constructor(middleware) {
        if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
        for (const fn of middleware) {
            if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
        }
        this.index = -1
        this.middleware = middleware
        return (next) => {
            this.next = next
            return this.dispatch(0)
        }
    }
    dispatch(i) {
        if (i <= this.index) return Promise.reject(new Error('next() called multiple times'))
        this.index = i
        let fn = this.middleware[i]
        if (i === this.middleware.length) fn = this.next
        if (!fn) return Promise.resolve()
        try {
            return Promise.resolve(fn(this.dispatch.bind(this, i + 1)))
        } catch (err) {
            return Promise.reject(err)
        }
    }
}
var middleware = [
    (next) => {
        console.log(1)
        next()
        console.log(2)
    },
    (next) => {
        console.log(3)
        next()
        console.log(4)
    },
    (next) => {
        console.log(5)
        next()
        console.log(6)
    }
]
var compose = new Compose(middleware)
compose()
var middleware = [
    (next) => {
        return next().then((res) => {
            return res + '1'
        })
    },
    (next) => {
        return next().then((res) => {
            return res + '2'
        })
    },
    (next) => {
        return next().then((res) => {
            return res + '3'
        })
    }
]
var compose = new Compose(middleware)
compose(() => {
    return Promise.resolve('0')
}).then((res) => {
    console.log(res)
})

这次不放执行结果的截图了,可以直接浏览器控制台中自行执行。


总结


koa-compose的实现原理就是通过递归来实现的,每次执行中间件的时候,都会返回一个成功的Promise


其实这里不使用Promise也是可以的,但是使用Promise可以有效的处理异步和错误。


而且从上面手动实现的代码案例中也可以看到,使用Promise可以有更多的灵活性,写法也是多元化。


目录
相关文章
|
6月前
|
JavaScript Unix Linux
Nodejs 第十二章(path)
Nodejs 第十二章(path)
51 0
|
Arthas Kubernetes 数据可视化
推荐10个GitHub上适合练手的后端项目(涵盖初中高阶)
上周,我们推出了26个好玩又有挑战的前端练习项目。 不少同学留言说,那后端的呢?后端也要! 淘系工程师一呼就应,我们邀请了2位淘系技术后端工程师,筛选出10个难度层层递进,好玩且实用的后端项目,包含java类库中的“瑞士军刀”工具、可视化API展现等等,难度依然分为【初级篇:4个】、【中级篇:3个】、【高级篇:3个】,不同学习诉求的同学可按需选择~
推荐10个GitHub上适合练手的后端项目(涵盖初中高阶)
|
JavaScript 中间件
什么是koa洋葱模型?
什么是koa洋葱模型?
146 0
|
中间件
说说你对koa中洋葱模型的理解?
说说你对koa中洋葱模型的理解?
144 0
|
6月前
|
设计模式 中间件 开发者
Koa2 的洋葱模型是什么?它是如何实现的?
Koa2 的洋葱模型是什么?它是如何实现的?
222 0
|
6月前
|
存储 JSON 缓存
【源码共读】axios的46个工具函数
【源码共读】axios的46个工具函数
151 0
|
JSON JavaScript API
【从零到一手撕脚手架 | 第二节】模块化封装 降低耦合度 封装 axios pinia router
前一节我们讲解了脚手架的基础项目搭建。接下来教大家将Vue技术栈常用的工具进行封装,让我们项目的代码更易维护。
344 0
【从零到一手撕脚手架 | 第二节】模块化封装  降低耦合度 封装 axios pinia router
|
JSON 前端开发 数据可视化
umi3源码探究简析
作为蚂蚁金服整个生态圈最为核心的部分,umi可谓是王冠上的红宝石,因而个人认为对于整个umi架构内核的学习及设计哲学的理解,可能比如何使用要来的更为重要;作为一个使用者,希望能从各位大佬的源码中汲取一些养分以及获得一些灵感
232 0
|
JSON JavaScript 前端开发
koa框架学习记录(3)
一个前端学习koa的简单记录
|
前端开发
koa框架学习记录(5)
一个前端学习koa的简单记录