资讯专栏INFORMATION COLUMN

koa源码分析系列(一)

Atom / 286人阅读

摘要:很明显是一个构造函数。默认为根据原生的对象生成一个对象回调函数处理服务器响应可以看到,方法返回的函数就是方法所需要的回调函数。

koa 是什么这里不介绍了,这里通过一个小例子结合源码讲一讲它的实现。

koa 源码结构

通过 npm 安装 koa(v2.2.0) 后,代码都在 lib 文件夹内,包括 4 个文件,application.js, context.js, request.js, response.js。

application.js 包含 app 的构造以及启动一个服务器

context.js app 的 context 对象, 传入中间件的上下文对象

request.js app 的请求对象,包含请求相关的一些属性

response.js app 的响应对象,包含响应相关的一些属性

本文主要关于 application.js 。

先看一个最简单的例子

// app.js
const Koa = require("koa")
const app = new Koa()

app.use(ctx => {
    ctx.body = "hello world"
})

app.listen(3000)

然后通过 node app.js 启动应用,一个最简单的 koa 服务器就搭建好了,浏览器访问 http://localhost:3000,服务器返回一个 hello world 的响应主体。

源码分析

接下来通过源码看看这个服务器是怎么启动的。

const app = new Koa(), 很明显 Koa 是一个构造函数。

module.exports = class Application extends Emitter {}

Application 类继承了 nodejs 的 Events 类,从而可以监听以及触发事件。

看一下构造函数的实现。

constructor() {
    super();

    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || "development";
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);

构造函数定义了一些 app 的实例属性,包括 proxy, middleware, subdomainOffset, env, context, request, response等。

至此 我们就生成了一个 app 的koa实例。

接下来就该用 app.use(middleware) 来使用中间件了。

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

首先会验证传入的参数是否是一个函数。如果不是一个函数,会报错。之后如果传入的函数是一个generator 函数,那么会将这个函数转化为一个 async 函数。使用的是 koa-convert 模块, 这是一个很重要的模块,能将很多 koa1 时代下的中间件转化为 koa2 下可用的中间件。并且注意到

Support for generators will be removed in v3.

在 koa3 中,将默认不支持 generator 函数作为中间件。

之后将传入的中间件函数推入 middleware 数组中,并且返回 this 以便链式调用。

app.use() 只是定义了一些要使用的中间件,并将它们放入 middleware 数组中,那么怎么使用这些中间件。来看看 app.listen 方法。

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

app.listen 算是 node 原生 listen 方法的语法糖。通过 app.callback 方法生成一个 http.createServer 方法所需要的回调函数,然后再调用原生 http server 的 listen 方法。事实上也可以发现,app 的 listen 方法接收 http server 的 listen 方法一样的参数。

那么再看看 app 的 callback 这个方法了,也是最重要的一个方法。

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

    if (!this.listeners("error").length) this.on("error", this.onerror);

    const handleRequest = (req, res) => {
      res.statusCode = 404; // 默认为 404 
      const ctx = this.createContext(req, res);
      // 根据 node.js 原生的 req, res 对象生成一个 ctx 对象
      const onerror = err => ctx.onerror(err);
      // onerror 回调函数
      const handleResponse = () => respond(ctx);
      // 处理服务器响应
      onFinished(res, onerror);
      return fn(ctx).then(handleResponse).catch(onerror);
    };

    return handleRequest;
  }

可以看到,callback 方法返回的 handleRequest 函数就是 http.createServer 方法所需要的回调函数。

callback 函数内,首先通过 koa-compose 模块将所有的中间件合并成一个中间件函数,以供 app.use 方法调用。随后监听一个 error 事件,onerror 作为默认的错误处理函数。

