源码学习目录

1. 前言

1.1 环境

  1. 操作系统: macOS 11.5.2
  2. 浏览器: Chrome 94.0.4606.81
  3. koa-compose 4.2.0
  4. koa 2.13.1

    1.2 阅读该文章可以get以下知识点

  5. koa洋葱模型原理

    2. 开始

    2.1 洋葱模型

    image.png
    上图是koa的洋葱模型图,一个http请求,从请求到响应的过程,一层一层进入,然后一层一层出来.可以看到每一个中间件的执行过程

例子:

  1. const Koa = require('koa');
  2. const app = new Koa();
  3. const PORT = 3000;
  4. // #1
  5. app.use(async (ctx, next)=>{
  6. console.log(1)
  7. await next();
  8. console.log(1)
  9. });
  10. // #2
  11. app.use(async (ctx, next) => {
  12. console.log(2)
  13. await next();
  14. console.log(2)
  15. })
  16. app.use(async (ctx, next) => {
  17. console.log(3)
  18. })
  19. app.listen(PORT);
  20. console.log(`http://localhost:${PORT}`);
  21. // 打印结果
  22. 1
  23. 2
  24. 3
  25. 2
  26. 1

2.2 koa相关源码

地址: github.com/koajs/koa
截取了洋葱模型的相关代码

  1. const compose = require('koa-compose')
  2. module.exports = class Application extends Emitter {
  3. constructor (options) {
  4. super()
  5. options = options || {}
  6. this.middleware = []
  7. this.context = Object.create(context)
  8. }
  9. // 第一步 use用来注册中间件,push到middleware数组中
  10. use (fn) {
  11. if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
  12. debug('use %s', fn._name || fn.name || '-')
  13. this.middleware.push(fn)
  14. return this
  15. }
  16. // 最后一步 监听端口,注册服务,调用了this.callback创建回调函数
  17. listen (...args) {
  18. debug('listen')
  19. const server = http.createServer(this.callback())
  20. return server.listen(...args)
  21. }
  22. // 处理中间件,创建request函数
  23. callback () {
  24. // compose洋葱模型的核心函数,2.3会具体讲到,fn函数是有参数的,第一个参数ctx(必填),第二个参数next(非必填)
  25. const fn = compose(this.middleware)
  26. if (!this.listenerCount('error')) this.on('error', this.onerror)
  27. const handleRequest = (req, res) => {
  28. // 创建ctx上下文,将http的请求和响应放到了ctx对象中,ctx对象里面有八个参数,可以查看createContext函数
  29. const ctx = this.createContext(req, res)
  30. // 创建请求函数
  31. return this.handleRequest(ctx, fn)
  32. }
  33. return handleRequest
  34. }
  35. handleRequest (ctx, fnMiddleware) {
  36. const res = ctx.res
  37. res.statusCode = 404
  38. const onerror = err => ctx.onerror(err)
  39. const handleResponse = () => respond(ctx)
  40. onFinished(res, onerror)
  41. // 执行中间件函数,然后执行响应函数handleResponse
  42. return fnMiddleware(ctx).then(handleResponse).catch(onerror)
  43. }
  44. createContext (req, res) {
  45. const context = Object.create(this.context)
  46. const request = context.request = Object.create(this.request)
  47. const response = context.response = Object.create(this.response)
  48. context.app = request.app = response.app = this
  49. context.req = request.req = response.req = req
  50. context.res = request.res = response.res = res
  51. request.ctx = response.ctx = context
  52. request.response = response
  53. response.request = request
  54. context.originalUrl = request.originalUrl = req.url
  55. context.state = {}
  56. return context
  57. }
  58. }

整个流程:
use注册中间件 => listen监听端口,创建服务 => 监听到请求 => 基于use注册中间件的先后顺序执行中间件 => 最后返回响应

2.2 compose源码

地址: github.com/koajs/compose

  1. module.exports = compose
  2. function compose (middleware) {
  3. // 判断中间件函数是不是数组,不是数组直接抛出错误
  4. if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  5. // 遍历数组,如果不是函数,抛出错误
  6. for (const fn of middleware) {
  7. if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  8. }
  9. // 中间件函数的两个参数context, next,第一次执行的时候next是undefined
  10. return function (context, next) {
  11. // last called middleware #
  12. let index = -1
  13. // 默认从第一个中间件开始递归执行执行Promise函数
  14. return dispatch(0)
  15. // Promise函数,并返回promise函数
  16. function dispatch (i) {
  17. // 当i数值越界,抛出异常
  18. // 为了避免在同一个function中多次调用next,如果多次调用
  19. if (i <= index) return Promise.reject(new Error('next() called multiple times'))
  20. // index赋值i
  21. index = i
  22. // 拿到当前的中间件函数
  23. let fn = middleware[i]
  24. // 当 i 等于 中间件数组长度, fn = undifined
  25. if (i === middleware.length) fn = next
  26. // fn不存在时,结束递归
  27. if (!fn) return Promise.resolve()
  28. try {
  29. // resolve函数是一个Promise
  30. // i每次+1,递归调用dispatch
  31. // 里面在调用中间件函数,第一个参数ctx,共享值,第二个参数时next,用来做递归调用
  32. return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
  33. } catch (err) {
  34. return Promise.reject(err)
  35. }
  36. }
  37. }
  38. }

源码总共49行代码,实现了koa的洋葱模型
compose的递归代码类似于下面这种

  1. const [fn1, fn2, fn3] = this.middleware;
  2. const fnMiddleware = function(context){
  3. return Promise.resolve(
  4. fn1(context, function next(){
  5. return Promise.resolve(
  6. fn2(context, function next(){
  7. return Promise.resolve(
  8. fn3(context, function next(){
  9. return Promise.resolve();
  10. })
  11. )
  12. })
  13. )
  14. })
  15. );
  16. };

3. 总结

  1. koa的application.js的源码部分代码量很少,几个主要的函数
    1. use 注册中间件
    2. listen 监听端口启动服务
    3. createContext 创建ctx对象
    4. handleRequest 处理请求和中间件
    5. respond 处理响应,由于和洋葱模型无关,就没有细讲,主要是处理状态,还有body的json序列化,还有异常处理
  2. compose函数主要就是promise.resolve的递归调用,因此是一个异步的调用过程

    4. 参考

  3. 学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理