Koa2 号称是下一代的 web 框架,由 Express 幕后原版人马打造,据说更小、更快、更加精简,主要采用了 es7 的语法糖async
和await
,并且 koa2 摒弃了 express 中的回调函数机制,这么多特性不得不去学习下它的原理,因此,自己通过实现精简版的 Qoa(koa2)来了解其原理实现,只是为了学习使用,欢迎来交流(github)。
简单实现 koa 基本功能,仅供学习参考,了解 koa 的核心思想以及实现过程。
依赖 node v7.6.0 或 ES2015 及更高版本和 async 方法支持。
主要依据koa
官网介绍,实现简易核心功能,基本实现以下几个功能
- use
- listen
- callback
- keys
- context 上下文
- response
- request
- middleware
实现use
API 其实就是将定义的方法发push
进入定义的中间件数组变量middleware
中,核心代码如下:
use(cb) {
this.middleware.push(cb)
}
主要是调用了 node 原生 API 中的http.createServer()
方法,在这会运行两个方法,基本如下:
1、创建ctx
2、定义fn
变量,保存use
中的push
进去的方法,
3、运行所有的fn
中的方法,同时通过定义的next
来触发fn
中下一个中间件方法
const server = http.createServer(async (req, res) => {
const ctx = this.createCtx(req, res)
const fn = compose(this.middleware)
await fn(ctx)
ctx.res.end(ctx.body)
})
return server.listen(...args)
之前定义的listen
中,使用http.createServer()
起的端口服务,如果需要定义多个端口,或者定义https
的话,你就可以使用callback
这个 API,并且将之前定义的中间件方法再次运行一次,然后挂在到新定义的服务上,代码如下:
const fn = compose(this.middleware)
const handleRequest = (req, res) => {
const ctx = this.createCtx(req, res)
return this[_handleRequest](ctx, fn)
}
return handleRequest
定义
this[_handleRequest]
主要是为了使用私有方法
,这样外部就能被访问。
这个主要是为了设置签名的 Cookie 密钥使用的,跟 koa2 实现基本一致,片段代码如下:
// context.js
get cookies() {
if (!this[COOKIES]) {
this[COOKIES] = new Cookies(this.req, this.res, {
keys: this.app.keys
});
}
return this[COOKIES];
},
set cookies(_cookies) {
this[COOKIES] = _cookies;
}
context.js 中能获取到 this.app 上定义的属性,主要是因为在application.js
中创建上下文的时候将this
赋值给当前context
、request
、response
的app
,片段代码如下:
// application.js
context.app = request.app = response.app = this
这里并没有实现完整的context
代码,主要是使用了get set
来定义方法,这样能监听对上下文的改变,并没有按照 koa 实现完整逻辑,只是为了实现基本流程,完整代码如下:
// context.js
const Cookies = require('cookies')
const COOKIES = Symbol('context#cookies')
module.exports = {
get url() {
return this.request.url
},
get body() {
return this.response.body
},
set body(val) {
this.response.body = val
},
get cookies() {
if (!this[COOKIES]) {
this[COOKIES] = new Cookies(this.req, this.res, {
keys: this.app.keys
})
}
return this[COOKIES]
},
set cookies(_cookies) {
this[COOKIES] = _cookies
}
}
为了实现基本流程,所以代码并不复杂,基本如下:
// response.js
module.exports = {
get body() {
return this._body
},
set body(val) {
this._body = val
}
}
// request.js
module.exports = {
get url() {
return this.req.url
}
}
koa2 的中间件其实就是洋葱结构,从上往下一层一层进来,再从下往上一层一层回去。实现简易版的中间件机制,完整代码如下:
// compose.js
function compose(middleware) {
return context => {
// 第一次出发use中的中间件的函数内容
return dispatch(0)
function dispatch(i) {
let fn = middleware[i]
if (!fn) return Promise.resolve()
return Promise.resolve(
fn(context, next => {
// 这里的next就是外层use中间件传入的next,用来确定执行下一个中间件的标记
return dispatch(i + 1)
})
)
}
}
}
module.exports = compose
这个得结合示例代码以及application.js
中的方法来读懂compose
方法,以下是application.js
中的use
、listen
、createCtx
三个方法:
module.exports = class Application {
// 省略
use(cb) {
this.middleware.push(cb)
}
listen(...args) {
const server = http.createServer(async (req, res) => {
const ctx = this.createCtx(req, res)
const fn = compose(this.middleware)
await fn(ctx) // 将上下文ctx传入compose.js中的compose方法中return出来的函数
ctx.res.end(ctx.body)
})
return server.listen(...args)
}
createCtx(req, res) {
const ctx = Object.create(this.context)
const request = (ctx.request = Object.create(this.request))
const response = (ctx.response = Object.create(this.response))
ctx.app = request.app = response.app = this
ctx.req = request.req = response.req = req
ctx.res = request.res = response.res = res
return ctx
}
// 省略
}
根据 demo 测试代码,你会发现最后浏览器预览输出的结果是123654
,123
好理解,根据上面的源码,发现 use 只是个同步代码,只有在触发next
时会触发compose
中的dispatch
方法,继续执行下一个异步中间件逻辑操作,因此会接着拼出654
,至于为什么不是123456
可以查看关于node 环境下的事件循环机制
这里主要看看入口文件,对于上下文到底做了什么,片段代码如下:
// application.js
// 省略
module.exports = class Application {
// 省略
createCtx(req, res) {
const ctx = Object.create(this.context)
const request = (ctx.request = Object.create(this.request))
const response = (ctx.response = Object.create(this.response))
ctx.app = request.app = response.app = this
ctx.req = request.req = response.req = req
ctx.res = request.res = response.res = res
return ctx
}
// 省略
}
这里需要解释下,ctx.req
、ctx.res
、ctx.app
、request.res
、response.req
:
- request - request 继承于 Request.js 静态类,包含操作 request 的一些常用方法
- response - response 继承于 Response.js 静态类,包含操作 response 的一些常用方法
- req - nodejs 原生的 request 对象
- res - nodejs 原生的 response 对象
- app - koa 的原型对象
运行test
文件夹下的 js 文件即可,基本实现以上几个简易的功能,测试代码如下:
const Qoa = require('../core/application')
const http = require('http')
const https = require('https')
const app = new Qoa()
app.use(async (ctx, next) => {
ctx.body = '1'
ctx.cookies.set('name', 'tobi', { signed: true })
await next()
ctx.body += '4'
})
app.use(async (ctx, next) => {
ctx.body += '2'
await next()
ctx.body += '5'
})
app.use(async (ctx, next) => {
ctx.body += '3'
await next()
ctx.body += '6'
})
app.keys = ['im a newer secret', 'i like turtle']
app.listen(3000, () => {
console.log('server start port 3000')
})
http.createServer(app.callback()).listen(1234, () => {
console.log('开启1234端口')
})
https.createServer(app.callback()).listen(443, () => {
console.log('开启443端口')
})
如果有什么不对的地方,欢迎提issues