源码共读-Koa

Koa是基于 Node.js 平台的下一代 web 开发框架,它的源码可以看这里,本章通过源码来简绍一下Koa是怎么实现的。

核心代码

Koa的核心代码只有4个文件,如图。

核心代码

各个文件的作用:

application.js:Koa的核心,对应Koa App类。
context.js:对应上下文对象ctx。
request.js:对应ctx.request对象。
response.js:对应ctx.response对象。

Koa实现

Koa使用

Koa使用如下:

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

app.use(ctx => {
ctx.body = 'Hello World';
});

app.listen(3000, () => {
console.log("服务器启动成功!");、
});

Koa底层是基于原生http模块,原生http模块怎么启动一个服务呢?如下:

1
2
3
4
5
6
7
8
9
10
const http = require('http');

const server = http.createServer(function(req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end("Hello World");
});

server.listen(3000, () => {
console.log("服务器启动成功!");、
});

观察上面的代码,两者是不是挺像的。

application源码

为了方便查看application的核心逻辑,下面是我去掉了部分非核心代码的application源码:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
const onFinished = require('on-finished')
const response = require('./response')
const compose = require('koa-compose')
const context = require('./context')
const request = require('./request')
const statuses = require('statuses')
const Emitter = require('events')
const Stream = require('stream')
const http = require('http')

class Application extends Emitter {
constructor (options) {
super()
options = options || {}
this.env = options.env || process.env.NODE_ENV || 'development'
this.compose = options.compose || compose
if (options.keys) this.keys = options.keys
this.middleware = []
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
}

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

use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
this.middleware.push(fn)
return this
}

callback() {
const fn = this.compose(this.middleware)
if (!this.listenerCount('error')) this.on('error', this.onerror)

const handleRequest = (req, res) => {
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)
}

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.status === 404 || err.expose) return
if (this.silent) return

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

function respond (ctx) {
// allow bypassing koa
if (ctx.respond === false) return

if (!ctx.writable) return

const res = ctx.res
let body = ctx.body
const code = ctx.status

// ignore body
if (statuses.empty[code]) {
// strip headers
ctx.body = null
return res.end()
}

if (ctx.method === 'HEAD') {
if (!res.headersSent && !ctx.response.has('Content-Length')) {
const { length } = ctx.response
if (Number.isInteger(length)) ctx.length = length
}
return res.end()
}

// status body
if (body == null) {
if (ctx.response._explicitNullBody) {
ctx.response.remove('Content-Type')
ctx.response.remove('Transfer-Encoding')
ctx.length = 0
return res.end()
}
if (ctx.req.httpVersionMajor >= 2) {
body = String(code)
} else {
body = ctx.message || String(code)
}
if (!res.headersSent) {
ctx.type = 'text'
ctx.length = Buffer.byteLength(body)
}
return res.end(body)
}

// responses
if (Buffer.isBuffer(body)) return res.end(body)
if (typeof body === 'string') return res.end(body)
if (body instanceof Stream) return body.pipe(res)

// body: json
body = JSON.stringify(body)
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body)
}
res.end(body)
}

当调用app.use的时候,实际上是把中间件函数加入到this.middleware数组当中。
当调用app.listen的时候,通过http.createServer来创建http服务并使用server.listen来监听服务。
这里比较难理解的是callback函数,它使用compose将中间件合并成一个调用函数,具体怎么合并的我们稍后再说。如果error事件没有监听的话,添加一个默认的监听函数,默认的onerror函数实际上就是打印错误信息;this.listenerCount是从哪里来的呢?实际上Application类是继承自node中的Emitter,该方法也是Emitter的方法。最后返回了一个handleRequest函数,该函数做了2件事,首先通过reqres构建ctx,然后调用this.handleRequest,注意this.handleRequestApplication类的属性而不是callback中的handleRequest,也就是这里并没有递归调用。
this.handleRequest函数中调用了中间件函数fnMiddleware(ctx),当中间件函数都调用完了以后调用respond(ctx)respond通过不同的情况去处理res的结果;失败的时候调用ctx.onerror(err)。另外在中间件处理之前会调用onFinished(res, onerror)来监听出错的情况,onFinished的代码请看这里

koa-compose源码

在讲述源码之前我们先看看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
const Koa = require('koa');
const app = new Koa();

app.use(async(ctx, next) => {
console.log('第1个中间件开始');
await next();
console.log('第1个中间件结束');
});

app.use(async(ctx, next) => {
console.log('第2个中间件开始');
await next();
console.log('第2个中间件结束');
});

app.use(async(ctx, next) => {
console.log('第3个中间件开始');
await next();
console.log('第3个中间件结束');
});

app.listen(3000, () => {
console.log("服务器启动成功!");
});

客户端打印:

1
2
3
4
5
6
第1个中间件开始
第2个中间件开始
第3个中间件开始
第3个中间件结束
第2个中间件结束
第1个中间件结束

这就是Koa中间件著名的洋葱模型。

洋葱模型

我们先不谈Koa只看看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
const compose = require('koa-compose');

const middleware = [
async(ctx, next) => {
console.log('第1个中间件开始');
await next();
console.log('第1个中间件结束');
},
async(ctx, next) => {
console.log('第2个中间件开始');
await next();
console.log('第2个中间件结束');
},
async(ctx, next) => {
console.log('第3个中间件开始');
await next();
console.log('第3个中间件结束');
}
];
const fn = compose(middleware);
const ctx = {};
fn(ctx).then(() => {
console.log('处理完成了');
});

上面打印:

1
2
3
4
5
6
7
第1个中间件开始
第2个中间件开始
第3个中间件开始
第3个中间件结束
第2个中间件结束
第1个中间件结束
处理完成了

