# koa源码分析-从入门到看不懂

# 前言

本文用来记录一下自己学习koa的时候阅读源码和相关学习资料的心得和总结,但是写的很乱,大致的意思没有体现出来,不建议阅读下去,如果想更加清楚的理解koa的核心源码,可以移步下一章节,自己实现一个mini版的koa,这会让你对源码的认知提升到一个很好的地步。

# koa是什么?

引用官网的话,koa是基于Node.js平台的下一代web开发框架,致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

用简单的话来总结就是:

1、基于node原生req和res为request和response对象赋能,并基于它们封装成一个context对象。

2、基于async/await(generator)的中间件洋葱模型机制。

# 基本用法




 









const Koa = require('koa')

const app = new Koa()

app.use(async(ctx, next) => {
    ctx.body = 'hello world'
    next()
})

app.listen(5050, () => {
    console.log('sever start')
})

以上是最简单的 hello world, 通过代码我们大致可以发现koa的使用并不难,而且koa的源码也并不多,但是设计的很抽象简洁,刚开始阅读的时候还是有点难接受的。

以下是网上借鉴来的一张koa结构示意图,可以更加直观的了解到koa的结构。

An image

# 源码结构

如果你看了koa的源码,你会发现koa源码其实很简单,就4个文件

── lib
   ├── application.js
   ├── context.js
   ├── request.js
   └── response.js

结合示意图,发现这个结构很简单,其中 context 、request 和 response 就是 3 个字面量形式创建的简单对象,上面封装了一些列方法(其实绝大部分是属性的赋值器(setter)和取值器(getter))。

这4个文件也就对应着koa的4个对象

── lib
   ├── new Koa()  || ctx.app
   ├── ctx
   ├── ctx.req  || ctx.request
   └── ctx.res  || ctx.response

那我们先对这4个文件进行初步的认识。

# application.js

从koa的package.json中我们发现,application.js是koa的入口,也是koa的核心所在。




 




{
  "name": "koa",
  "version": "2.7.0",
  "description": "Koa web app framework",
  "main": "lib/application.js",
  ---
}

下面对核心代码进行注释




 








































































































/**
 * 依赖模块,包括但不止于下面的,只列出核心需要关注的内容
 */
const response = require('./response');
const compose = require('koa-compose');
const context = require('./context');
const request = require('./request');
const Emitter = require('events');

/**
 * `Application`继承于`Emitter`,说明具有异步处理的能力
 */
module.exports = class Application extends Emitter {
  constructor() {
    super();

    this.middleware = [];//存放中间件函数
    // context 、request 和 response 就是 3 个字面量形式创建的简单对象,他们将作为app的相应属性的原型
    // Object.create() 让两者不会指向同一片内存,不会相互影响。
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }

/**
 * 创建服务器
 */
  listen(...args) {
    // 调用原生的node创建服务器的方法起一个服务
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

/**
 * use函数用于将中间件函数收集起来,存放在middleware中
 */
  use(fn) {
    // 兼容koa1 的写法,因为koa1主要使用generator,而koa2主要使用 async await
    if (isGeneratorFunction(fn)) {
      fn = convert(fn);
    }
    // 将中间件函数收集起来,存放在middleware中
    this.middleware.push(fn);
    // 返回当前实例,支持链式调用
    return this;
  }

/**
 * http.createServer的参数,返回一个类似于 (req, res) => {} 的函数,作为服务请求的处理函数
 */
  callback() {
    // 将所有use函数收集的中间件函数集合形成超集,实现洋葱模型中间件机制
    const fn = compose(this.middleware);
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    const handleRequest = (req, res) => {
      // 基于req、res封装出更强大的ctx超集
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
    return 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);
  }

/**
 * 将req res 进行组合,形成强大的ctx
 */ 
  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;
  }

/**
 * 错误处理函数
 */
  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();
  }
};

通过application.js的初步解读,我们可以总结一下application.js的作用 1、框架入口

2、实现洋葱模型的中间件机制

3、将原生req、res处理成一个强大的超集ctx

4、错误的统一处理

# context.js




 






























const util = require('util');
const createError = require('http-errors');
const httpAssert = require('http-assert');
const delegate = require('delegates');

const proto = module.exports = {
  // 省略了一些不甚重要的函数
  onerror(err) {
    // 触发application实例的error事件
    this.app.emit('error', err, this);
  },
};

