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);
    }
  }
});
相关推荐
戮漠summer21 分钟前
Missing Semester 计算机教育中缺失的一课 Lecture 01 Shell
开发语言·后端·scala
名字还没想好☜30 分钟前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司1 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
江华森1 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http
IT_陈寒2 小时前
Redis的KEYS命令把我搞崩溃了,改用SCAN才活过来
前端·人工智能·后端
长不胖的路人甲2 小时前
SpringCloud 服务雪崩、熔断、降级
后端·spring·spring cloud
Jackson__2 小时前
为什么 Agent 越聊越慢?聊聊 Context(上下文)管理
前端·agent·ai编程
KaMeidebaby3 小时前
卡梅德生物技术快报|抗体合成:多肽抗体合成工程化方案:Nsp2 保守肽多抗制备与多维度验证
前端·网络·数据库·人工智能·算法
青禾网络3 小时前
前端做音画匹配这件事,我从"随机塞"到"AI 自动对齐"
前端·github
爱折腾的小黑牛3 小时前
小程序多人协作的权限设计:三级权限的实现
后端