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中间件处理流程。

koa2笔记_第1张图片
洋葱模型.png

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

Koa2.js的源码阅读笔记

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


koa2笔记_第2张图片
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)
      }
    }
  }
}

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


koa2笔记_第3张图片
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

你可能感兴趣的:(koa2笔记)