koa 源码解析

koa 框架是基于 Node.js 下一代的 web server 框架, 舍弃了回调写法, 提高了错误处理效率, 而且其不绑定任何中间件, 核心代码只提供优雅轻量的函数库.

平时经常使用到 koa 框架, 所以希望通过阅读源码学习其思想, 本文是基于 koa2 的源码进行分析.

koa 整体架构

koa 框架的源码结构非常简单, 在 lib 文件夹下, 只有 4 个文件, 分别是application.js, context.js, request.js, response.js.

  • application.js 是 koa 框架的入口文件;
  • context.js 的作用是创建网络请求的上下文对象;
  • request.js 是用于包装 koa 的 request 对象的;
  • response.js则是用于包装 koa 的 response 对象的.

我们这里使用 koa 框架建立一个简单的 node 服务, 以此来逐步了解 koa 内部机理.

const koa = require('koa');
​
const app = new koa();
​
app.use(async (ctx, next) {
 ctx.body = 'Hello World';
});
​
app.listen(3000);

上面的代码, 先生成了一个 koa 对象, 然后通过使用 use 函数往 server 中添加中间件函数, 最后使用 listen 函数进行对 3000 端口的监听.

koa 源码剖析

由上面的简单代码, 我们会有几个疑问: koa 对象中包含了些什么属性与方法? use 函数对于中间件函数的处理是怎么样的? listen 函数做了什么?

因此我们先来看一下 application.js 的源码:

application.js

application.js 暴露了一个 Application 类供我们使用, 也即是说, 我们 new 一个 koa 对象实质上就是新建一个 Application 的实例对象. 而 Application 类是继承于 EventEmitter (Node.js events 模块)的, 所以我们在 koa 实例对象上可以使用 on, emit 等方法进行事件监听.

构造函数

constructor() {
 super();   // 因为继承于 EventEmitter, 这里需要调用 super
 this.proxy = false;    // 代理设置
 this.middleware = [];  // 存储中间件的list
 this.subdomainOffset = 2;   // 子域名偏移设置
 this.env = process.env.NODE_ENV || 'development';   // node 环境变量
 this.context = Object.create(context);
 this.request = Object.create(request);
 this.response = Object.create(response);
 if (util.inspect.custom) {
    this[util.inspect.custom] = this.inspect;
 }
}

可以看到在 constructor 函数中, 实例对象会初始化几个重要的属性,

  • proxy 属性是代理设置;
  • middleware 属性是中间件数组, 用于存储中间件函数的;
  • subdomainOffset 属性是子域名偏移量设置;
  • env 属性保存 node 的环境变量 NODE_ENV 值;
  • context, requets, response 则是 koa 自身的包装的 context 对象, request 对象, response 对象.

这里特别讲解一下 proxy 属性与subdomainOffset 属性. proxy 属性值是 true 或者 false, 它的作用在于是否获取真正的客户端 ip 地址(详细请看附录的第一点). subdomainOffset 属性会改变获取 subdomain 时返回数组的值, 比如 test.page.example.com 域名, 如果设置 subdomainOffset 为 2, 那么返回的数组值为 [“page”, “test”], 如果设置为 3, 那么返回数组值为 [“test”].

app.use()与中间件

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

本文基于koa2,也就是async/await版本,所以关于generator 函数暂且不看。

所以在调用app.use()时,也很简单,仅仅是把当前中间件push进中间件数组this.middleware。

所以, 所谓中间件函数的串联其实就是通过数组来逐个执行的, 至于 koa 是怎么利用 koa-compose 建立起核心的中间件机制的, 这里按下不表, 详细请阅读 理解 koa 中间件机制 博文.

listen 原理

listen 函数的原理其实很简单, 它实际上是一个缩写的函数, 它本质上就是在内部通过 Node 原生的http 模块建立起一个 http server, 而这个 http server 的回调函数使用的是 koa 中的 callback 函数的执行结果(也就是callback函数return 的函数).

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

