最近读了 koa2 的源码,理清楚了架构设计与用到的第三方库。本系列将分为 3 篇,分别介绍 koa 的架构设计和 3 个核心库,最终会手动实现一个简易的 koa。这是系列第 3 篇,模拟实现玩具版 koa

源码和测试代码放在了:simple-koa

准备

设计思想和第三方库原理都在前 2 篇详细说明了。这篇主要目的是做一个验证检验,在语法使用 ES6/7 的语法。

在开始前,安装一下需要用到的库:

1
2
npm install --save koa-compose koa-convert is-generator-function
npm install --save events

测试文件

为了说明效果,先按照正常使用 koa 的逻辑编写了测试文件。当启动它的时候,它的预期行为是:

  • 监听 3000 端口
  • 加载中间件
  • 浏览器访问localhost:3000,屏幕打印hello
  • 服务器的控制台依次输出:1inner => 2innter => 2outer => 1outer

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const Koa = require('./lib/application')

const server = new Koa()

async function middleware1(ctx, next) {
console.log('1 inner')
await next()
console.log('1 outer')
}

async function middleware2(ctx, next) {
ctx.res.body = 'hello'
console.log('2 inner')
await next()
console.log('2 outer')
}

server.use(middleware1)
server.use(middleware2)

server.listen(3000)

玩具 koa

只准备了一个文件,跑通上面的逻辑即可。文件是 lib/application.js

文件代码:

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
const http = require('http')
const Emitter = require('events')
const compose = require('koa-compose')

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

this.middleware = []
this.context = {}
this.request = {}
this.response = {}
}

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

use(fn) {
this.middleware.push(fn)
return this
}

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

return (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
}

handleRequest(ctx, fnMiddleware) {
const res = ctx.res
// res.statusCode = 404
const handleResponse = () => {
res.end(res.body)
}

return fnMiddleware(ctx).then(handleResponse).catch(this.onerror)
}

createContext(req, res) {
const context = Object.create(this.context)
context.request = Object.create(this.request)
context.response = Object.create(this.response)
context.req = req
context.res = res

context.app = this
context.state = {}

return context
}

onerror(error) {
console.log(`error occurs: ${error.message}`)
}
}

下面开始解析lib/application.js文件代码

构造函数

首先对外暴露的就是一个继承 Emitter 的 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
const http = require('http')
const Emitter = require('events')
const compose = require('koa-compose')

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

this.middleware = [] // 中间件
this.context = {} // 上下文
this.request = {} // 请求信息
this.response = {} // 返回信息
}

listen(...args) {}

use(fn) {}

callback() {}

handleRequest(ctx, fnMiddleware) {}

createContext(req, res) {}

onerror(error) {
console.log(`error occurs: ${error.message}`)
}
}

继承 Emitter 事件类,是为了方便监听和处理报错。

use

将外面传入的中间件保存起来:

1
2
3
4
use (fn) {
this.middleware.push(fn)
return this
}

createContext

主要用于创建上下文。外面可以通过访问 ctx 上的 req/res 拿到请求或者返回信息。

1
2
3
4
5
6
7
8
9
10
11
12
createContext (req, res) {
const context = Object.create(this.context)
context.request = Object.create(this.request)
context.response = Object.create(this.response)
context.req = req
context.res = res

context.app = this
context.state = {}

return context
}

listen 和 callback

监听端口,启动服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
listen (...args) {
const server = http.createServer(this.callback())
return server.listen(...args)
},
callback () {
const fn = compose(this.middleware)
this.on('error', this.onerror)

return (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
}

handleRequest

callback 方法中真是返回的内容,它的作用就是:处理请求,并且返回给客户端。

1
2
3
4
5
6
7
8
9
10
11
handleRequest(ctx, fnMiddleware) {
const res = ctx.res
// res.statusCode = 404
const handleResponse = () => {
res.end(res.body)
}

return fnMiddleware(ctx)
.then(handleResponse)
.catch(this.onerror)
}

效果截图

启动 index.js 后,在浏览器访问本地 3000 端口:

回到控制台,查看中间件的输出顺序是否正确: