资讯专栏INFORMATION COLUMN

Koa2源码阅读笔记

plus2047 / 2485人阅读

摘要:引言最近空闲时间读了一下的源码在阅读的源码的过程中,我的感受是代码简洁思路清晰不得不佩服大神的水平。调用的时候就跟有区别使用必须使用来调用除了上面的的构造函数外,还暴露了一些公用的,比如两个常见的,一个是,一个是。

引言

最近空闲时间读了一下Koa2的源码;在阅读Koa2(version 2.2.0)的源码的过程中,我的感受是代码简洁、思路清晰(不得不佩服大神的水平)。
下面是我读完之后的一些感受。

Koa的设计理念

Koa 是一个轻量级的、极富表现力的 http 框架。
一个web request会通过 Koa 的中间件栈,来动态完成 response 的处理。
Koa2 采用了 async 和 await 的语法来增强中间件的表现力。
Koa 不在内核方法中绑定任何中间件,它仅仅提供了一个轻量优雅的函数库。

Koa基本组成

Koa源码非常精简,只有四个文件:

application.js:框架入口;负责管理中间件,以及处理请求

context.js:context对象的原型,代理request与response对象上的方法和属性

request.js:request对象的原型,提供请求相关的方法和属性

response.js:response对象的原型,提供响应相关的方法和属性

application.js
// application.js

module.exports = class Application extends Emitter {
  constructor() {
    super();

    this.proxy = false; // 是否信任 proxy header 参数,默认为 false
    
    this.middleware = []; //保存通过app.use(middleware)注册的中间件
    
    this.subdomainOffset = 2; // 子域默认偏移量,默认为 2
    
    this.env = process.env.NODE_ENV || "development"; // 环境参数,默认为 NODE_ENV 或 ‘development’
    
    this.context = Object.create(context); //context模块,通过context.js创建
    
    this.request = Object.create(request); //request模块,通过request.js创建
    
    this.response = Object.create(response); //response模块,通过response.js创建
  }

  // ...
}

application.js 是 koa 的入口主要文件,暴露应用的 class, 这个 class 继承自 EventEmitter ,这里可以看出跟 koa1.x 的不同,koa1.x 是用的是构造函数的方式,koa2 大量使用 es6 的语法。调用的时候就跟 koa1.x 有区别

var koa = require("koa");
// koa 1.x
var app = koa();
// koa 2.x
// 使用class必须使用new来调用
var app = new koa();

application.js除了上面的的构造函数外,还暴露了一些公用的api,比如两个常见的,一个是listen,一个是use

use函数
// application.js

use(fn) {
  if (typeof fn !== "function") throw new TypeError("middleware must be a function!");
  if (isGeneratorFunction(fn)) {
    deprecate("Support for generators will be removed in v3. " +
              "See the documentation for examples of how to convert old middleware " +
              "https://github.com/koajs/koa/blob/master/docs/migration.md");
    fn = convert(fn);
  }
  debug("use %s", fn._name || fn.name || "-");
  this.middleware.push(fn);
  return this;
}

use函数做的事很简单:注册一个中间件fn,其实就是将fn放入middleware数组。

listen函数
// application.js

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

listen方法首先会通过this.callback方法来返回一个函数作为http.createServer的回调函数,然后进行监听。我们已经知道,http.createServer的回调函数接收两个参数:reqres,下面来看this.callback的实现:

// application.js

callback() {
  const fn = compose(this.middleware);
    
  if (!this.listeners("error").length) this.on("error", this.onerror);
    
  const handleRequest = (req, res) => {
    res.statusCode = 404;
    const ctx = this.createContext(req, res);
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fn(ctx).then(handleResponse).catch(onerror);
  };
    
  return handleRequest;
}

首先,callback方法把所有middleware进行了组合,使用了koa-compose,我们来看一下koa-compose的代码:

// koa-compose

function compose (middleware) {
// 传入的middleware必须是一个数组
  if (!Array.isArray(middleware)) throw new TypeError("Middleware stack must be an array!")
// 传入的middleware的每一个元素都必须是函数
  for (const fn of middleware) {
    if (typeof fn !== "function") throw new TypeError("Middleware must be composed of functions!")
  }

  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]
      //下面两行代码是处理最后一个中间件还有next的情况的,其实就是直接resolve出来
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve() 
      try {
        // 这里就是传入next执行中间件代码了
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

可以看到koa-compose基本就是个dispatch函数的递归调用。其中最重要的就是下面这段代码:

return Promise.resolve(fn(context, function next () {
  return dispatch(i + 1)
}))

这段代码等价于:

fn(context, function next () {
 return dispatch(i + 1)
})
return Promise.resolve()

这里middlewareFunction的第二个参数(也就是next)是动态传递进去的信使,它会调取dispatch(index)执行下一个的middleware。最后会返回一个Resolved(已完成)状态的Promise对象。这个对象的作用我们稍后再说。

我们先暂时回到callback方法里面,前面说了它先对middleware进行了组合,生成了一个函数fn
然后,callback方法返回http.createServer所需要的回调函数handleRequest

handleRequest函数,先把http code默认设置为404,接着利用createContext函数把node返回的req和res进行了封装创建出context
然后通过onFinished(res, onerror)监听http response,当请求结束时执行回调。这里传入的回调是context.onerror(err),即当错误发生时才执行。
最后返回 fn(ctx).then(handleResponse).catch(onerror)的执行结果,这里的fn函数就是就是组合所有middleware后生成的函数,调用它执行所有middleware后会返回前面提到的Resolved(已完成)状态的Promise对象,之后执行响应处理函数respond(ctx)respond函数里面也主要是一些收尾工作,例如判断http code为空如何输出啦,http method是head如何输出啦,body返回是流或json时如何输出;代码就不贴了,感兴趣的小伙伴自己可以去看一下),当抛出异常时同样使用 context.onerror(err)处理。

