koa洋葱模型 compose函数原理 中间件

1,856 阅读7分钟

用法

koa 框架里面的中间件用法

同步示例

app.use((ctx, next) => {
    console.log(1)
    next()
    console.log(3)
})
app.use((ctx) => {
    console.log(2)
})
//  1 => 2 => 3

异步示例

const Koa = require('koa');
 
const app = new Koa();
 
app.use(async (ctx, next) => {
    console.log(1);
    await next();
    console.log(1.1);
});
 
app.use(async (ctx, next) => {
    console.log(2);
    await next();
    console.log(2.2);
});
 
app.use(async (ctx, next) => {
    console.log(3);
    await next();
    console.log(3.3);
});

打印结果

// 1
// 2
// 3
// 3.3
// 2.2
// 1.1

当程序运行到await next()的时候就会暂停当前程序,进入下一个中间件,处理完之后才会仔回过头来继续处理。也就是说,当一个请求进入,#1会被第一个和最后一个经过,#2则是被第二和倒数第二个经过,依次类推。

注意:每个 use 里面都是一个中间件,这中间件可以自己编写,也有很多第三方的,app.use 经常可以见到

洋葱模型

实际用法举例

const Koa = require("koa");
const app = new Koa();
 
// x-response-time
 
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set("X-Response-Time", `${ms}ms`);
});
 
// logger
 
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});
 
// response
 
app.use(async ctx => {
  ctx.body = "Hello World";
});
 
app.listen(3000);

基本上,Koa 所有的功能都是通过中间件实现的。

简单源码实现

思路:use函数中push中间件函数进数组;;;遇到next则执行下一个中间件

下面这个是实际的源码,可以扫一眼之后,往下看

class Koa {
    constructor () {
        this.middlewares = []
    }
    use (callback) {
        this.middlewares.push(callback)
    }
}
  
var app = new Koa()
  
app.use(async (ctx, next) => {
    console.log(1)
    await next()
    console.log(2)
})
  
app.use(async (ctx, next) => {
    console.log(3)
    await next()
    console.log(4)
})
  
app.use(async (ctx, next) => {
    console.log(5)
})
  
function compose (middlewares) {
    let index = -1
  
    return dispatch(0)
  
    function dispatch (i) {
        index = i
        if (i === middlewares.length) return
          
        let fn = middlewares[i]
          
        let result = fn(null, function next() { // 这里的null应该是context,
            return dispatch(i+1)    // 世界上核心的思想就是,next()时先执行下一个middleware
        })
        return Promise.resolve(result)
    }
}
  
compose(app.middlewares)

如果上面已经看懂了,就不需要往下看了。。。

开始源码

首先来实现一个简单的同步的例子

class Koa {
    constructor () {
        this.middlewares = []
    }
    use (fn) {
        this.middlewares.push(fn)
    }
}
var app = new Koa()
// 中间件1
app.use(function (next) {
    console.log(1)
    next()
    console.log(2)
})
// 中间件2
app.use(function (next) {
    console.log(3)
})
 
function compose (middlewares) {
    function dispatch (index) {
        if (index === middlewares.length) return // 所有中间件都执行完了
        const fn = middlewares[index] // 获得中间件
        fn( // 注意这是一个中间件函数执行,下面的 function next 是参数,函数作为参数,所以中间件中才能执行这个函数
            // 开始执行中间件,打印console.log(1)
            function next(){ // 相当于 自建了一个 next 函数,等待中间件中 await next() 调用,这行中的next就是个标识,可以不写
                // 中间件中执行的await next() 就相当于执行了这个函数
                // 直接进入下一个中间件执行
                dispatch(index+1)
            
            }
         )
    }
    dispatch(0)
}
compose(app.middlewares) // 1 3 2
 
// 整个过程就相当于
function next () {
    m2()
}
function m2 () {
    console.log(3)
}
function m1 () {
    console.log(1)
  next()
  console.log(2)
}
m1() // 执行

同步比较好理解吧,接下来就看异步,中间件为 async 函数,await next()

首先先看一下一个知识点:

async function test() {
    var res = await ajax()
    console.log('res', res)
}
 
function ajax() {
    new Promise(resolve => { // 这里没 return
        setTimeout(() => {
            resolve(1)
            console.log(2)
        }, 2000)
    })
}
 
test()
// 输出
// res: undefined
// 2秒后输出2

你会发现,res 没有 await 等待2秒后的 resolve。

是因为ajax在第7行并没有 return ,也就是说 ajax()返回的是 undefined ,而不是promise,等同于下面这样

async function test() {
    var res = await ajax()
    console.log('res', res)
}
 
function ajax() {
    setTimeout(() => {
        console.log(2)
    }, 2000)
}
 
test()

说白了,你如果 await 后面的 ajax() 不返回一个 promise 对象,那么 await 这关键字就认为没啥用了

var res = await ajax()

===

var res = ajax()

因此,若想发挥 await 的等待的作用,一定要后面返回的是 promise 对象

开始异步代码分析

前言,在以前 node 不支持 async 和 await 的时候,compose 的异步 promise 支持,代码原理非常麻烦,各种 .then

但是,当前 node 已经支持 async 和 await,看下面

改成异步:


