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);
    }
  }
});
相关推荐
reembarkation2 小时前
使用pdfjs-dist 预览pdf,并添加文本层的实现
前端·javascript·pdf
KenXu2 小时前
F2C-PTD工具将需求快速转换为代码实践
前端
给月亮点灯|3 小时前
Vue3基础知识-setup()、ref()和reactive()
前端·javascript·vue.js
芜青3 小时前
【Vue2手录12】单文件组件SFC
前端·javascript·vue.js
冷冷的菜哥3 小时前
react实现无缝轮播组件
前端·react.js·typescript·前端框架·无缝轮播
csdn_aspnet3 小时前
Windows Node.js 安装及环境配置详细教程
windows·node.js
hrrrrb3 小时前
【Python】字符串
java·前端·python
AAA修煤气灶刘哥3 小时前
Kafka 入门不踩坑!从概念到搭环境,后端 er 看完就能用
大数据·后端·kafka
阿笑带你学前端3 小时前
Supabase云同步架构:Flutter应用的数据同步策略
前端