导读:根据koa2应用的中间件处理的流程、规范和特性来简单实现一下。以koa2的脚手架生成的实例工程为例:
npm i koa-generator -g
koa2 yourname
npm i & npm run dev
打开工程根目录下的app.js文件,如下。可以看到,koa2的中间件通过app.use来进行注册, 每一个中间件有固定的格式:
- 都是一个async函数,即一个promise.
- 每个中间件接收两个参数ctx和next,这个ctx就是context上下文的意思,next就是执行下一个中间件的一个方法。这里面相关的有一个洋葱圈模型,如下图,意思是说整个中间件的执行过程就像一个洋葱,一个中间件包裹了另外一个中间件。
const Koa = require('koa')
const app = new Koa()
const views = require('koa-views')
const json = require('koa-json')
const onerror = require('koa-onerror')
const bodyparser = require('koa-bodyparser')
const logger = require('koa-logger')
const index = require('./routes/index')
const users = require('./routes/users')
// error handler
onerror(app)
// middlewares
app.use(bodyparser({
enableTypes:['json', 'form', 'text']
}))
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))
app.use(views(__dirname + '/views', {
extension: 'pug'
}))
// logger
app.use(async (ctx, next) => {
const start = new Date()
await next()
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})
// routes
app.use(index.routes(), index.allowedMethods())
app.use(users.routes(), users.allowedMethods())
// error-handling
app.on('error', (err, ctx) => {
console.error('server error', err, ctx)
});
module.exports = app
在实现之前,我们要想清楚koa2中间件做了什么处理:
- 通过app.use方法把中间件的执行串联了起来,像一个洋葱一样,一层套一层.
- 每一个中间件都是一个async函数,而async函数本质上是一个promise.
- 每个中间件接收两个参数,ctx和next,ctx就是req和res的集合,next用来执行下一个中间件。
以koa官网的代码示例为例:
const Koa = require('koa');
const app = new Koa();
// logger
app.use(async (ctx, next) => {
console.log('第一层洋葱开始')
await next();
const rt = ctx['X-Response-Time'];
console.log(`${ctx.req.method} ${ctx.req.url} - ${rt}`);
console.log('第一层洋葱结束')
});
// x-response-time
app.use(async (ctx, next) => {
console.log('第二层洋葱开始')
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx['X-Response-Time'] = `${ms}ms`;
console.log('第二层洋葱结束')
});
// response
app.use(async ctx => {
console.log('第三层洋葱开始')
ctx.res.end('This is like koa2');
console.log('第三层洋葱结束')
});
app.listen(8000);
使用node执行app.js打印如下:
第一层洋葱开始 => 第二层洋葱开始 => 第三层洋葱开始 => 第三层洋葱结束 => 第二层洋葱结束 => 第一层洋葱结束。
可以看出,koa2的中间件数据结构其实是一个先进后出的队列结构。
以上3条一个个来实现一下:
1. app.use
class Koa2 {
constructor() {
// 存储注册的中间件
this.middlewareList = []
}
// 注册中间件
use(fn) {
this.middlewareList.push(fn)
}
// 很重要,用来生成一个koa2服务, 中间件的所有执行都在callback中实现
listen(...args) {
// 中间件的执行入口
const server = http.createServer(this.callback())
server.listen(...args)
}
}
2. 实现中间件函数的嵌套调用以及参数绑定和实现(ctx和next)
这里使用了 dispatch.bind(null, i+1) 来实现中间件中的next。
const http = require('http')
class Koa2 {
constructor() {
// 存储注册的中间件
this.middlewareList = []
}
// 注册中间件
use(fn) {
this.middlewareList.push(fn)
}
createContext(req, res) {
const ctx = {req, res}
ctx.query = req.query
return ctx
}
recursiveMiddleware(middlewareList){
return executionMiddleware(ctx) {
const dispatch = (i) => {
const fn = middlewareList[i]
return fn(ctx, dispatch.bind(null, i+1))
}
return dispatch(0)
}
}
callback() {
// http server的回调函数格式
return (req, res) => {
const ctx = createContext(req, res)
return this.recursiveMiddleware(this.middlewareList)(ctx)
}
}
// 很重要,用来生成一个koa2服务, 中间件的所有执行都在callback中实现
listen(...args) {
// 中间件的执行入口
const server = http.createServer(this.callback())
server.listen(...args)
}
}
module.exports = Koa2
继续优化
按照书写规范,中间件要求是async格式的函数,是一个promise. 所以这里还需要做一下兼容,如果不写async,也要是一个promise:
const http = require('http')
class Koa2 {
constructor() {
// 存储注册的中间件
this.middlewareList = []
}
// 注册中间件
use(fn) {
this.middlewareList.push(fn)
}
createContext(req, res) {
const ctx = {req, res}
ctx.query = req.query
return ctx
}
recursiveMiddleware(middlewareList){
return executionMiddleware(ctx) {
const dispatch = (i) => {
const fn = middlewareList[i]
// 这里使用try、catch来捕获promise中的reject
try{
return await Promise.resolve(fn(ctx, dispatch.bind(null, i+1)))
}catch(err) {
return Promise.reject(err)
}
}
return dispatch(0)
}
}
callback() {
// http server的回调函数格式
return (req, res) => {
const ctx = createContext(req, res)
return this.recursiveMiddleware(this.middlewareList)(ctx)
}
}
// 很重要,用来生成一个koa2服务, 中间件的所有执行都在callback中实现
listen(...args) {
// 中间件的执行入口
const server = http.createServer(this.callback())
server.listen(...args)
}
}
module.exports = Koa2
希望对你有所启发!
原文链接:https://juejin.cn/post/7212924329428795450 作者:阿里阿多