Koa源码解析

介绍

先来了解下 Koa,摘自[koa][https://koa.bootcss.com/]中文官网

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

源代码见[koajs/koa][https://github.com/koajs/koa]

demo

1
2
3
4
5
6
7
8
const Koa = require("koa");
const app = new Koa();

app.use(async (ctx, next) => {
ctx.body = "Hello world~";
});

app.listen(3000);

源码

打开 package.json 文件,可以看到 koa 实例的入口

1
"main": "lib/application.js",

constructor.js

基本配置和声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = class Application extends Emitter {

constructor(options) {
super();
options = options || {}; //配置
this.proxy = options.proxy || false; //是否proxy模式
this.subdomainOffset = options.subdomainOffset || 2; //domain要忽略的偏移量
this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For'; //proxy自定义头部
this.maxIpsCount = options.maxIpsCount || 0; //代理服务器数量
this.env = options.env || process.env.NODE_ENV || 'development'; //环境变量
if (options.keys) this.keys = options.keys; // 自定义cookie 密钥
this.middleware = []; //中间件数组
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);

if (util.inspect.custom) { //自定义检查,这里的作用是get app时,去执行this.inspect 。感兴趣可见http://nodejs.cn/api/util.html#util_util_inspect_custom
this[util.inspect.custom] = this.inspect;
}
}
...

其中 Object.create()嵌套的 context,request,response 分别为其他三个文件导出的实例,考虑到存在多个 new Koa 的应用存在,防止这些 app 相互污染,保证不同引用地址。

listen 方法

1
2
3
4
5
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}

listen 方法创建 httpServer 并传入定义好的 callback

toJSON,inspect 方法

1
2
3
4
5
6
7
8
9
10
11
toJSON() {
return only(this, [
'subdomainOffset',
'proxy',
'env'
]);
}

inspect() {
return this.toJSON();
}

该方法通过[only][https://www.npmjs.com/package/only]库筛选出对象,且只包含指定的属性。

use 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 || '-');
// 保存到middleware数组中
this.middleware.push(fn);
return this;
}

