【源码共读】第5期 | koa-compose

1. 前言

2. 什么是中间件

阅读koa-compose源码前,先了解下什么是中间件。

中间件是一种在应用程序中提供服务的软件。它是在未做出最终响应之前,组成处理请求响应周期的一部分程序处理过程。

在 web 开发中,中间件是指特定的软件组件,这些组件能够处理和转换 HTTP 请求和响应。这些组件可以拦截请求、添加头部信息、进行身份验证等等。Node.js、Express、Koa 等 web 框架中都支持中间件机制。

中间件主要有以下三个特点:

  1. 拦截请求:中间件可以拦截 HTTP 请求,对请求进行前置处理,比如路由匹配、数据校验等。
  2. 修改响应:中间件可以修改 HTTP 响应,比如添加头部信息、重定向请求等。
  3. 链式调用:中间件可以通过串联的方式调用多个中间件,将多个功能组合在一起。

利用中间件,我们可以方便地实现在请求处理过程中添加各种服务,比如记录请求日志、添加缓存、进行流量控制等等。

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

image.png

第一层next加了await

image.png

第二层next加了await

image.png

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)
      }
    }
  }
}

运行流程:

  1. 首先,compose 函数接受一个包含多个中间件函数的数组作为参数 middleware

  2. 在函数的开始部分,有两个错误检查:确保 middleware 是一个数组,并且数组内的每个元素都是函数。

  3. compose 函数返回一个新的函数,这个新函数接受两个参数: contextnext,使用高阶函数。

  4. 每次调用新函数都会调用 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。

  5. 如果存在中间件函数,函数尝试调用该中间件函数,并传入 context 和一个绑定了 i + 1 的新 dispatch 函数作为参数。新的 dispatch 函数在当前中间件调用 next() 时会被触发。

  6. 中间件调用或许抛出错误,如果捕获到任何错误,函数将返回一个被拒绝的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]))
    })
})


  1. 先收集所有中间件

image.png

  1. 执行中间件第一个函数fn,dispatch返回promise包裹后fn执行的结果

image.png

  1. 遇到await next()进入 执行next函数 ,注意此时next函数是传入的dispatch(i+1)就是下一个中间件

image.png

image.png

此时i等于1

image.png

进入第二个中间件内部执行

image.png

遇到next 执行dispatch(i+1) 此时i为2

image.png

进入第三个中间件内部
image.png

遇到next 执行dispatch(i+1) 此时i为3,注意此时middleware中没有了 ,最初传入的为空对象 所以next为undefined,返回promise的结果

image.png

执行最后一层的await next()后的代码

image.png

上一层的next执行完成返回上一层的next后的代码

image.png

image.png

自此全过程执行完毕。

4.3 洋葱模型

中间件的处理过程如下,借鉴别人的图:

image.png

处理过程如剥洋葱一样,所以叫洋葱模型

image.png

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!


参考文章

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYZZouZD' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片