IT虾米网

从浅入深了解Koa2源码

wyy 2022年05月13日 编程语言 155 0

在前文我们介绍过什么是 Koa2 的基础

简单回顾下

什么是 koa2

  1. NodeJS 的 web 开发框架
  2. Koa 可被视为 nodejs 的 HTTP 模块的抽象

源码重点

中间件机制

洋葱模型

compose

源码结构

Koa2 的源码地址:CSDN

其中 lib 为其源码

koa2源码

可以看出,只有四个文件:application.jscontext.jsrequest.jsresponse.js

application

为入口文件,它继承了 Emitter 模块,Emitter 模块是 NodeJS 原生的模块,简单来说,Emitter 模块能实现事件监听和事件触发能力

application1

删掉注释,从整理看 Application 构造函数

Application构造函数

Application 在其原型上提供了 listen、toJSON、inspect、use、callback、handleRequest、createContext、onerror 等八个方法,其中

  • listen:提供 HTTP 服务
  • use:中间件挂载
  • callback:获取 http server 所需要的 callback 函数
  • handleRequest:处理请求体
  • createContext:构造 ctx,合并 node 的 req、res,构造 Koa 的 参数——ctx
  • onerror:错误处理

其他的先不要在意,我们再来看看 构造器 constructor

Application的构造器

晕,这都啥和啥,我们启动一个最简单的服务,看看实例

const Koa = require('Koa') 
 
const app = new Koa() 
 
app.use((ctx) => { 
  ctx.body = 'hello world' 
}) 
 
app.listen(3000, () => { 
  console.log('3000请求成功') 
}) 
 
console.dir(app)

实例

能看出来,我们的实例和构造器一一对应,

打断点看原型

断点

哦了,除去非关键字段,我们只关注重点

Koa 的构造器上的 this.middleware、 this.context、 this.request、this.response

原型上有:listen、use、callback、handleRequest、createContext、onerror

注:以下代码都是删除异常和非关键代码

先看 listen

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

可以看出 listen 就是用 http 模块封装了一个 http 服务,重点是传入的 this.callback()。好,我们现在就去看 callback 方法

callback

  callback() { 
    const fn = compose(this.middleware) 
    const handleRequest = (req, res) => { 
      const ctx = this.createContext(req, res) 
      return this.handleRequest(ctx, fn) 
    } 
    return handleRequest 
  }

它包含了中间件的合并,上下文的处理,以及 res 的特殊处理

中间件的合并

使用了 koa-compose 来合并中间件,这也是洋葱模型的关键,koa-compose 的源码地址:CSDN。这代码已经三年没动了,稳的一逼

function compose(middleware) { 
  return function (context, next) { 
    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) 
      } 
    } 
  } 
}

一晃眼是看不明白的,我们需要先明白 middleware 是什么,即中间件数组,那它是怎么来的呢,构造器中有 this.middleware,谁使用到了—— use 方法

我们先跳出去先看 use 方法

use

use(fn) { 
    this.middleware.push(fn) 
    return this 
}

除去异常处理,关键是这两步,this.middleware 是一个数组,第一步往 this.middleware 中 push 中间件;第二步返回 this 让其可以链式调用,当初本人被面试如何做 promise 的链式调用,懵逼脸,没想到在这里看到了

回过头来看 koa-compose 源码,设想一下这种场景

... 
app.use(async (ctx, next) => { 
    console.log(1); 
    await next(); 
    console.log(6); 
}); 
app.use(async (ctx, next) => { 
    console.log(2); 
    await next(); 
    console.log(5); 
}); 
 
app.use(async (ctx, next) => { 
    console.log(3); 
    ctx.body = "hello world"; 
    console.log(4); 
}); 
...

我们知道 它的运行是 123456

它的 this.middleware 的构成是

this.middleware = [ 
  async (ctx, next) => { 
    console.log(1) 
    await next() 
    console.log(6) 
  }, 
  async (ctx, next) => { 
    console.log(2) 
    await next() 
    console.log(5) 
  }, 
  async (ctx, next) => { 
    console.log(3) 
    ctx.body = 'hello world' 
    console.log(4) 
  }, 
]

不要感到奇怪,函数也是对象之一,是对象就可以传值

const fn = compose(this.middleware)

我们将其 JavaScript 化,其他不用改,只需要把最后一个函数改成

async (ctx, next) => { 
  console.log(3); 
  -ctx.body = 'hello world'; 
  +console.log('hello world'); 
  console.log(4); 
}

测试compose

测试compose2

逐行解析 koa-compose

这一段很重要,面试的时候常考,让你手写一个 compose ,淦它

//1. async (ctx, next) => { console.log(1); await next(); console.log(6); } 中间件 
//2. const fn = compose(this.middleware) 合并中间件 
//3. fn() 执行中间件 
 
function compose(middleware) { 
    return function (context, next) { 
        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); 
            } 
        } 
    }; 
}

执行 const fn = compose(this.middleware),即如下代码

const fn = function (context, next) { 
    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) 
      } 
    } 
  } 
}

执行 fn(),即如下代码:

const fn = function (context, next) { 
    let index = -1 
    return dispatch(0) 
    function dispatch (i) { 
      if (i <= index) return Promise.reject(new Error('next() called multiple times')) 
      index = i    // index = 0 
      let fn = middleware[i] // fn 为第一个中间件 
      if (i === middleware.length) fn = next // 当弄到最后一个中间件时,最后一个中间件赋值为 fn 
      if (!fn) return Promise.resolve() 
      try { 
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1))) 
          // 返回一个 Promise 实例,执行 递归执行 dispatch(1) 
      } catch (err) { 
        return Promise.reject(err) 
      } 
    } 
  } 
}