其中[isGeneratorFunction][https://www.npmjs.com/package/is-generator-function]是对传入函数是否是 generator 函数做判断,这是对 koa1.x 的兼容,现在 2.x 默认使用 async 函数形式,这步意义就是使用[convert][https://www.npmjs.com/package/koa-convert]将 fn 转换成一个返回 promise 的函数。

其中 convert 中主要使用[co][https://www.npmjs.com/package/co]库

1
2
3
4
5
6
7
8
9
10
11
const converted = function (ctx, next) {
return co.call(
ctx,
mw.call(
ctx,
(function* (next) {
return yield next();
})(next)
)
);
};

callback 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
callback() {
// 依次调用middleware中函数,即洋葱模型核心
const fn = compose(this.middleware);

// 判断设置监听error事件
if (!this.listenerCount('error')) this.on('error', this.onerror);

// 根据当前req,res创建context,并传入handleRequest函数
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};

return handleRequest;
}

其中[compose][https://www.npmjs.com/package/koa-compose]函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function compose(middleware) {
// 判断middleware是否数组,其内容是否为function类型
if (!Array.isArray(middleware))
throw new TypeError("Middleware stack must be an array!");
for (const fn of middleware) {
if (typeof fn !== "function")
throw new TypeError("Middleware must be composed of functions!");
}

return function (context, next) {
// 当前调用的middleware索引
let index = -1;
// 调用dispatch,开始进入"洋葱模型"
return dispatch(0);
// dispatch属于闭包存在内存中,便于后续调用next()
function dispatch(i) {
// 判断index,防止多次调用next
if (i <= index)
return Promise.reject(new Error("next() called multiple times"));
// 更新index索引
index = i;
// 依次读取middleware中的function
let fn = middleware[i];
// 遍历到最后一项,next赋值给fn,此时为undefined,然后resolve跳出
if (i-- - middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {
// 包装成promise,返回调用下一个中间件
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}

再看下 async 中间件 demo,执行某逻辑后,调用 next 也就是刚才说到的内存中的 dispatch 函数,然后进入下一个中间件。

1
2
3
4
5
6
7
8
9
10
function log(ctx) {
console.log(ctx.method, ctx.header.host + ctx.url);
}

module.exports = function () {
return async function (ctx, next) {
log(ctx);
await next();
};
};

handleRequest 函数

1
2
3
4
5
6
7
8
9
10
11
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
// 监听错误
const onerror = err => ctx.onerror(err);
// 返回响应对象
const handleResponse = () => respond(ctx);
// http请求结束后调用
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

其中 onFinished 作用是当 HTTP 请求关闭、完成或出错时执行回调。之后将 context 依次传入到每个中间件中使用,然后调用 handleResponse。

createContext 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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。让每次 http 请求都生成一个 context,并且单次生成的 context 是全局唯一的,相互之间隔离。同样的,Object.create(this.request | response)也是同理。将 Object.create(this.request | response)赋值给 context.request | response,这样我们可以在 context 上访问到 request 和 response。
这里我们其实就是做了让 response、this.request、context,可以共享 app、res、req 这些属性,并且可以互相访问。

onerror 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 onerror(err) {
// 判断是否原生错误对象,否则抛出类型错误
const isNativeError =
Object.prototype.toString.call(err) --- '[object Error]' ||
err instanceof Error;
if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err));

if (404 --- err.status || err.expose) return;
// 如果为静默模式,return掉,不打印错误信息
if (this.silent) return;

const msg = err.stack || err.toString();
console.error(`\n${msg.replace(/^/gm, ' ')}\n`);
}

static get default() {
return Application;
}
};

respond 函数

它主要做的是ctx 返回不同情况的处理,如 method 为 head 时加上 content-length 字段、body 为空时去除 content-length 等字段,返回相应状态码、body 为 Stream 时使用 pipe 等。

context.js

onerror 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
onerror(err) {
if (null == err) return;

const isNativeError =
Object.prototype.toString.call(err) --- '[object Error]' ||
err instanceof Error;
if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err));

// 判断当前是否存在一个响应头,下一步存在则return
let headerSent = false;
if (this.headerSent || !this.writable) {
headerSent = err.headerSent = true;
}

// 利用emit将错误转交application中的onerror处理,可以使用emit,on发布订阅是因为app继承events中的Emitter对象
this.app.emit('error', err, this);

if (headerSent) {
return;
}

const { res } = this;

if (typeof res.getHeaderNames --- 'function') {
res.getHeaderNames().forEach(name => res.removeHeader(name));
} else {
res._headers = {}; // Node < 7.7
}

// 下边主要是设置并返回错误相关信息
this.set(err.headers);

this.type = 'text';

let statusCode = err.status || err.statusCode;

if ('ENOENT' --- err.code) statusCode = 404;

if ('number' !== typeof statusCode || !statuses[statusCode]) statusCode = 500;

const code = statuses[statusCode];
const msg = err.expose ? err.message : code;
this.status = err.status = statusCode;
this.length = Buffer.byteLength(msg);
res.end(msg);
},

委托模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
delegate(proto, "response")
.method("attachment")
.method("redirect")
.method("remove")
.method("vary")
.method("has")
.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 函数,以及 method,access,getter,查看源码。

1
2
3
4
5
6
7
8
9
10
11
Delegator.prototype.method = function (name) {
var proto = this.proto;
var target = this.target;
this.methods.push(name);

proto[name] = function () {
return this[target][name].apply(this[target], arguments);
};

return this;
};

代码不难理解,proto[name]实际返回的是 target[name]函数的调用,即使 proto 可以调用 target 上的函数。

1
2
3
4
5
6
7
8
9
10
11
Delegator.prototype.getter = function (name) {
var proto = this.proto;
var target = this.target;
this.getters.push(name);

proto.__defineGetter__(name, function () {
return this[target][name];
});

return this;
};
1
2
3
4
5
6
7
8
9
10
11
Delegator.prototype.setter = function (name) {
var proto = this.proto;
var target = this.target;
this.setters.push(name);

proto.__defineSetter__(name, function (val) {
return (this[target][name] = val);
});

return this;
};

而 getter 和 setter 则是分别使用__defineGetter____defineSetter__劫持 proto 的 get 和 set,实际调用的也是 target 上的内容。

1
2
3
Delegator.prototype.access = function (name) {
return this.getter(name).setter(name);
};

access 同理。

这里主要将 response 和 request 代理到 context,这样就可以在 context 中访问,类似 ctx.body,ctx.header。

request.js/response.js

这两个文件主要是对原生 req,res 的封装,整理请求或响应的信息,然后代理到 context 中使用。

提两句

因为学习node这块接触koa和一大堆中间件,自己的算法能力不够硬核,借此机会多多阅读源码,学学比较精髓的代码用法,风格和设计模式等等。在阅读过程中也有自己解读的不到位,还烦请各位指出,我也会随着技术精进反复阅读,改进,共勉。

参考链接:

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

给阿姨来一杯卡普基诺~

支付宝
微信