1. 前言
1.1 环境
- 操作系统: macOS 11.5.2
- 浏览器: Chrome 94.0.4606.81
- koa-compose 4.2.0
-
1.2 阅读该文章可以get以下知识点
-
2. 开始
2.1 洋葱模型
上图是koa的洋葱模型图,一个http请求,从请求到响应的过程,一层一层进入,然后一层一层出来.可以看到每一个中间件的执行过程
例子:
const Koa = require('koa');
const app = new Koa();
const PORT = 3000;
// #1
app.use(async (ctx, next)=>{
console.log(1)
await next();
console.log(1)
});
// #2
app.use(async (ctx, next) => {
console.log(2)
await next();
console.log(2)
})
app.use(async (ctx, next) => {
console.log(3)
})
app.listen(PORT);
console.log(`http://localhost:${PORT}`);
// 打印结果
1
2
3
2
1
2.2 koa相关源码
地址: github.com/koajs/koa
截取了洋葱模型的相关代码
const compose = require('koa-compose')
module.exports = class Application extends Emitter {
constructor (options) {
super()
options = options || {}
this.middleware = []
this.context = Object.create(context)
}
// 第一步 use用来注册中间件,push到middleware数组中
use (fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
debug('use %s', fn._name || fn.name || '-')
this.middleware.push(fn)
return this
}
// 最后一步 监听端口,注册服务,调用了this.callback创建回调函数
listen (...args) {
debug('listen')
const server = http.createServer(this.callback())
return server.listen(...args)
}
// 处理中间件,创建request函数
callback () {
// compose洋葱模型的核心函数,2.3会具体讲到,fn函数是有参数的,第一个参数ctx(必填),第二个参数next(非必填)
const fn = compose(this.middleware)
if (!this.listenerCount('error')) this.on('error', this.onerror)
const handleRequest = (req, res) => {
// 创建ctx上下文,将http的请求和响应放到了ctx对象中,ctx对象里面有八个参数,可以查看createContext函数
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)
// 执行中间件函数,然后执行响应函数handleResponse
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
}
}
整个流程:
use注册中间件 => listen监听端口,创建服务 => 监听到请求 => 基于use注册中间件的先后顺序执行中间件 => 最后返回响应
2.2 compose源码
module.exports = compose
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!')
}
// 中间件函数的两个参数context, next,第一次执行的时候next是undefined
return function (context, next) {
// last called middleware #
let index = -1
// 默认从第一个中间件开始递归执行执行Promise函数
return dispatch(0)
// Promise函数,并返回promise函数
function dispatch (i) {
// 当i数值越界,抛出异常
// 为了避免在同一个function中多次调用next,如果多次调用
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
// index赋值i
index = i
// 拿到当前的中间件函数
let fn = middleware[i]
// 当 i 等于 中间件数组长度, fn = undifined
if (i === middleware.length) fn = next
// fn不存在时,结束递归
if (!fn) return Promise.resolve()
try {
// resolve函数是一个Promise
// i每次+1,递归调用dispatch
// 里面在调用中间件函数,第一个参数ctx,共享值,第二个参数时next,用来做递归调用
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
源码总共49行代码,实现了koa的洋葱模型
compose的递归代码类似于下面这种
const [fn1, fn2, fn3] = this.middleware;
const fnMiddleware = function(context){
return Promise.resolve(
fn1(context, function next(){
return Promise.resolve(
fn2(context, function next(){
return Promise.resolve(
fn3(context, function next(){
return Promise.resolve();
})
)
})
)
})
);
};