async function next () {
    return m2()
}
async function m2 () { // 异步中间件2
    return new Promise(resolve => {
        setTimeout(() => {
        resolve()
            console.log(3)
    }, 2000)
    })
}
async function m1 () { // 异步中间件1
    console.log(1)
  await next() // 等待
  console.log(2)
}
m1() // 执行
// 1
// 3
// 2

可以看到,2的输出确实是等待了await 2秒后了。说明了啥?

说明了 await 后面的 next() ,你只要返回的是正常的promise对象,那么即使你是异步,和同步的效果是一样的:

什么效果?就是 m1 里面每一行都按顺序来啊,console.log(2) 还是会等待 next()啊,再回看下同步

// 整个过程就相当于
function next () {
    m2()
}
function m2 () {
    console.log(3)
}
function m1 () {
    console.log(1)
  next() // 同步代码,本身就先执行完 next() 再下一步
  console.log(2)
}
m1() // 执行

所以对于 m1 来说,执行到 next 那行代码时

同步情况:next(),本身就要等next()执行完再执行下面的代码

异步情况:await next() ,有await起作用,等待,n 秒后 resolve ,再执行后续的代码

总结:

说了这么多,我们的目的是什么?compose 最核心的本质是什么?

洋葱模型的最核心的本质是:

function m1 () {
    console.log(1)
  next() // 去执行下一个中间件 m2,一定要完全执行完后,才能继续下面的代码!!!
  console.log(2)
}

本质:console.log(2)一定要在next()完全完事后再执行

即:next()执行下一个中间件的这个过程,一定要中断 m1 ,等next()完全执行完后,再继续 m1


app.use(async next=>{
    console.log(1)
  await next()
  console.log(1.1)
})
 
app.use(async next=>{
    console.log(2)
  await next()
  console.log(2.1)
})
 
app.use(async next=>{
    console.log(3)
  await next()
  console.log(3.1)
})
 
app.use(async next=>{
    console.log(4)
})

输出顺序:1 2 3 4 3.1 2.1 1.1

console.log(1) 一定要等 await next()完全执行完后,才执行 console.log(1.1)

而 2 3 4 3.1 2.1 是不是就是next完全执行完?

function m1 () {
    console.log(1)
  await next() // 必须保证完全执行完
  console.log(2)
}

因此对于异步来说,核心就是保证 await next()完全执行完毕

保证1:必须写 await 关键字,否则无效

保证2:next() 必须返回的是 promise 对象,否则无效

因此,对于异步源码来说,仅需对同步源码进行改造,完成保证2中返回 promise 对象即可

因为保证1中的 await ,是我们用的时候手写的啊,又不是源码中的。。。

注意别忘了:

await等待到啥时候,是取决于后面promise中何时 resolve

关于 async 和 await 忘记了的,还得去看 async 原理那个文章

异步源码:

class Koa {
    constructor () {
        this.middlewares = []
    }
    use (fn) {
        this.middlewares.push(fn)
    }
}
var app = new Koa()
// 中间件1
app.use(async function (next) {
    console.log(1)
    await next()
    console.log(2)
})
function ajax () {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve()
            console.log(5)
        }, 2000)
    })
}
// 中间件2
app.use(async function (next) {
    await ajax()
    console.log(3)
})
 
function compose (middlewares) {
    function dispatch (index) {
        if (index === middlewares.length) return // 所有中间件都执行完了
        const fn = middlewares[index] // 获得中间件
        let res = fn( // 注意这是一个中间件函数执行,下面的 function next 是参数,函数作为参数,所以中间件中才能执行这个函数
            // 开始执行中间件,打印console.log(1)
            function next(){ // 相当于 自建了一个 next 函数,等待中间件中 await next() 调用,这行中的next就是个标识,可以不写
                // 中间件中执行的await next() 就相当于执行了这个函数
                // 直接进入下一个中间件执行
                return dispatch(index+1)
                // 这里的 return 为了确保 next()有返回值,而若想返回值为promise对象,那是不是得 dispatch()返回?
        
            }
         )
          
         // 注意,这里才是 dispatch()返回值得地方,对应第39行。必须要返回 promise ,供 await 使用
         // 而返回什么 promise 对象啊?返回的当然是你要 await 啥了啊
         // await next(),   next() 的本质是  完全执行完下一个中间件函数
         // ,因此 await 等待的 promise 对象,就是中间件函数执行完后返回的promise !
         // 来看看中间件函数是啥
         /* async function (next) {
              console.log(3)
          }
                */
            // 这就是要执行的下一个中间件函数啊,也就是第34行的fn啊
          // 那目的就成为 fn() 执行后要返回一个 promise
          // 而 fn 本身就是带有 async 的,async 关键字的函数本身就返回 promise !
          // 因此如果你中间件函数直接写 async 了,那直接返回 res 就行了!
          
         return res // async本身返回的就是 promise
         // 但是源码中会有判断你中间件函数写没写 async,因此如果fn()返回的是乱七八糟的,兼容都转成promise了
         // return Promise.resolve(res) // 不管你 res是啥,全都转成 promise
    }
    dispatch(0)
}
compose(app.middlewares)

从34行看到61行,仔细想下,注意中间件2中还有个await ajax(),目的是模拟 next()是一个n秒的异步,await 等待他