# 深入 Koa 原理
通过上一篇文章,我们已经了解到 Koa 是个非常精简的框架,实现起来难度并不大,这一章我们就一起来手写一个 koa 吧!
- 编写 application 文件
- 编写 request 和 response 文件
- 编写 context 文件
# 编写 application 文件
Application 文件是 koa 的核心,所有逻辑都会经过该文件调度。
# 开启 Web 服务器
首先,koa 作为一个 web 服务器框架,开启一个 web 服务器是最基本的方法。
- 封装 listen 方法开启服务器(底层调用 http.createServer)。
- 在 http.createServer 回调函数中调用 callback 方法,执行中间件。
- 中间件执行不报错,会调用 respond 方法,对返回及结果进行操作。
- 中间件执行报错,会调用 onerror 方法,输出错误信息到客户端。
const http = require("http");
const Emitter = require("events");
// koa 继承至 events,这样就可以在实例上使用 on 方法监听事件。
class DiyKoa extends Emitter {
constructor() {
super();
this.middlewares = [];
}
use(middleware) {
this.middlewares.push(middleware);
}
// compose(){}
// responseBody
// onerror
callback() {
const fnMiddleware = this.compose(this.middlewares);
return (req, res) => {
const ctx = this.createContext(req, res);
const respond = this.responseBody(ctx);
const onerror = this.onerror(ctx);
return fnMiddleware(ctx)
.then(respond)
.catch(onerror);
};
}
listen(...args) {
const server = http.createServer(this.callback());
server.listen(...args);
}
}
module.exports = DiyKoa;
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
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
# 创建 context 代理请求和响应
为了更方便用户的操作,koa 将请求和响应两个对象进行了代理,通过创建 context 对象,掌管整个请求和响应。
class DiyKoa extends Emitter {
// 具体代理操作见下文
createContext(req, res) {
const ctx = this.context;
ctx.request = this.request;
ctx.response = this.response;
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
return ctx;
}
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 封装中间件执行逻辑
上一章 koa 中间件介绍时,我们知道,中间件执行的顺序类似于洋葱模型,即:先按正序执行中间件 next 前半部分的代码,然后按倒序执行中间件 next 后半部分的代码。
笔者没有使用官方版本编写 compose 函数,而是通过函数组合的方式实现了该洋葱模型。
- 初始化 next 方法。
- 遍历所有中间件。
- 将遍历到的中间件封装到新 next 方法中,并覆盖掉老 next 方法。
- 封装之后,下一个中间件的 next 方法中就包含上一个中间件的代码。
- 执行 next 方法。
class DiyKoa extends Emitter {
compose(middlewares) {
return async ctx => {
let next = async () => {
await Promise.resolve();
};
function createNext(middleware, oldNext) {
return async () => {
await middleware(ctx, oldNext);
};
}
for (let i = middlewares.length - 1; i >= 0; i--) {
const currentMiddleware = middlewares[i];
next = createNext(currentMiddleware, next);
}
await next();
};
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 中间件执行完成后的操作
中间件执行完成后会执行以下方法。
- responseBody 向客户端输出数据之前,进行数据的处理。
- onerror 如果中间件出错,则做出对应操作。
class DiyKoa extends Emitter {
responseBody(ctx) {
return () => {
const context = ctx.body;
if (typeof context === "object") {
ctx.res.end(JSON.stringify(context));
} else {
ctx.res.end(context);
}
};
}
onerror(ctx) {
return err => {
if (err.code === "ENOENT") {
ctx.status = 404;
} else {
ctx.status = 500;
}
let msg = err.message;
ctx.res.end(msg);
this.emit("error", err);
};
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 编写 request 和 response 文件
Request 和 Response 文件是对请求和响应的一层浅封装,提供一套更加方便的请求响应处理方法。
# 编写 request 文件
var url = require("url");
// 封装源生 request 操作
// 例如:增加quert方法,快速定位参数。headers 方法快速扩区headers字段
module.exports = {
get query() {
return url.parse(this.req.url, true).query;
},
get url() {
return this.req.url;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 编写 response 文件
// 封装源生 response 操作
// 例如:body 方法,统一返回数据到客户端。socket 方法,快速获取 res 中的 socket 对象。
module.exports = {
get body() {
return this._body;
},
set body(data) {
this._body = data;
},
get status() {
return this.res.statusCode;
},
set status(code) {
if (typeof code !== "number") {
throw new Error("statusCode 只能是数字");
}
this.res.statusCode = code;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 编写 context 文件
在 context 文件中代理 request 和 response。
- 使用
__defineSetter__
方法代理 set 请求。 - 使用
__defineGetter__
方法代理 get 请求。
let proto = {};
function delegateSet(property, name) {
proto.__defineSetter__(name, function(val) {
this[property][name] = val;
});
}
function delegateGet(property, name) {
proto.__defineGetter__(name, function() {
return this[property][name];
});
}
// 定义需要代理的属性
let requestSet = [];
let requestGet = ["query", "url"];
let responseSet = ["body", "status"];
let responseGet = responseSet;
requestSet.forEach(item => {
delegateSet("request", item);
});
requestGet.forEach(item => {
delegateGet("request", item);
});
responseSet.forEach(item => {
delegateSet("response", item);
});
responseGet.forEach(item => {
delegateGet("response", item);
});
module.exports = proto;
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
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
# 总结
本文从头到尾实现了一个简单的 koa 框架原型,最后总结了一张图,供大家参考。