使用Typescript实现中间件模式,顺便发了个npm包

lxf2023-04-11 08:22:01

开启AdminJS成长之旅!这是我参与「AdminJS · 12 月更文挑战」的第12天,点击查看活动详情

什么是中间件模式

通常来说,提到中间件都是用于不同程序之间,比如消息中间件(消息队列);中间件模式是一种软件架构模式,它可以将多个不同的应用程序或系统连接在一起,实现通信和数据交换。中间件通常是独立于应用程序,它充当应用程序和应用程序之间的桥梁,为应用程序提供统一的访问接口。

但是在前端提到的中间件模式,就不得不提express,koa,redux等流行库的洋葱模型了,如下图显示

使用Typescript实现中间件模式,顺便发了个npm包

比如下面这段来自koa的示例代码,这个中间件会在每一个请求调用前后记录时间,在最后输出日志,就像一个洋葱一样,一层层执行


app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

看到这里是不是觉得和AOP[面向切面编程]很相似,关于AOP内容可以看一下我前面的文章Typescript装饰器-AOP

个人觉得前端的中间件也算是AOP的一种实现,都可以用于对业务逻辑进行解耦

如何实现中间件模式

之前写过一个类似koa中间件实现,测试率百分百,可以在前端的业务代码中使用
首先安装依赖

npm install @lujs/middleware

API使用如下


const m1: IMiddleware<string> = (ctx, next) => {
  console.log(1);
  next();
  console.log(6);
};
const m2: IMiddleware<string> = (ctx, next) => {
  console.log(2);
  next();
  console.log(5);
};
const m3: IMiddleware<string> = (ctx, next) => {
  console.log(3);
  next();
  console.log(4);
};

const runner = new MiddlewareRunner<string>();
runner.use(m1);
runner.use(m2);
runner.use(m3);

runner.run('');

// log 1 2 3 4 5 6

IMiddleware的泛型表示context的类型,另外还支持promise的方式调用

const m1: IMiddleware<string> = async (ctx, next) => {
  console.log('async', 1);
  await next();
  console.log('async', 6);
};
const m2: IMiddleware<string> = async (ctx, next) => {
  console.log('async', 2);
  await next();
  console.log('async', 5);
};
const m3: IMiddleware<string> = async (ctx, next) => {
  console.log('async', 3);
  await next();
  console.log('async', 4);
};

const runner = new MiddlewareRunner<string>();
runner.use(m1);
runner.use(m2);
runner.use(m3);

runner.run('').then(() => {
  console.log('finally');
});

比如前文说到的那个例子,在say函数前后都打印日志

const log = () => {
    return  (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
        const oldFn = descriptor.value
        descriptor.value = function (...args:any[]) {
            console.log('在函数执行前');
            const res = oldFn.apply(this, args);
            console.log('在函数执行后', res);
            return res;
        }
    };
}

class A { @log() say() { return 'Hello world' } }

使用中间件可以这样实现

  class A { say() { return 'Hello world' } }

  const m1: IMiddleware<string> = (ctx, next) => {
    console.log('before');
    next();
    console.log('after');
  };
  const say: IMiddleware<string> = (ctx, next) => {
    const world = new A().say()
    console.log(world)
  };
  const runner = new MiddlewareRunner<string>();
  runner.use(m1);
  runner.use(say);
  runner.run('');

实际源码非常简单

export type IMiddlewareNextFunction = () => Promise<any>;

export interface IMiddleware<CTX> {
  (context: CTX, next: IMiddlewareNextFunction): any | Promise<any>;
}

export class MiddlewareRunner<CTX> {
  middleware: IMiddleware<CTX>[] = [];

  middlewareCurrent = 0;

  use = (middleware: IMiddleware<CTX>) => {
    this.middleware.push(middleware);
  };

  run = async (context: CTX) => {
    let err: Error | null = null;
    const next = async () => {
      const middleware = this.middleware[this.middlewareCurrent];
      this.middlewareCurrent += 1;
      if (typeof middleware === 'function') {
        try {
          const p = middleware(context, next);
          if (p instanceof Promise) {
            await p;
          }
        } catch (e) {
          await next();
          err = e as Error;
        }
      }
    };
    await next();

    if (err) {
      throw err;
    }
    return context;
  };
}

代码在链接,期待你的star,谢谢!


推荐阅读
在线等,后端悄悄改了接口文档被我抓住了怎么办?
和后端对线 | 前端如何保存base64字符串为文件
释放生产力 | Yapi,swagger2,swagger3生成请求代码
什么?在React中也可以使用vue响应式状态管理
clean-js | 在hooks的时代下,使用class管理你的状态
clean-js | 手把手教你写一个羊了个羊麻将版
有没有一种可能,你从来都没有真正理解async