1. 前言
-
本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
-
这是源码共读的第5期,链接:koa-compose
2. 什么是中间件
阅读koa-compose源码前,先了解下什么是中间件。
中间件是一种在应用程序中提供服务的软件。它是在未做出最终响应之前,组成处理请求响应周期的一部分程序处理过程。
在 web 开发中,中间件是指特定的软件组件,这些组件能够处理和转换 HTTP 请求和响应。这些组件可以拦截请求、添加头部信息、进行身份验证等等。Node.js、Express、Koa 等 web 框架中都支持中间件机制。
中间件主要有以下三个特点:
- 拦截请求:中间件可以拦截 HTTP 请求,对请求进行前置处理,比如路由匹配、数据校验等。
- 修改响应:中间件可以修改 HTTP 响应,比如添加头部信息、重定向请求等。
- 链式调用:中间件可以通过串联的方式调用多个中间件,将多个功能组合在一起。
利用中间件,我们可以方便地实现在请求处理过程中添加各种服务,比如记录请求日志、添加缓存、进行流量控制等等。
3. Koa中使用的中间件
const { default: axios } = require('axios')
const Koa = require('koa')
const app = new Koa()
// 第一层
app.use(async (ctx, next) => {
console.log(1)
const res = await axios.get('http://localhost:3000/user')
console.log('打印***res', res.data)
await next()
console.log(11)
})
// 第二层
app.use(async (ctx, next) => {
console.log(2)
const res = await axios.get('http://localhost:3000/user')
console.log('打印***res', res.data)
await next()
console.log(22)
})
// 第三层
app.use(async (ctx, next) => {
console.log(3)
const res = await axios.get('http://localhost:3000/user')
console.log('打印***res', res.data)
next()
console.log(33)
})
app.listen(1000, err => {
console.log('启动了')
})
next 没有加await
第一层next加了await
第二层next加了await
4. 源码解析
4.1 源码流程分析
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!')
}
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
运行流程:
-
首先,
compose
函数接受一个包含多个中间件函数的数组作为参数middleware
。 -
在函数的开始部分,有两个错误检查:确保
middleware
是一个数组,并且数组内的每个元素都是函数。 -
compose
函数返回一个新的函数,这个新函数接受两个参数:context
和next
,使用高阶函数。 -
每次调用新函数都会调用
dispatch
并传入0
作为参数,从而开始执行中间件函数链。4.1.
dispatch
函数接收一个参数i
,表示即将被调用的中间件在数组中的索引。4.2. 如果
i <= index
,则抛出异常,因为这说明一个中间件多次调用了next()
函数。4.3. 函数通过
middleware[i]
获取当前的中间件函数fn
。如果i
等于middleware.length
,那么fn
将设置为参数中的next
函数。4.4. 如果没有中间件函数可以执行(比如已经执行完所有中间件,或者
next
函数不存在),函数将立即返回一个解析过的Promise。 -
如果存在中间件函数,函数尝试调用该中间件函数,并传入
context
和一个绑定了i + 1
的新dispatch
函数作为参数。新的dispatch
函数在当前中间件调用next()
时会被触发。 -
中间件调用或许抛出错误,如果捕获到任何错误,函数将返回一个被拒绝的Promise,直到调用完毕。
4.2 源码调试过程
简化测试用例:
'use strict'
/* eslint-env jest */
const compose = require('..')
describe('Koa Compose', function () {
it('should work', async () => {
const arr = []
const stack = []
stack.push(async (context, next) => {
arr.push(1)
await next()
arr.push(6)
})
stack.push(async (context, next) => {
arr.push(2)
await next()
arr.push(5)
})
stack.push(async (context, next) => {
arr.push(3)
await next()
arr.push(4)
})
await compose(stack)({})
expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6]))
})
})
- 先收集所有中间件
- 执行中间件第一个函数fn,dispatch返回promise包裹后fn执行的结果
- 遇到await next()进入 执行next函数 ,注意此时next函数是传入的dispatch(i+1)就是下一个中间件
此时i等于1
进入第二个中间件内部执行
遇到next 执行dispatch(i+1) 此时i为2
进入第三个中间件内部
遇到next 执行dispatch(i+1) 此时i为3,注意此时middleware中没有了 ,最初传入的为空对象 所以next为undefined,返回promise的结果
执行最后一层的await next()后的代码
上一层的next执行完成返回上一层的next后的代码
自此全过程执行完毕。
4.3 洋葱模型
中间件的处理过程如下,借鉴别人的图:
处理过程如剥洋葱一样,所以叫洋葱模型
:
5 Koa 中间件源码缩减版
5.1 Koa中中间件使用
class Koa {
constructor() {
this.middleware = []
}
use(fn) {
// 1. 收集中间件的函数
this.middleware.push(fn)
}
listen(...args) {
this.compose(this.middleware)
}
compose(middleware) {
function dispatch(i) {
if (i >= middleware.length) return Promise.resolve()
const fn = middleware[i]
return Promise.resolve(fn('ctx', () => dispatch(i + 1)))
}
return dispatch(0)
}
}
核心源码就几行,类似责任链模式
两者区别
中间件模式和责任链模式有些相似,但中间件模式通常更加灵活且粒度更小。在中间件模式中,请求处理过程往往是由一系列中间件按照特定的顺序依次处理,每个中间件中都可以完成一些基本的功能,然后再将请求传递给下一个中间件。
责任链模式是一种更为严格的模式,只有当当前对象无法处理请求时,才将请求传递给下一个对象,依次类推,直到有一个对象能够正确的处理请求。这种模式更适用于请求处理流程中的异常处理、错误处理等场景。
5.2 Koa源码实现精简版
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
class Application {
constructor() {
// 收集中间件处理函数
this.middleware = []
// 拷贝一份避免同一个引用
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
}
listen(...args) {
//createServer 回调函数默认传入req,res 通过执行callback 返回处理函数
const server = http.createServer(this.callback())
server.listen(...args)
}
// use 方法 fn 处理函数
use(fn) {
// 1. 收集中间件的函数
this.middleware.push(fn)
}
//4. 创建上下文对象
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
}
// 2.生成对应的处理函数
callback() {
const fnMiddleware = this.compose(this.middleware)
// 处理中间件函数
const handleRequest = (req, res) => {
// 每个请求都会创建一个独立的上下文对象
const context = this.createContext(req, res)
fnMiddleware(context)
.then(() => {
console.log('end')
res.end('my Koa')
res.end(context.body) // 实际数据
})
.catch(err => {
console.log('打印***err', err)
res.end(err.message)
})
}
return handleRequest
}
//3. 异步遍历递归中间件处理函数
compose(middleware) {
// 高阶函数
return function (context) {
// 处理分发的函数内容
const dispatch = index => {
if (index >= middleware.length) return Promise.resolve()
// 拿出函数
const fn = middleware[index]
// 执行 第一个是ctx 第二个是下一个中间件函数
// 不一定是promise 包装成promise 这是next函数
return Promise.resolve(fn(context, () => dispatch(index + 1)))
}
// 返回第一个中间件处理函数 调用
return dispatch(0)
}
}
}
module.exports = Application
6. 总结
通过中间件的学习,可以了解koa中对于中间件的使用,了解了什么是洋葱模型,努力扩宽自己的视野。一起加油O^O!