onerror(err) {
    assert(err instanceof Error, `non-error thrown: ${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();
  }

onerror 函数只是仅仅输出 error.stack 作为错误信息。

handleRequest 函数内完成了对请求的处理以及对响应结果的返回。首先 app.createContext 方法生成一个 ctx 供中间件函数 fn 调用。

createContext(req, res) {
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    // request 属性
    const response = context.response = Object.create(this.response);
    // response 属性
    context.app = request.app = response.app = this;
    // request 和 response 上获得 app 属性,指向这个 app 实例
    context.req = request.req = response.req = req;
    // req 属性,req 是原生 node 的请求对象
    context.res = request.res = response.res = res;
    // res 属性,res 是原生 node 的响应对象
    request.ctx = response.ctx = context;
    // request 和 response 上获得 ctx 属性,指向 context 对象
    request.response = response;
    response.request = request;
    // request 和 response 互相指向对方
    context.originalUrl = request.originalUrl = req.url;
    // 获得 originalUrl 属性,为原生 req 对象的 url 属性
    context.cookies = new Cookies(req, res, {
      keys: this.keys,
      secure: request.secure
    }); // cookie 属性
    request.ip = request.ips[0] || req.socket.remoteAddress || "";
    // ip 属性
    context.accept = request.accept = accepts(req);
    // accept 属性,是个方法,用于判断 Content-Type 
    context.state = {};
    // context.state 属性,用于保存一次请求中所需要的其他信息
    return context;
  }

所以,createContext 方法将一些常用的属性,如 resquest , response, node 原生 req, res 挂载到 context 对象上。

再来看这句话 const handleResponse = () => respond(ctx).

function respond(ctx) {
  // allow bypassing koa
  if (false === ctx.respond) return;

  const res = ctx.res;
  if (!ctx.writable) return;

  let body = ctx.body; // 响应主体
  const code = ctx.status; // 响应状态码

  // ignore body
  if (statuses.empty[code]) { // 这里具体是指 204 205 304 三种
    // strip headers
    ctx.body = null;
    return res.end();
  }

  if ("HEAD" == ctx.method) { // 如果是 `HEAD` 方法
    if (!res.headersSent && isJSON(body)) {
      ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }
  // status body
  if (null == body) { // 如果没有设置 body , 设置ctx.message 为 body。当然默认是 Not Found ,因为 status 默认是 404
    body = ctx.message || String(code);
    if (!res.headersSent) {
      ctx.type = "text";
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }
  // 以下根据 body 的类型,决定响应结果
  // responses
  if (Buffer.isBuffer(body)) return res.end(body);
  if ("string" == typeof body) return res.end(body);
  if (body instanceof Stream) return body.pipe(res);

  // body: json
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}

所以 respond 函数的作用就是,根据传入的 ctx 对象的 body ,method 属性来决定对 request 处理的方式以及如何 response。

onFinished(res, onerror)

首先看看 onFinished(res, listener) 函数的介绍

Attach a listener to listen for the response to finish. The listener will be invoked only once when the response finished. If the response finished to an error, the first argument will contain the error. If the response has already finished, the listener will be invoked.

也就是当服务端响应完成后,执行 listener 回调函数,如果响应过程中有错误发生,那么 error 对象将作为 listen 回调函数的第一个参数,因此 onFinished(res, onerror) 表示 当 koa 服务器发送完响应后,如果有错误发生,执行 onerror 这个回调函数。

return fn(ctx).then(handleResponse).catch(onerror)。来看看这一句,fn 之前说过了,是所有的中间件函数的 “集合”, 用这一个中间件来表示整个处理过程。同时 fn 也是一个 async 函数,执行结果返回一个 promise 对象,同时 handleResponse 作为其 resolved 函数,onerror 是 rejected 函数。

总结

总结一下,application.js 描述了 一个 koa 服务器(实例)生成的整个过程。

new Koa() 生成了一个 koa 实例

app.use(middleware) 定义了这个 app 要使用的中间件

app.listen 方法,通过 callback 将合并后的中间件函数转化成一个用于 http server.listen 调用的回调函数,之后调用原生的 server.listen 方法。

全文完

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

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

相关文章

  • B站Up主-山地人-这位老哥2019年的前端自学计划进展如何?——讲个B站Up主自学前端85天的故

    摘要:前言自从上次在掘金发布年山地人的前端完整自学计划讲一个站主山地人的天前端自学故事以来,一眨眼山地人老哥在站做主已经有天了。所以这个体系里的一些框架包括也是山地人年自学计划的一部分。月底,山地人老哥开启了的两个专题。 前言 自从上次在掘金发布【2019年山地人的前端完整自学计划——讲一个B站UP主山地人的40天前端自学故事】 以来,一眨眼山地人老哥在B站做Up主已经有85天了。 时隔一个...

    cocopeak 评论0 收藏0
  • 中间件执行模块koa-Compose源码分析

    摘要:原文博客地址,欢迎学习交流点击预览读了下的源码,写的相当的精简,遇到处理中间件执行的模块决定学习一下这个模块的源码。当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件恢复执行其上游行为。 原文博客地址,欢迎学习交流:点击预览 读了下Koa的源码,写的相当的精简,遇到处理中间件执行的模块koa-Compose,决定学习一下这个模块的源码。 阅读本文可以学到: Koa中间件的加载...

    imtianx 评论0 收藏0
  • express分析和对比

    摘要:前言目前最新版本是所以本文分析也基于这个版本。源码分析直接切入主题由于目前是一个独立的路由和中间件框架。所以分析的方向也以这两个为主。源码去年的时候有分析过现在对比分析思考下。 前言 目前express最新版本是4.16.2,所以本文分析也基于这个版本。目前从npm仓库上来看express使用量挺高的,express月下载量约为koa的40倍。所以目前研究下express还是有一定意义...

    mmy123456 评论0 收藏0
  • Express与Koa中间件机制分析

    摘要:目前使用人数众多。通过利用函数,帮你丢弃回调函数,并有力地增强错误处理。这个系列的博客主要讲解和的中间件机制,本篇将主要讲解的中间件机制。其中间件机制的核心为内部方法的实现。 提到 Node.js 开发,不得不提目前炙手可热的2大框架 Express 和 Koa。 Express 是一个保持最小规模的灵活的 Node.js Web 应用程序开发框架,为 Web 和移动应用程序提供一组...

    zilu 评论0 收藏0
  • 深入探析koa之中间件流程控制篇

    摘要:到此为止,我们就基本讲清楚了中的中间件洋葱模型是如何自动执行的。 koa被认为是第二代web后端开发框架,相比于前代express而言,其最大的特色无疑就是解决了回调金字塔的问题,让异步的写法更加的简洁。在使用koa的过程中,其实一直比较好奇koa内部的实现机理。最近终于有空,比较深入的研究了一下koa一些原理,在这里会写一系列文章来记录一下我的学习心得和理解。 在我看来,koa最核心...

    fuchenxuan 评论0 收藏0

发表评论

0条评论

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