从源码角度分析Koa2.0中间件机制

中间件概念

在NodeJS中,中间件主要是指封装所有Http请求细节处理的方法。一次Http请求通常包含很多工作,如记录日志、ip过滤、查询字符串、请求体解析、Cookie处理、权限验证、参数验证、异常处理等,但对于Web应用而言,并不希望接触到这么多细节性的处理,因此引入中间件来简化和隔离这些基础设施与业务逻辑之间的细节,让开发者能够关注在业务的开发上,以达到提升开发效率的目的。

中间件的行为与Vue里面的过滤器非常相似,就是在进入具体的业务处理之前,先让过滤器处理。中间件的最常见的处理模型叫做洋葱模型:

洋葱模型


Koa实现原理

Koa.js中间件机制是由koa-compose模块来实现的,也就是Koa.js实现洋葱模型的核心模块。Koa的中间件机制主要有以下两个重要的部分:

1、context的保存和传递

context是一个上下文对象,Koa针对每次请求都会创建一个上下文对象,这个对象会在中间件之间传递。

2、中间件的管理和next的实现

正如前文提到的,中间件一般不止一个,那么就需要考虑以下三个问题:

1、如何保存多个中间件

2、中间件的存放顺序

3、如何自动触发下个中间件处理函数,也就是如何实现next方法

带着以上问题,我们来看一下相关源码:

listen(...args) {

    debug('listen');

    const server = http.createServer(this.callback());

    return server.listen(...args);

}

// const compose = require('koa-compose');

callback() {

    //   this.middleware是保存所有中间件处理函数的数组,中间件处理函数是通过   koa实例.use()方法添加到this.middleware里面的

    //  fn是koa-compose模块处理后的结构

    // koa-compose会将所有中间件函数构成一个调用链

    const fn = compose(this.middleware);

    ......

    const handleRequest = (req, res) => {

        //  Koa特有的封装上下文对象方法,也就是上文提到的context对象

        const ctx = this.createContext(req, res);

        return this.handleRequest(ctx, fn);

    };

    return handleRequest;  //  handleRequest是请求的处理函数, 最后会作为http.createServer()的参数

}

//  fnMiddleware对应前文const fn = compose(this.middleware)里面的fn(中间件调用链)

handleRequest(ctx, fnMiddleware) {

     ......

    // handleResponse是真正处理业务的函数

    // onerror是错误处理函数

    return fnMiddleware(ctx).then(handleResponse).catch(onerror);

}

以上是Koa里面的主体逻辑,接下来我们把关注点放到koa-compose模块的实现,我们先来看看koa-compose模块的源代码:

koa-compose源码

源码代码量很少,理解起来却并不是那么容易。我们一步一步来,首先compose函数返回的也是一个函数,返回的函数就是前文的fnMiddleware函数,fnMiddleware可以接收两个参数:context(针对某一次请求的上下文对象),next(允许用户在中间件队尾加上指定的处理函数,也可以认为是一个中间件函数,Koa里面并没有使用这个参数)。

compose函数使用了js闭包:内部定义了一个index变量,以及引用它的dispatch函数;设置这个闭包的意图是防止一个请求多次调用同一个中间件。

接下来我们仔细说说dispatch函数,dispatch函数内部有这么两行:

if (i === middleware.length) fn = next   // next是用户可自定义的加在中间件队尾的处理函数

if (!fn) return Promise.resolve()

以上两行代码的意思是当用户并没有传入next参数时,默认返回一个resolve状态的promise对象。到这里为止,我们可以猜想compose函数期待返回的是一个promise对象,或者说compose函数的目标就是返回一个promise对象。那到底是不是呢?我们继续往下看:

try {

    return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));  

} catch (err) {

    return Promise.reject(err)

}

Promise.resolve和Promise.reject函数返回的都是promise对象,只是状态不同。至此为止,我们已经证明了我们的猜想,compose函数的返回结果是promise对象。

return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

这一行代码的应该是整个compose方法的核心,也是创建中间件队列的关键!

首先简单了解一下Promise.resolve方法:

1、允许调用时不带参数,直接返回一个resolved状态的 Promise 对象;

2、如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例;

3、参数是一个thenable对象,thenable对象指的是具有then方法的对象;

4、如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。这种情况比较特殊,例子如下:

const p = Promise.resolve('Hello');

p.then(function (s){

    console.log(s)

});

// Hello

Promise.reject方法的用法跟Promise.resolve方法基本一致,只是返回promise对象的状态不一样而已。


接下来看看这一部分:fn(context, dispatch.bind(null, i + 1))

fn:当前中间件函数;

context:本次请求的上下文对象;

dispatch.bind(null, i + 1):下一个中间件的调用入口(并不是下一个中间件本身)。

下一个中间件的调用入口这一部分可能并不好理解,但是看看以下这个小例子大家就应该懂了。

koa项目案例

下一个中间件的调用入口就是上图看到的next函数,是不是很熟悉?这下大家应该明白为什么koa中每个中间函数都会拿到下一个中间件的调用入口了。根据以上原理,中间件就构成了一个调用链。

由上可知,调用链执行完以后是返回一个promise对象,Koa最后还为我们定义了默认的处理函数handleResponse,它会在用户请求走完洋葱模型以后执行:

业务处理函数

很多小伙伴就有一个问题了:为什么要定义handleResponse?这其实很简单,该函数是用来返回最后的处理结果,如果没有这个函数,用户的的请求就永远无法处理完成。那为什么handleResponse会在洋葱模型之后才执行呢?这就关联到JS事件循环的内容了,这里不展开讨论。


其实到这里还没有完,我们再来看看洋葱模型的图:

洋葱模型


为什么会这样呢?我们来看下面两个图:

中间件执行顺序图1
中间件执行顺序图2


至此我们已经进一步了解了洋葱模型,但是还没有结束。。。。。为什么这么说呢?以上我们并没有考虑中间件函数中包含异步操作的情况!这里我门简单说一下如何处理异步的情况,我们需要借助ES6的async/await。

const Koa = require('koa')

const app = new Koa()

app.use(async (ctx, next) => {

    console.log(1)

    await next() // 这里得到的就是中间件2返回的promise对象

    console.log(3)}

)

app.use((ctx) => {

    return new Promise((resolve,reject) => {

        setTimeout(() => {

            console.log(2) resolve()

        }, 2000)

    })

})

app.listen(3001)

async/await可将异步代码像同步一样去执行,保证了洋葱模型。至于为什么要保证洋葱模型,我简单说以下两个好处:

1、首先可以在最外层定义一个捕捉错误的中间件,提高代码的健壮性。

2、当我们需要通过ctx对象在中间件之间传递数据时,洋葱模型可以很好的保证数据的正确性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容

  • 本节将结合例子和源码对koa2的中间件机制做一介绍。 什么是中间件? 中间件的本质就是一种在特定场景下使用的函数,...
    空无一码阅读 1,443评论 0 2
  • 我们知道,Koa 中间件是以级联代码(Cascading) 的方式来执行的。类似于回形针的方式,可参照下面这张图:...
    中间件阅读 1,554评论 0 4
  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,710评论 0 5
  • 前言 原文地址 最近几天花了比较长的时间在koa(1)的源码分析上面,初次看的时候,�被中间件执行那段整的晕乎乎的...
    谦龙阅读 1,508评论 0 9
  •   koa 中间件是以级联代码(Cascading) 的方式来执行的,可参照下面这张图: callback的源码如...
    小小的开发人员阅读 2,327评论 0 4