从浅入深了解Koa2源码

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 从浅入深了解Koa2源码

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


简单回顾下



什么是 koa2


  1. NodeJS 的 web 开发框架


  1. Koa 可被视为 nodejs 的 HTTP 模块的抽象


源码重点


中间件机制


洋葱模型


compose


源码结构



Koa2 的源码地址:https://github.com/koajs/koa


其中 lib 为其源码


image.png


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


application



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


image.png


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


image.png


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


image.png


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


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


image.png


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


打断点看原型

image.png


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


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 的源码地址:https://github.com/koajs/compose。这代码已经三年没动了,稳的一逼


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);
}


image.png


image.png


逐行解析 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()
            }))
        }))
    }
  }
}


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


image.png


这里要多思考一下,递归的使用,对 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


参考资料



KOA2 框架原理解析和实现[2]


可能是目前最全的 koa 源码解析指南[3]



[1] Koa2 的基础: https://fe.azhubaby.com/Koa2/Koa2%E5%9F%BA%E7%A1%80.html


[2] KOA2 框架原理解析和实现: https://segmentfault.com/a/1190000018488597


[3] 可能是目前最全的 koa 源码解析指南: https://developers.weixin.qq.com/community/develop/article/doc/0000e4c9290bc069f3380e7645b813


相关文章
|
存储 前端开发 JavaScript
AntV X6源码探究简析
AntV是蚂蚁金服全新一代数据可视化解决方案,其中X6主要用于解决图编辑领域相关的解决方案,其是一款图编辑引擎,内置了一下编辑器所需的功能及组件等,本文旨在通过简要分析x6源码来对图编辑领域的一些底层引擎进行一个大致了解,同时也为团队中需要进行基于X6编辑引擎进行构建的图编辑器提供一些侧面了解,在碰到问题时可以较快的找到问题点。
400 0
|
7月前
|
缓存 JavaScript 前端开发
vue核心面试题汇总【查缺补漏】(二)
vue核心面试题汇总【查缺补漏】(二)
|
7月前
|
缓存 移动开发 JavaScript
vue核心面试题汇总【查缺补漏】(一)
vue核心面试题汇总【查缺补漏】(一)
136 0
|
7月前
|
JavaScript 前端开发 安全
【面试题】 TypeScript 前端面试题 由浅到深(一)
【面试题】 TypeScript 前端面试题 由浅到深(一)
|
7月前
|
JavaScript 前端开发 安全
【面试题】 TypeScript 前端面试题 由浅到深(二)
【面试题】 TypeScript 前端面试题 由浅到深(二)
|
7月前
|
前端开发 中间件 索引
【源码共读】洋葱模型 koa-compose
【源码共读】洋葱模型 koa-compose
65 0
|
存储 前端开发 关系型数据库
[后端浅了解]web开发合作
[后端浅了解]web开发合作
|
前端开发
前端学习笔记202307学习笔记第五十七天-模拟面试笔记react-前端洋葱圈模型
前端学习笔记202307学习笔记第五十七天-模拟面试笔记react-前端洋葱圈模型
79 0
|
存储 前端开发 JavaScript
人人都是前端架构师:我来带你阅读 React18 源码!
人人都是前端架构师:我来带你阅读 React18 源码!
463 0
|
JSON 前端开发 数据可视化
umi3源码探究简析
作为蚂蚁金服整个生态圈最为核心的部分,umi可谓是王冠上的红宝石,因而个人认为对于整个umi架构内核的学习及设计哲学的理解,可能比如何使用要来的更为重要;作为一个使用者,希望能从各位大佬的源码中汲取一些养分以及获得一些灵感
238 0