博客
博客
文章目录
  1. 如何使用 koa
  2. koa 的构造函数
  3. 启动 http 服务
  4. 洋葱模型
  5. ctx 对象

koa2 源码解析

如何使用 koa

在看 koa2 的源码之前,按照惯例先看看 koa2 的 hello world 的写法。

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

//response
app.use (ctx => {
ctx.body = 'Hello Koa';
});

app.listen (3000);

一开始就通过 new 关键词对 koa 进行了实例化,并且实例化后的对象具有 uselisten 方法。废话不多说,先看 require ('koa') 引入的 application.js

koa 的构造函数

const Emitter = require('events');

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

this.proxy = false;
this.middleware = [];
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create (context);
this.request = Object.create (request);
this.response = Object.create (response);
}
//...
}

可以看到整个 Application 继承自 node 原生的 events,这是 node 的事件系统,这里不做过多展开,与浏览器中的事件绑定类似。
对 koa 实例化之后,先使用 use 方法进行中间件的绑定,然后调用 listen 方法监听系统端口,并且启动 http 服务。

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. ')
fn = convert (fn);
}
// 将中间件函数放入 middleware 队列中
this.middleware.push (fn);
return this;
}

首先需要判断传入的中间件是否为一个函数,koa1 的版本传入的为迭代器来做异步操作,且需要使用 convert 进行包装,koa2 的中间件使用 async function,然后将这些中间件函数放入 middleware 队列中。

启动 http 服务

接下来调用 listen 方法启动一个 http 服务,http 服务的回调函数由 callback 方法生成。

const http = require('http');

listen (...args) {
//koa 内部其实还是调用 node 自带的 http 服务
const server = http.createServer (this.callback ());
return server.listen (...args);
}


const compose = require('koa-compose');

callback () {
const fn = compose (this.middleware); // 用于生成 koa 的洋葱模型

if (!this.listenerCount ('error')) this.on ('error', this.onerror);

const handleRequest = (req, res) => {
// 创建 ctx 对象,该对象贯穿整个 koa,根据 http 类提供的 req 和 res 对象生成
const ctx = this.createContext (req, res);
return this.handleRequest (ctx, fn); // 返回值为 http 服务的回调函数
};

return handleRequest;
}

handleRequest (ctx, fnMiddleware) {
// 开始执行洋葱模型
return fnMiddleware (ctx)
.then (() => respond (ctx))
.catch (err => ctx.onerror (err));
}

洋葱模型

洋葱模型

通过 compose 函数包装中间件数组,用于生成洋葱模型,该函数属于 koa 的一个模块 koa-compose,现在深入 koa-compose 模块看看,它到底对 middleware 做了什么。

module.exports = compose
function compose (middleware) {
return function (context) {
return dispatch (0) // 先调用第一个中间件
function dispatch (i) {
let fn = middleware [i]
//fn 不存在表示已经没有中间件了,直接进行 resolve 操作
if (!fn) return Promise.resolve ()
try {
// 调用中间件函数,第二个参数表示调用下一个中间件
return Promise.resolve (fn (context, dispatch.bind (null, i + 1)));
} catch (err) {
return Promise.reject (err)
}
}
}
}

可以很明显的看到这是一个闭包,为了方便阅读我省略了一些错误处理,完整源码请看:链接。闭包返回的函数最后在 handleRequest 方法中被调用,而这个方法就是每次 http 服务的回调函数,所以每次发起 http 请求都会走入这个洋葱模型。

这个闭包里面进行了一个递归操作,先取出 middleware 的第 0 个函数,然后给这个函数传入两个参数第一个是 ctx 对象,这个对象属于 koa 自己封装的对象,后面再讲,第二个参数就是我们在中间件中使用的的 next 方法。

dispatch.bind (null, i + 1)

可以看到 next 方法其实就是递归调用下一个中间件函数。还有一点需要注意 dispatch 方法返回的是 Promise 对象,因为我们在使用 koa 的时候,中间件都是 async function,而且调用 next 的时候使用的是 await next (),这里必须返回的是一个 Promise 对象。

下面初始化了三个中间件,然后具体执行流程看下图。

执行 middleware

执行结果如下

执行结果

ctx 对象

看完洋葱模型之后看看 koa 的另一个重点,ctx 对象。该对象是通过 http 服务的回调函数传入的 reqres 两个对象生成的。

const ctx = this.createContext (req, res);


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

这里基本上是几个对象之间的相互挂载,进行了一系列的循环引用,为使用者提供方便。这里最后返回的 context 对象就是我们在 koa 中间件中使用的 ctx 对象。

const context = require('./context');
const context = Object.create (this.context)

可以看到这里的 context 对象继承自 context.js 暴露的对象,查看起代码可以发现,context 对象通过 delegate 方法,将 response 和 request 上的一些属于与方法代理到了自生上。

