koa2笔记

Q1:什么是中间件

中间件(Middleware),也叫中介层,是提供系统软件和应用软件之间连接的软件,便于软件各部分之间的沟通。
中间件只是一种服务,没有这种服务 系统也能够存在。
一般提到中间件这个概念就必须要提到AOP(一个中间件一般有两个切面,遵循先进后出的切面执行顺序)。

koa中的中间件函数能够访问请求对象和响应对象以及应用程序的请求/响应循环中的下一个中间件函数。类似过滤器,在请求和响应到来前,先进行处理掉一些逻辑。

Q2:什么是AOP

面向切面编程AOP(Aspect Oriented Programmming)是一种非侵入式扩充对象、方法和函数行为的技术。

  • 侵入式是需要知道框架中的代码,与框架代码紧密结合在一起。
  • 非侵入式是可以自由选择和组装各个功能模块,没有过多的依赖。

AOP就是在现有代码程序中,在程序生命周期或横向流程中加入/减去一个或多个功能,不影响原有功能。
(⚠️继承、组合、委托等也可以用来增加和合并行为,但是多数情况下,AOP被证明是更灵活和更少侵入的方式)

场景描述:我们需要在thing.doSomething()中做一些数据分析,需要收集当前函数执行的时间等信息,应该如何扩展呢?

var originDoSomething = Thing.prototype.doSomething;
Thing.prototype.doSomething = function(){
    doSomethingBefore(); //增加行为
    return originDoSomething.apply(this, arguments);
}

上述实现有效的为thing.doSomthing();增加了行为。在调用thing.doSomthing();时,将首先调用doSomethingBefore(),然后再执行原来的行为。

上述实现方案的好处:

  • Thing的源代码没有被修改。(VS 粗暴的直接将要增加的行为添加到Thing.prototype.doSomething中)
  • Thing的使用方无需修改调用代码。(VS 为不侵入Thing实现,将行为增加到Thing的每个调用位置;继承的话也需调用方修改因为引入了新的构造函数)
  • doSomething的原本行为得以保留。
  • Thing并不知道doSomethingBefore的存在,并且不依赖它。因此,Thing的单测也无需更新。

从AOP的角度,可以说doSomethingBefore()是应用于this.doSomething()的一个行为切面,被称为"before advice",即thing.doSomething()在执行原来的行为之前会先执行doSomethingBefore。(AOP通常可以实现多种类型,如before、after、afterReturning、afterThrowing、around)。

🐰其实现在很多跨端兼容的框架都是采用AOP的思想实现的。对源码无侵入,采用拦截器的形式对原型进行拦截,增加行为处理逻辑。

AOP VS OOP

AOP(面向切面编程)是OOP(面向对象编程)的延续。二者在字面是虽然非常类似,但却是面向不同领域的两种设计思想。

  • OOP针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更佳清晰高效的逻辑单元划分;
  • AOP则针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合的隔离效果。

Q3:什么是洋葱模型?

koa2最出色的就是基于洋葱模型的HTTP中间件处理流程。

洋葱模型.png

通过next()把多个中间件串联执行的效果。所有中间件都会执行两遍,就像洋葱一样,从洋葱的一侧进入就会从另一侧出去。

Koa2.js的源码阅读笔记

Koa中间件采用堆栈形式先进后出(first-in-last-out)的执行顺序。


image.png

先来看一段koa的使用用例:

const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
    console.log('...fun1 begin');
    const start = Date.now();
    await next();
    console.log('...fun1 end');
    const ms = Date.now() - start;
    ctx.set('X-Response-Time', `${ms}ms`);
});

