koa-compose
是一个非常简单的函数,它接受一个中间件数组,返回一个函数,这个函数就是一个洋葱模型的核心。
洋葱模型
网上一搜一大把图,我就不贴图了,代码也不上,因为等会源码就是,这里只是介绍一下概念。
洋葱模型是一个非常简单的概念,它的核心是一个函数,这个函数接受一个函数数组,返回一个函数,这个函数就是洋葱模型的核心。
这个返回的函数就是聚合了所有中间件的函数,它的执行顺序是从外到内,从内到外。
例如:
- 传入一个中间件数组,数组中有三个中间件,分别是
a
、b
、c
。 - 返回的函数执行时,会先执行
a
,然后执行b
,最后执行c
。 - 执行完
c
后,会从内到外依次执行b
、a
。 - 执行完
a
后,返回执行结果。
这样说的可能还是不太清楚,来看一下流程图:
这里是两步操作,第一步是传入中间件数组,第二步是执行返回的函数,而我们今天要解析就是第一步。
源码
源码并不多,只有不到 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) { // ... } } }
这里确实是第一次见这样玩变量提升的,所以先给大家讲一下变量提升的规则:
- 变量提升是在函数执行前,函数内部的变量和函数声明会被提升到函数顶部。
- 变量提升只会提升变量声明,不会提升赋值。
- 函数提升会提升函数声明和函数表达式。
- 函数提升会把函数声明提升到函数顶部,函数表达式会被提升到变量声明的位置。
这里的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) // ... }
这个函数是返回的函数,这个函数接收两个参数,context
和next
,context
是上下文,next
是下一个中间件,这里的next
是compose
函数的第二个参数,也就是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
,所以这个判断是不会执行的。
后面就赋值了index
为i
,这样就可以防止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
是刚才取到的中间件,直接执行。
然后传入context
和dispatch.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
可以有更多的灵活性,写法也是多元化。