/**
* 响应体代理.
*/
const proto = module.exports = {
//...
};
delegate (proto, 'response')
.method ('attachment')
.method ('redirect')
.method ('remove')
.method ('vary')
.method ('set')
.method ('append')
.method ('flushHeaders')
.access ('status')
.access ('message')
.access ('body')
.access ('length')
.access ('type')
.access ('lastModified')
.access ('etag')
.getter ('headerSent')
.getter ('writable');

/**
* 请求体代理.
*/

delegate (proto, 'request')
.method ('acceptsLanguages')
.method ('acceptsEncodings')
.method ('acceptsCharsets')
.method ('accepts')
.method ('get')
.method ('is')
.access ('querystring')
.access ('idempotent')
.access ('socket')
.access ('search')
.access ('method')
.access ('query')
.access ('path')
.access ('url')
.access ('accept')
.getter ('origin')
.getter ('href')
.getter ('subdomains')
.getter ('protocol')
.getter ('host')
.getter ('hostname')
.getter ('URL')
.getter ('header')
.getter ('headers')
.getter ('secure')
.getter ('stale')
.getter ('fresh')
.getter ('ips')
.getter ('ip');

这里的代码很简单,就是将 context 的 request 和 response 的一些属性和方法直接代理到 context 本身上。下面简单看看 delegate 这个库做的事情。

// 构造函数 
function Delegator(proto, target) {
// 非 new 调用时,自动进行实例化
if (!(this instanceof Delegator)) return new Delegator (proto, target);
this.proto = proto;
this.target = target;
}

// 从 targe 上委托一方法到 proto 上.
Delegator.prototype.method = function(name){
var proto = this.proto;
var target = this.target;
proto [name] = function(){
return this[target][name].apply (this[target], arguments);
};
return this;
};

// 代理一个可访问又可设置的属性.
Delegator.prototype.access = function(name){
return this.getter (name).setter (name);
};

// 代理一个可访问的属性.
Delegator.prototype.getter = function(name){
var proto = this.proto;
var target = this.target;
proto.__defineGetter__(name, function(){
return this[target][name];
});
return this;
};

// 代理一个可设置的属性.
Delegator.prototype.setter = function(name){
var proto = this.proto;
var target = this.target;
proto.__defineSetter__(name, function(val){
return this[target][name] = val;
});
return this;
};

注意这里用到的 __defineGetter____defineSetter__ 都是 v8 引擎带的对象方向,并不属于 web 标准(之前有过提案,但是后面已经从 Web 标准中删除)。

然后在 request 对象和 response 对象中,通过 getter 和 setter 设置了很多属性,就是获取或者设置 node 的 http 服务的原生对象(req、rsp)。

这里列举几个常用的属性:

  • request
module.exports = {
// 获取请求体的某个字段,兼容 referer 写法
get(field) {
const req = this.req;
switch (field = field.toLowerCase ()) {
case 'referer':
case 'referrer':
return req.headers.referrer || req.headers.referer || '';
default:
return req.headers [field] || '';
}
},
// 获取 http 请求的 method
get method () {
return this.req.method;
},
// 获取请求参数
get query () {
const str = this.querystring;
const c = this._querycache = this._querycache || {};
return c [str] || (c [str] = qs.parse (str));
},
get querystring () {
if (!this.req) return '';
return parse (this.req).query || '';
}

// 其他关于 http 请求体的相关属性(header、origin、accept、ip……)就不一一列举了
}
  • response
module.exports = {
// 最常用的就是对 body 进行设置
set body (val) {
const original = this._body;
this._body = val;

//no content
if (null == val) {
if (!statuses.empty [this.status]) this.status = 204;
this.remove ('Content-Type');
this.remove ('Content-Length');
this.remove ('Transfer-Encoding');
return;
}

//set the status
if (!this._explicitStatus) this.status = 200;

//set the content-type only if not yet set
const setType = !this.header ['content-type'];

//string
if ('string' == typeof val) {
if (setType) this.type = /^\s*</.test (val) ? 'html' : 'text';
this.length = Buffer.byteLength (val);
return;
}

//buffer
if (Buffer.isBuffer (val)) {
if (setType) this.type = 'bin';
this.length = val.length;
return;
}

//stream
if ('function' == typeof val.pipe) {
onFinish (this.res, destroy.bind (null, val));
ensureErrorHandler (val, err => this.ctx.onerror (err));

//overwriting
if (null != original && original != val) this.remove ('Content-Length');

if (setType) this.type = 'bin';
return;
}

//json
this.remove ('Content-Length');
this.type = 'json';
},
}

关于 http 请求体和响应体设置的属性比较多,这里就不一一列举了,最后在看看 ctx 对象和 req、rsp 直接的关系吧。

ctx

支持一下
扫一扫,支持一下
  • 微信扫一扫
  • 支付宝扫一扫