Koa学习

koa 源码解析

2019-04-21  本文已影响0人  DC_er

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

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

koa 整体架构

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

我们这里使用 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 属性与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源码阅读

上一篇 下一篇

猜你喜欢

热点阅读