/*
 在application.createContext函数中,
 被创建的context对象会挂载基于request.js实现的request对象和基于response.js实现的response对象。
 下面2个delegate的作用是让context对象代理request和response的部分属性和方法
*/
delegate(proto, 'response')
  .method('attachment')
  ...
  .access('status')
  ...
  .getter('writable')
  ...;

delegate(proto, 'request')
  .method('acceptsLanguages')
  ...
  .access('querystring')
  ...
  .getter('origin')
  ...;

从代码中我们可以总结context的作用: 1、错误的处理 2、代理request、response的属性和方法

# request.js 和 response.js




 











module.exports = {
  
  // 在application.js的createContext函数中,会把node原生的req作为request对象(即request.js封装的对象)的属性
  // request对象会基于req封装很多便利的属性和方法
  get header() {
    return this.req.headers;
  },

  set header(val) {
    this.req.headers = val;
  },

  // 省略了大量类似的工具属性和方法
};

request对象基于node原生req封装了一系列便利属性和方法,也提供了一些原生上没有的属性和方法,如 path

所以当你访问ctx.request.xxx的时候,实际上是在访问request对象上的赋值器(setter)和取值器(getter)。

response对象和request对象类似,但是需要注意的是返回的body支持Buffer、Stream、String以及最常见的json

# 深入了解源码机制

上文中我们对koa的源码结构和对应文件的功能做了一些小的总结,下面我们从初始化、启动应用、中间件处理、错误处理来更加深入了解一下koa。

# 初始化




const Koa = require('koa');
const app = new Koa();

首先我们创建了 Koa 的实例 app,其构造函数十分简单,如下:




 








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

结合上文中的示意图,在创建实例的时候context request response分别被初始化,也在实例上挂载了一些常用的如 subdomainOffset env 等属性

我们可以总结一下,koa在初始化的时候,koa获得处理异步事件的能力,并且挂载一些属性和方法。

# 启动应用




 
app.listen(5050, () => {
    console.log('sever start')
})

app.listen()做的事件也很简单, 我们已经知道,通过向 http.createServer 创建一个服务,部分源码如下

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

唯一需要我们关注的就是这个 this.callback ,也是理解 koa 应用的核心所在。

this.callback() 执行返回一个类似于 (req, res) => {} 的函数

那么这个函数具体是怎么做的呢?首先,它基于 req 和 res 封装出我们中间件所使用的 ctx 对象,再将 ctx 传递给中间件所组合成的一个嵌套函数。中间件组合的嵌套函数返回的是一个 Promise 的实例,等到这个组合函数执行完( resolve ),通过 ctx 中的信息(例如 ctx.body )想 res 中写入数据,执行过程中出错 (reject),这调用默认的错误处理函数。

原理还是很简单,看一下代码:




 







callback() {
  const fn = compose(this.middleware);
  return (req, res) => {
    res.statusCode = 404;
    const ctx = this.createContext(req, res);
    const onerror = err => ctx.onerror(err);
    onFinished(res, onerror);
    fn(ctx).then(() => respond(ctx)).catch(onerror);
  };
}

就像我们阅读源码发现时候一样,通过 compose方法对中间件函数组合成一个大的嵌套函数供后序执行的时候调用

createContext根据 req 和 res 封装中间件所需要的 ctx




 









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;
  // 省略一点无关紧要的代码
  return context;
}

简单的说就是创建了3个简单的对象,并且将他们的原型指定为我们 app 中对应的对象。然后将原生的 req 和 res 赋值给相应的属性,这也是为何以下结构得到的结果是一样的原因。




 

    console.log(ctx.req.url); 
    console.log(ctx.request.req.url);
    console.log(ctx.request.url);
    console.log(ctx.url); // ctx.request.url

如我们示意图,整个 Koa 的结构就完整了。

但是,ctx 上不是暴露出来很多属性吗?它们在哪?他们就在我们示意图的最右边,一开始我们略过的 3 个简单对象。通过原型链的形式,我们 ctx.request 所能访问属性和方法绝大部分都在其对应的 request 这个简单的对象上面。request 又是怎么封装的呢?我只需要简单的贴一点源码,大家就秒懂了。

module.exports = {
  //...
  get method() {
    return this.req.method;
  },

  set method(val) {
    this.req.method = val;
  }
  //...
}

# 中间件处理

---略

# 错误处理

---略

# 参考文章

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

十分钟带你看完 KOA 源码