也就是第一个中间件,要先等第二个中间件执行完才返回,第二个要等第三个执行完才返回,直到中间件执行执行完毕

Promise.resolve 就是个 Promise 实例,之所以使用 Promise.resolve 是为了解决异步,之所以使用 Promise.resolve 是为了解决异步

抛去 Promise.resolve,我们先看一下递归的使用,执行以下代码

const fn = function () { 
    return dispatch(0); 
    function dispatch(i) { 
        if (i > 3) return; 
        i++; 
        console.log(i); 
        return dispatch(i++); 
    } 
}; 
fn(); // 1,2,3,4

回过头来再看一次 compose,代码类似于

// 假设 this.middleware = [fn1, fn2, fn3] 
function fn(context, next) { 
    if (i === middleware.length) fn = next // fn3 没有 next 
    if (!fn) return Promise.resolve() // 因为 fn 为空,执行这一行 
    function dispatch (0) { 
        return Promise.resolve(fn(context, function dispatch(1) { 
            return Promise.resolve(fn(context, function dispatch(2) { 
                return Promise.resolve() 
            })) 
        })) 
    } 
  } 
}

这种递归的方式类似执行栈,先进先出

执行栈

这里要多思考一下,递归的使用,对 Promise.resolve 不用太在意

上下文的处理

上下文的处理即调用了 createContext

createContext(req, res) { 
    const context = Object.create(this.context) 
    const request = (context.request = Object.create(this.request)) 
    const response = (context.response = Object.create(this.response)) 
    context.app = request.app = response.app = this 
    context.req = request.req = response.req = req 
    context.res = request.res = response.res = res 
    request.ctx = response.ctx = context 
    request.response = response 
    response.request = request 
    context.originalUrl = request.originalUrl = req.url 
    context.state = {} 
    return context 
}

传入原生的 request 和 response,返回一个 上下文——context,代码很清晰,不解释

res 的特殊处理

callback 中是先执行 this.createContext,拿到上下文后,再去执行 handleRequest,先看代码:

handleRequest(ctx, fnMiddleware) { 
    const res = ctx.res 
    res.statusCode = 404 
    const onerror = (err) => ctx.onerror(err) 
    const handleResponse = () => respond(ctx) 
    onFinished(res, onerror) 
    return fnMiddleware(ctx).then(handleResponse).catch(onerror) 
}

一切都清晰了

const Koa = require('Koa'); 
const app = new Koa(); 
 
console.log('app', app); 
app.use((ctx, next) => { 
    ctx.body = 'hello world'; 
}); 
app.listen(3000, () => { 
    console.log('3000请求成功'); 
});

这样一段代码,实例化后,获得了 this.middleware、this.context、this.request、this.response 四大将,你使用 app.use() 时,将其中的函数推到 this.middleware。再使用 app.listen() 时,相当于起了一个 HTTP 服务,它合并了中间件,获取了上下文,并对 res 进行了特殊处理

错误处理

onerror(err) { 
    if (!(err instanceof Error)) 
        throw new TypeError(util.format('non-error thrown: %j', err)) 
 
    if (404 == err.status || err.expose) return 
    if (this.silent) return 
 
    const msg = err.stack || err.toString() 
    console.error() 
    console.error(msg.replace(/^/gm, '  ')) 
    console.error() 
}

context.js

引入我眼帘的是两个东西

// 1. 
const proto = module.exports = { 
    inspect(){...}, 
    toJSON(){...}, 
    ... 
} 
// 2. 
delegate(proto, 'response') 
  .method('attachment') 
  .access('status') 
  ...

第一个可以理解为,const proto = { inspect() {...} ...},并且 module.exports 导出这个对象

第二个可以这么看,delegate 就是代理,这是为了方便开发者而设计的

// 将内部对象 response 的属性,委托至暴露在外的 proto 上 
delegate(proto, 'response') 
  .method('redirect') 
  .method('vary') 
  .access('status') 
  .access('body') 
  .getter('headerSent') 
  .getter('writable'); 
  ...

而使用 delegate(proto, 'response').access('status')...,就是在 context.js 导出的文件,把 proto.response 上的各个参数都代理到 proto 上,那 proto.response 是什么?就是 context.response,context.response 哪来的?

回顾一下, 在 createContext 中

createContext(req, res) { 
    const context = Object.create(this.context) 
    const request = (context.request = Object.create(this.request)) 
    const response = (context.response = Object.create(this.response)) 
    ... 
}

context.response 有了,就明了了, context.response = this.response,因为 delegate,所以 context.response 上的参数代理到了 context 上了,举个例子

  • ctx.header 是 ctx.request.header 上代理的
  • ctx.body 是 ctx.response.body 上代理的

request.js 和 response.js

一个处理请求对象,一个处理返回对象,基本上是对原生 req、res 的简化处理,大量使用了 ES6 中的 get 和 post 语法

大概就是这样,了解了这么多,怎么手写一个 Koa2 呢,请看下一篇——手写 Koa2

参考资料


评论关闭
IT虾米网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!

发现一款香到可以替代postman的API管理工具apipost