我们可以看看createContext函数

// application.js

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.cookies = new Cookies(req, res, {
    keys: this.keys,
    secure: request.secure
  });
  request.ip = request.ips[0] || req.socket.remoteAddress || "";
  context.accept = request.accept = accepts(req);
  context.state = {};
  return context;
}

createContext创建context的时候,还会同时创建request和response,通过下图可以比较直观地看到所有这些对象之间的关系。

图中:

最左边一列表示每个文件的导出对象

中间一列表示每个Koa应用及其维护的属性

右边两列表示对应每个请求所维护的一些列对象

黑色的线表示实例化

红色的线表示原型链

蓝色的线表示属性

通过上面的分析,我们已经可以大概得知Koa处理请求的过程:当请求到来的时候,会通过 req 和 res 来创建一个 context (ctx) ,然后执行中间件。

content.js

content.js 主要的功能提供了对requestresponse对象的方法与属性便捷访问能力。
其中使用了node-delegates(有兴趣的可以看一下源码),将context.requestcontext.response上的方法与属性代理到context上。
在源码中,我们可以看到:

// context.js

delegate(proto, "response")
  .method("attachment")
  // ...
  .access("status")
  // ...
  .getter("writable");

delegate(proto, "request")
  .method("acceptsLanguages")
  // ...
  .access("querystring")
  // ...
  .getter("ip");
request.js

request.js 封装了请求相关的属性以及方法。通过 application.js 中的createContext方法,代理对应的 request 对象。

const request = context.request = Object.create(this.request);
// ...
context.req = request.req = response.req = req;
// ...
request.response = response;

request.req为原生的请求对象,在 request.js 中属性的获取都是通过 ths.req来获取的(即 request.req)。

response.js

response.js 封装了响应相关的属性以及方法。与 request 相同,通过createContext方法代理对应的 response 对象。

const response = context.response = Object.create(this.response);
// ...
context.res = request.res = response.res = res;
// ...
response.request = request;
结语

关于Koa2的源码就先分析到这,希望对大家有所帮助。
如有不同的看法,欢迎交流!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/83586.html

相关文章

  • Koa源码阅读笔记(4) -- ctx对象

    摘要:本笔记共四篇源码阅读笔记源码阅读笔记源码阅读笔记服务器启动与请求处理源码阅读笔记对象起因前两天终于把自己一直想读的源代码读了一遍。首先放上关键的源代码在上一篇源码阅读笔记服务器启动与请求处理中,我们已经分析了的作用。 本笔记共四篇Koa源码阅读笔记(1) -- coKoa源码阅读笔记(2) -- composeKoa源码阅读笔记(3) -- 服务器の启动与请求处理Koa源码阅读笔记(4...

    ityouknow 评论0 收藏0
  • Koa源码阅读笔记(2) -- compose

    摘要:于是抱着知其然也要知其所以然的想法,开始阅读的源代码。问题读源代码时,自然是带着诸多问题的。源代码如下在被处理完后,每当有新请求,便会调用,去处理请求。接下来会继续写一些阅读笔记,因为看的源代码确实是获益匪浅。 本笔记共四篇Koa源码阅读笔记(1) -- coKoa源码阅读笔记(2) -- composeKoa源码阅读笔记(3) -- 服务器の启动与请求处理Koa源码阅读笔记(4) -...

    roland_reed 评论0 收藏0
  • Koa源码阅读笔记(3) -- 服务器の启动与请求处理

    摘要:本笔记共四篇源码阅读笔记源码阅读笔记源码阅读笔记服务器启动与请求处理源码阅读笔记对象起因前两天阅读了的基础,和中间件的基础。的前端乐园原文链接源码阅读笔记服务器启动与请求处理 本笔记共四篇Koa源码阅读笔记(1) -- coKoa源码阅读笔记(2) -- composeKoa源码阅读笔记(3) -- 服务器の启动与请求处理Koa源码阅读笔记(4) -- ctx对象 起因 前两天阅读了K...

    mrcode 评论0 收藏0
  • 教你从写一个迷你koa-router到阅读koa-router源码

    摘要:本打算教一步步实现,因为要解释的太多了,所以先简化成版本,从实现部分功能到阅读源码,希望能让你好理解一些。 本打算教一步步实现koa-router,因为要解释的太多了,所以先简化成mini版本,从实现部分功能到阅读源码,希望能让你好理解一些。希望你之前有读过koa源码,没有的话,给你链接 最核心需求-路由匹配 router最重要的就是路由匹配,我们就从最核心的入手 router.get...

    yzzz 评论0 收藏0
  • koa源码阅读[1]-koa与koa-compose

    摘要:接上次挖的坑,对相关的源码进行分析第一篇。和同为一批人进行开发,与相比,显得非常的迷你。在接收到一个请求后,会拿之前提到的与来创建本次请求所使用的上下文。以及如果没有手动指定,会默认指定为。 接上次挖的坑,对koa2.x相关的源码进行分析 第一篇。 不得不说,koa是一个很轻量、很优雅的http框架,尤其是在2.x以后移除了co的引入,使其代码变得更为清晰。 express和ko...

    vibiu 评论0 收藏0

发表评论

0条评论

plus2047

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<