koa-compose把多个中间件合并成一个函数,通过await next()来调用下一个中间件,其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function compose (middleware) {
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) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}

首先对middleware做类型检查,middleware必须是数组,同时每一个中间件必须是函数。然后返回一个函数,这个函数第一个参数是上下文对象,第二个参数是下个中间件执行的next函数。核心逻辑是上面的dispatch方法,在dispatch方法中会返回Promise。dispatch方法实际上就是next方法,首次会调用dispatch(0)来触发第一个中间件函数。当一个中间件中调用next方法后会把index标记为当前的索引,如果一个中间件多次调用next方法,那么由于第一次调用是index会标记为i,那么第二次调用的时候iindex是相等的,也就是第二次的时候会走if (i <= index) return Promise.reject(new Error('next() called multiple times'))逻辑,也就是会报错。每次调用的时候根据索引获取当前要执行的中间件函数,在第18行会执行当前中间件,并把下一个dispatch当作第二个参数next传入到下一个中间件中。当执行到最后一个中间件的时候,设置fn = next由于Application代码的第52行并没有传递第二个参数,所以此时nextundefined,那么compose中将会走第16行if (!fn) return Promise.resolve()的逻辑。如果传递了函数那么会执行传入的函数,当此函数中调用next以后,由于索引已经超过了middleware的长度,所以下次函数执行事也会走第16行的逻辑。

context源码

context是对上下文对象的封装,具体代码如下:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
const util = require('util')
const createError = require('http-errors')
const httpAssert = require('http-assert')
const delegate = require('delegates')
const statuses = require('statuses')
const Cookies = require('cookies')

const COOKIES = Symbol('context#cookies')

const proto = module.exports = {
inspect () {
if (this === proto) return this
return this.toJSON()
},

toJSON () {
return {
request: this.request.toJSON(),
response: this.response.toJSON(),
app: this.app.toJSON(),
originalUrl: this.originalUrl,
req: '<original node req>',
res: '<original node res>',
socket: '<original node socket>'
}
},

assert: httpAssert,

throw (...args) {
throw createError(...args)
},

onerror (err) {.
if (err == null) 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))

let headerSent = false
if (this.headerSent || !this.writable) {
headerSent = err.headerSent = true
}

// delegate
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
}

// then set those specified
this.set(err.headers)

// force text/plain
this.type = 'text'

let statusCode = err.status || err.statusCode

// ENOENT support
if (err.code === 'ENOENT') statusCode = 404

// default to 500
if (typeof statusCode !== 'number' || !statuses[statusCode]) statusCode = 500

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

get cookies () {
if (!this[COOKIES]) {
this[COOKIES] = new Cookies(this.req, this.res, {
keys: this.app.keys,
secure: this.request.secure
})
}
return this[COOKIES]
},

set cookies (_cookies) {
this[COOKIES] = _cookies
}
}

/**
* Response delegation.
*/
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')

/**
* Request delegation.
*/
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实际上就是一个对象,它对Cookieonerror做了一个封装。最后使用delegate()来代理requestresponse对象,delegate不了解的同学可以看下面这个示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const delegate = require('delegates');

const obj = {
aaa: {
name: 'aaa',
age: 18,
isBoy: true,
say() {
console.log(`我是${this.name},今年${this.age}`);
}
}
};

delegate(obj, 'aaa')
.method('say')
.getter('name')
.setter('age')
.access('isBoy')


console.log(obj.name); // 打印 aaa
obj.age = 19; // 可以设置属性
obj.say(); // 打印 我是aaa,今年19
console.log(obj.isBoy); // 打印 true

上面代理了obj对象的aaa属性,所以直接可以通过obj来访问aaa中代理的属性和方法,其中method表示代理方法,getter表示代理get方法,setter表示代理set方法,access表示不但代理了get同时也代理了set。delegates的实现也不难:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
function Delegator(proto, target) {
if (!(this instanceof Delegator)) return new Delegator(proto, target);
this.proto = proto;
this.target = target;
this.methods = [];
this.getters = [];
this.setters = [];
this.fluents = [];
}

Delegator.auto = function(proto, targetProto, targetProp){
var delegator = Delegator(proto, targetProp);
var properties = Object.getOwnPropertyNames(targetProto);
for (var i = 0; i < properties.length; i++) {
var property = properties[i];
var descriptor = Object.getOwnPropertyDescriptor(targetProto, property);
if (descriptor.get) {
delegator.getter(property);
}
if (descriptor.set) {
delegator.setter(property);
}
if (descriptor.hasOwnProperty('value')) { // could be undefined but writable
var value = descriptor.value;
if (value instanceof Function) {
delegator.method(property);
} else {
delegator.getter(property);
}
if (descriptor.writable) {
delegator.setter(property);
}
}
}
};

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

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

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

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

Delegator.prototype.fluent = function (name) {
var proto = this.proto;
var target = this.target;
this.fluents.push(name);

proto[name] = function(val){
if ('undefined' != typeof val) {
this[target][name] = val;
return this;
} else {
return this[target][name];
}
};

return this;
};

request与response

requestresponse就是一个简单的对象,没什么好说的,比如request代码大致如下:

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
module.exports = {
get header () {
return this.req.headers
},

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

get headers () {
return this.req.headers
},

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

get url () {
return this.req.url
},

set url (val) {
this.req.url = val
},

// 省略其他代码
}

这里需要注意的是有一个this.req对象,这个对象是从哪里来的?请看ApplicationcreateContext方法的第61行,在这里把node的req挂载了上来,res同理。

-------------本文结束 感谢您的阅读-------------
0%