koa洋葱模型理解

前置---node服务理解

node服务MVP:本质上就是利用http模块,通过response.end响应请求

从node服务的角度上看,GET、POST等所有请求方法以及接口,只是request.method和request.url的区别,但本质都是走的http.createServer的回调函数,说白了接口定义的方式,无论是url亦或是请求方法的隔离,都是框架层面(koa,express...)基于node的http模块做的上层封装

js 复制代码
const http = require('http')
​
const serverHandler = (request, response) => {
  response.end('Hello World' + '@' + request.method + '@' + request.url)
}
​
http
  .createServer(serverHandler)
  .listen(8888, _ => console.log('Server run at http://127.0.0.1:8888'))

express中间件模式实现

支持:以请求方法+url的形式定义接口 & 多中间件逻辑串联

app.get('/', callback)的方式注册接口,本质上就是通过this.route方法在this.handlers上挂载方法

ts 复制代码
type handlers = {
  GET: Array<() => void>;
  POST: Array<() => void>;
};

callback的返回值即为node服务处理请求的回调,对于每个中间件,都用next方法进行包装增强,目的只有一个,为中间件回调函数增加除了request, response之外的第三个参数next方法,以此控制下一个中间件的调用(是否调用/调用时机)

js 复制代码
class App {
  constructor() {
    this.handlers = {}
​
    this.get = this.route.bind(this, 'GET')
    this.post = this.route.bind(this, 'POST')
  }
​
  route(method, path, ...handler) {
    let pathInfo = (this.handlers[path] = this.handlers[path] || {})
​
    pathInfo[method] = handler
  }
​
  callback() {
    return (request, response) => {
      let { url: path, method } = request
​
      let handlers = this.handlers[path] && this.handlers[path][method]
​
      if (handlers) {
        let context = {}
        function next(handlers, index = 0) {
          handlers[index] &&
            handlers[index].call(context, request, response, () =>
              next(handlers, index + 1)
            )
        }
​
        next(handlers)
      } else {
        response.end('404')
      }
    }
  }
}

使用方式

js 复制代码
const http = require("http");
​
const app = new App();
​
function generatorId(request, response, next) {
  this.id = 123
  next()
}
​
app.get('/', generatorId, function(request, response) {
  response.end(`Hello World ${this.id}`)
})
​
http.createServer(app.callback()).listen(3001, () => {
  console.log("listening on 3001");
});

Koa中间件模式实现

🧅洋葱模型

同上封装,用next方法来包装所有中间件函数,做两件事情:对中间件回调的入参进行改造;对中间件的返回值进行改造

  • 中间件入参改造:第一个参数ctx为所有中间件共享的上下文对象;第二个参数next为调用下一个中间件的控制函数
  • 中间件返回值改造:改造为promise,以实现中间件执行的🧅洋葱模型

next方法需要让中间件的调用顺序满足洋葱模型,算法角度如何理解洋葱模型?只考虑其中一个中间件,以最外层的,也就是第一个中间件为例:

  • 中间件逻辑被await next()分割为上下两部分,上面部分属于"从外到内进入洋葱时经过当前层"执行的逻辑,下面部分属于"从内到外出去洋葱时经过当前层"执行的逻辑,逻辑界限就是是否执行完了下一层的逻辑
  • 站在某个内层中间件的角度思考:自身逻辑是否执行完成这个信息需要通过promise让外层拿到,也就是next返回值为promise,并在中间件回调执行完成后进行resolve即可
js 复制代码
const http = require("http");
​
class App {
  constructor() {
    this.handlers = {};
​
    this.get = this.route.bind(this, "GET");
    this.post = this.route.bind(this, "POST");
  }
​
  route(method, path, ...handler) {
    let pathInfo = (this.handlers[path] = this.handlers[path] || {});
​
    // register handler
    pathInfo[method] = handler;
  }
​
  callback() {
    return (request, response) => {
      let { url: path, method } = request;
​
      let handlers = this.handlers[path] && this.handlers[path][method];
​
      if (handlers) {
        let context = { url: request.url };
        function next(handlers, index = 0) {
          return new Promise((resolve, reject) => {
            if (!handlers[index]) return resolve();
​
            handlers[index](context, () => next(handlers, index + 1)).then(
              resolve,
              reject
            );
          });
        }
​
        next(handlers).then((_) => {
          // 结束请求
          response.end(context.body || "404");
        });
      } else {
        response.end("404");
      }
    };
  }
}

使用方式

js 复制代码
const app = new App();
​
const one = async (ctx, next) => {
  console.log(1);
  await next();
  console.log(2);
};
​
const two = async (ctx, next) => {
  console.log(3);
  await next();
  console.log(4);
};
​
const three = async (ctx, next) => {
  console.log(5);
  await next();
  console.log(6);
};
​
app.get("/", one, two, three); // 请求后输出 1 3 5 6 4 2
​
http.createServer(app.callback()).listen(3001, () => {
  console.log("listening on 3001");
});

洋葱模型实战场景

类似于spring框架中常见的基于aop的切面逻辑,我们可以利用洋葱模型编写一个错误统一处理的中间件:

这个切面统一捕获后续中间件的异常,并记录日志(addLogger)以及错误响应(ctx.body)

js 复制代码
app.use(async (ctx, next) => {
  try {
    await next();
  } catch (error) {
    console.error(error);
    if (ctx.body instanceof PassThrough) { // 流式响应
      const msg = buildErrorResponseBody(error);
      addLogger({
        // ...ctx
      });
      ctx.body.write(`error: ${JSON.stringify(msg)}`);
      setTimeout(() => {
        ctx.body.end();
      }, 1000);
    } else { // 普通响应
      addLogger({
        // ...ctx
      });
      ctx.body = buildErrorResponseBody(error);
    }
  }
});
相关推荐
崔庆才丨静觅28 分钟前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60611 小时前
完成前端时间处理的另一块版图
前端·github·web components
KYGALYX1 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了1 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅1 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法2 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
崔庆才丨静觅2 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment2 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端