下面我们来看一下this.callback()函数。

callback() {
 const fn = compose(this.middleware);
​
 if (!this.listenerCount('error')) this.on('error', this.onerror);
​
 // handleRequest 函数相当于 http.creatServer 的回调函数, 有 req, res 两个参数, 
 // 代表原生的 request, response 对象.
 const handleRequest = (req, res) => {
   // 每次接受一个新的请求就是生成一次全新的 context
   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);  // 响应处理
 // 为 res 对象添加错误处理响应, 当 res 响应结束时, 执行 context 中的 onerror 函数
 // (这里需要注意区分 context 与 koa 实例中的 onerror)
 onFinished(res, onerror);
 // 执行中间件数组所有函数, 并结束时调用 respond 函数
 return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

对于 this.createContext 函数, 它的用于就是生成一个新的 context 对象并建立 koa 中 context, requets, response 属性之间与原生 http 对象的关系的.

而 handleRequest 函数只是负责执行中间件所有的函数, 并在中间件函数执行结束的时候调用 respond.

对于在 koa 中的 context 对象, request 对象, response 对象与 http 模块原生的 req 与 res 之间的关系我并不打算陈列代码, 下面我以图解的形式来帮助阅读:

image

request.js

request.js主要是对原生的 http 模块的 requets 对象进行封装, 其实就是对 request 对象某些属性或方法通过重写 getter/setter 函数进行代理, 请看下面的图进行更好的理解:

image

内容协商

TODO

response.js

同样的, response.js 也是对 http 模块的 response 对象进行封装, 通过对 response 对象的某些属性或方法通过重写 getter/setter 函数进行代理, 请看下面的图帮助理解:

image

context.js

分析了上面的 request 与 response, context 的分析更为简单了, context 的核心就是通过 delegates 这一个库, 将 request, response 对象上的属性方法代理到 context 对象上.

也就是说例如 this.ctx.headersSent 相当于 this.response.headersSent. request 对象与 response 对象的所有方法与属性都能在 ctx 对象上找到. 这里我们来看一下 delegates 库的属性代理函数的片段, 借此理解一下 context 是如何代理 request 与 response 上的属性与方法的:

delegate(proto, 'response')
 .getter('headerSent');
Delegator.prototype.getter = function(name){
 // this.proto 指向原型, 这里的 proto 就是上面的 proto, 也就是说 context 对象
 var proto = this.proto;
 // target 是指 'response' 字符串
 var target = this.target;
 // 将 name 加入到 delegator 实例对象的 getters 数组中
 this.getters.push(name);
 // 调用原生的 __defineGetter__ 方法进行 getter 代理, 那么 proto[name] 就相当于 proto[target][name]
 // 而 context.response 就相当于 response 对象
 // 由此实现属性代理
 proto.__defineGetter__(name, function(){
 return this[target][name];
 });
​
 return this;
};

参考文章

koa-用到的delegates NPM包详解
koa-compose源码阅读

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容

  • 1.简书 koa是由Express原班人马打造,致力于成为一个更小、更富有表现力、更健壮的Web框架。使用koa编...
    不去解释阅读 2,643评论 0 11
  • Koa源码解析 整体架构 核心文件只有4个,在lib文件夹下: application.js koa框架的入口...
    Ethan_lcm阅读 2,424评论 0 1
  • 一、基本用法 1.1 架设 HTTP 服务 // demos/01.jsconst Koa = require('...
    majun00阅读 1,343评论 0 5
  • 微小說:同性戀 男人和女人歷經各種驚天動地的坎坷後終於幸福在一起了!婚禮現場,女人問男人,你為什麼會選擇和我在一起...
    纳兰长君阅读 153评论 0 0
  • 很高兴又到了给好种子浇水施肥的时候了! 怀着喜悦和放松的心情回忆今天种下的好种子,虽然每天种下的都是一些微不足...
    绚风阅读 122评论 0 2