app.use(async ( ctx, next) => {
    const start = Date.now();
    console.log('...fun2 begin');
    await next();
    console.log('...fun2 end');
    const ms = Date.now() - start;
    console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

app.use(async ctx => {
    console.log('...fun3 begin');
    ctx.body = "Hello World";
});

app.listen(3000);

/* 代码输出结果如下:
...fun1 begin
...fun2 begin
...fun3 begin
返回 respond: ctx.body是Hello world
...fun2 end  ⚠️这里开始原路返回了
GET / - 4ms
...fun1 end
*/

根据上述代码的输出结果,🤔️ async/await会暂停当前流程,next参数是什么呢?
每碰到 await next,代码会跳出当前中间件,执行下一个,最终还会原路返回,依次执行await next下面的代码。实际上是一个递归返回Promise。

🤔️如何实现中间件洋葱执行模型的?

  1. 基于generator + co.js(koa1)
function* fun1(){
    console.log('fun1 begin');
    yield *fun2Iterator;
    console.log('fun1 end')
}

function* fun2(){
    console.log('fun2 begin');
    yield *fun3Iterator;
    console.log('fun2 end')
}

function* fun3(){
    console.log('fun3 begin');
}

var fun1Iterator = fun1(),
    fun2Iterator = fun2(),
    fun3Iterator = fun3();
fun1Iterator.next();
/**
fun1 begin
fun2 begin
fun3 begin
fun2 end
fun1 end
 */

es6中引入了Generator函数,类似于一个状态机,封装了多个内部状态。通过yield语句暂停,输出当前的状态。

⚠️在koa中使用的是yeild next,而这里我们使用的是yield *next;在koa中yeild nextyeild *next是等价的,这主要得益于co库

  1. 基于async/await(koa2)
    node.js v7.6.0开始完全支持async/await,koa2 node环境需要7.6.0以上;
    利用匿名函数自执行的特性结合Promise.resolve()实现代码如下:
Promise.resolve((async()=>{
    console.log('fun1 begin');
    await Promise.resolve((async() => {
        console.log('fun2 begin');
        await Promise.resolve((async() => {
            console.log('fun3 begin');
        })());
        console.log('fun2 end');
    })());
    console.log('fun1 end');
})());

/**
fun1 begin
fun2 begin
fun3 begin
fun2 end
fun1 end
*/

看到输出结果,不就是洋葱模型的输出嘛 😄 到这里,你是否能清晰的感知到Promise结合await async后的强大能力呢?那么问题又来了,koa2是如何实现通过await next()开始直接执行下一个中间件的呢?他是如何将中间件按顺序串联起来的呢?答案就藏在compose模块模块中。

下面👇一起看看koa2.js都做了些什么吧

1. 封装node http Server

先不要着急,我们先来看看不依赖于框架,直接使用Node.js提供的API如何实现一个Server:

const http = require('http');

http.createServer((req, res) => {
    res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
    res.end("Hello World");
})
.listen(9999);

请求一进来,就会执行http.createServer的callback。

所以koa对callback模块进行了一些处理(主要由app.use来注册回调函数),通过app.listen()开启server并传入callback,如下:

listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

2. 构造resquest, response, context对象

callback()初始化的时候会执行compose对所有的中间件函数进行聚合,方便后续可以按顺序控制执行中间件函数调用,并返回新构建的handleRequest()

在请求进来的时候才会执行callbackhandleRequest,这时会对req和res进行合并成为ctx,并递归执行中间件。

callback() {
    const fn = compose(this.middleware); //组合middleware

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn); //合并req和res到ctx上
    };

    return handleRequest;
}
  • request:对node原生的request对象的封装;
  • response:对node原生的response对象的封装;
    使用JS的gettersetter属性,基于node的对象req/res对象封装Koa的request/response对象。
  • context:回调函数的上下文,挂载了koa request和response对象;使用delegates模块对一些常用方法进行了代理。

参考代理机制实现

为什么需要ctx呢?

koa处理请求是按照中间件的形式的,而中间件并没有返回值的。那么如果一个中间件的处理结果需要下一个中间件使用的话,该怎么把这个结果告诉下一个中间件呢?如:有一个中间件是解析token的将它解析成userId,然后后面的中间件需要使用到,那么如果将它传递过去呢?

其实中间件就是一个函数,维护一个对象ctx,给每个中间件都传入ctx,所有中间件便能通过这个ctx来进行交互了。

3. 中间件机制

由于对middleware中间件函数的整合处理compose(),所以一旦有请求进来会把所有中间件函数执行一遍,具体实现如下:

function compose (middleware) {
  return function (context, next) {
    let index = -1
    return dispatch(0) //派发执行第一个中间件
    function dispatch (i) {
      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))); //通过resolve执行async方法将下一个中间件函数传入进入,所以在app.use上可以接受到next(下一个中间件)
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

下一个中间件函数以参数的形式传入进来了:


next中间件.png

⚠️ 执行第一个中间件的await next()的时候实际执行的是dispatch(1) 由于dispatch()是一个闭包,所以它会拿到父级的index。

总结一下中间件机制的实现:koa2利用async + await实现让中间件的洋葱模型;通过compose()组合中间件数组构造next(),实现 await next()派发下一个中间件,控制中间件的执行顺序。

4. 错误处理

一个健壮的框架必须保证在发生错误的时候,能够捕获错误并有降级方案返回给客户端。细心的伙计应该发现了Koa2中的Application继承自nodejs中的events

推荐阅读:
https://juejin.im/post/5decf130f265da339565d40e?utm_source=gold_browser_extension

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

推荐阅读更多精彩内容

  • 本节将结合例子和源码对koa2的中间件机制做一介绍。 什么是中间件? 中间件的本质就是一种在特定场景下使用的函数,...
    空无一码阅读 1,442评论 0 2
  • Koa 学习 历史 Express Express是第一代最流行的web框架,它对Node.js的http进行了封...
    Junting阅读 2,809评论 0 0
  • [TOC] Koa2 Koa2是现在最流行的基于Node.js平台的web开发框架,它很小,但扩展性很强使用 ko...
    duangdong阅读 1,803评论 0 5
  • Koa 必须使用 7.6 以上的版本。如果你的版本低于这个要求,就要先升级 Node。 基本用法 Koa 提供一个...
    Gukson666阅读 2,451评论 0 1
  • 框架提出的背景 ES6/7带来的变革 自ES6确定和ES7中async/await开始普及,Node的发展变得更加...
    宫若石阅读 8,497评论 1 14