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;// #1app.use(async (ctx, next)=>{console.log(1)await next();console.log(1)});// #2app.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}`);// 打印结果12321
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.resres.statusCode = 404const onerror = err => ctx.onerror(err)const handleResponse = () => respond(ctx)onFinished(res, onerror)// 执行中间件函数,然后执行响应函数handleResponsereturn 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 = thiscontext.req = request.req = response.req = reqcontext.res = request.res = response.res = resrequest.ctx = response.ctx = contextrequest.response = responseresponse.request = requestcontext.originalUrl = request.originalUrl = req.urlcontext.state = {}return context}}
整个流程:
use注册中间件 => listen监听端口,创建服务 => 监听到请求 => 基于use注册中间件的先后顺序执行中间件 => 最后返回响应
2.2 compose源码
module.exports = composefunction 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是undefinedreturn 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赋值iindex = i// 拿到当前的中间件函数let fn = middleware[i]// 当 i 等于 中间件数组长度, fn = undifinedif (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();}))}))}));};
