【typescript工具库】带你在生产环境封装职责链模式

前言

我发现网上有很多人都在说职责链模式,但是却没有人将其抽象出来,或者说抽象的程度并不能达到足以上生产环境的程度。刚好项目中有需要用到,正好做一个业务场景的复盘

项目背景

先简单说一下用到这设计模式的需求的背景,也算是我们这个项目的复盘吧,我们当时需要做一个平台,类似于文件管理和模型管理。然后我们需要判断上传文件类型.其中有xml,普通文件(video,img),model,图纸(dwg格式),然后进行不同的操作。在这期间需要跟bimface 和 阿里oss 以及我们研究院的后端进行交互。由于业务隐私问题,这里我用的是几个版本前上传xml的解析图(部分)

我们有一个请求次数 = num(task)。而这个task 的数量有足足1000以上。也就是说有1000+的请求在这个上传过程中需要被处理的。这一部分的处理如果大家感兴趣的话以后会讲。我举这一个例子是为了说明说在这个链路中,只要有一个流程或者一个promise请求失败了,那么就整体失败了。并且这个xml图还只是上传与xml逻辑中的一环。

我们看到单单xml上传这里一系列交互过程,涉及到的请求已经非常多了。假如我们用常规逻辑进行判断,不难想象到后期维护起来有多酸爽。那么我们怎么构建整个链路的处理逻辑呢。职责链就可以有用武之地了。不妨我们写一个工具类

职责链模式

简介

职责链模式是一种行为设计模式,它允许你将请求沿着处理链传递,每个处理器都有机会处理请求或将其传递给下一个处理器。

实现职责链模式

数据结构

我们要实现一个职责链,我们首先应该想到的是,我们的代码的数据结构需要怎么进行定义。我们可以发现我们职责链的逻辑是沿着处理链传递的,学过算法的朋友应该反应的过来,这其实是一个链表。

链表的最简示例如下

kotlin 复制代码
  class ListNode {
      val: number
      next: ListNode | null
      constructor(val?: number, next?: ListNode | null) {
          this.val = val
          this.next = next
      }
  }

好的,接下来就是如何组织代码了,这里我们用 ChainData 代替 val(通过dataSet进行设置),然后用 asyncNext代替我们上面的next方法.这是数据结构的问题。

节点的值

我们接着聊一下数据的传递吧,我们在链表中单个链表中各个节点所存储的数据不一样。这里我们也是但是区别在于我们这个需要应用于业务,因此我们的数据为了持久化,各个节点存储的数据应该是增量的。

节点跳转

我们在一个事件处理完后,同步的代码我们可以通过return 值直接监听到,异步的逻辑我们在完成后却不能直接监听到,因此我们在使用的时候需要手动指定回调。也就是asyncNext方法。我们在定义class中也需要简单判断一下是不是同步方法。看看是否执行继续下一个处理器

错误事件 | 自定义事件

这一部分其实基于我们的发布者订阅者模式,关于事件,除了错误事件,还有完成事件和进度条事件之类的可以加上去。这一部分也会暴露给使用者

最后使用 TypeScript 来实现一个职责链模式吧。这个模式包含以下核心部分:

  • Chain 类:用于创建职责链。
  • ChainData :每一个链表中的数据。
  • emit 方法:用于触发事件。
  • dataSet 方法:用于初始化数据。
  • asyncNext 方法:用于执行下一个处理器。
  • nodeSet 方法:用于设置下一个处理器。
  • passRequest 方法:用于执行当前处理器并决定是否继续下一个处理器。

职责链代码

kotlin 复制代码
type OrderResult = 'chainNext' | any;
​
type OrderHandler = (...args: any[]) => OrderResult;
type emitNameType = 'finish' | 'error';
type ChainDataType = {
  eventBus?:{
    finish:Array<Function>
    error:Array<Function>
  }
  [key:string]:any
}
/**
 * @des 链式调用数据
 */
class Chain {
  private fn: OrderHandler;
  private nodeNext: Chain | null;
  // 一般会被当作初始数据
  ChainData: ChainDataType;
  constructor(fn: OrderHandler) {
    this.fn = fn;
    this.ChainData = {};
    this.nodeNext = null;
  }
​
  /**
   * @des 触发某一个事件
   * @param name
   * @param data 给function的值
   */
  emit = (name: emitNameType, data: any) => {
    if(this.ChainData.eventBus){
      if (this.ChainData.eventBus[name]) {
        this.ChainData.eventBus[name].forEach((element: Function) => {
          element(data);
        });
      } else {
        throw new Error('没有这个事件');
      }
    }
  };
​
  /**
   * @des 初始化数据
   * @param data
   */
  dataSet(data: ChainDataType): void {
    this.ChainData = data;
  }
  /**
   * @des settimeout的 异步
   * this.fn(...args) 不会返回值那么就要放到后面去 | 避免 return 判断。因为不会return 值
   * @param args
   * @returns
   */
  asycNext(): OrderResult {
    if (this.nodeNext) {
      this.nodeNext.dataSet(this.ChainData);
      return this.nodeNext.passRequest();
    }
    return this.fn();
  }
​
  nodeSet(nodeNext: Chain): void {
    this.nodeNext = nodeNext;
  }
​
  passRequest(): OrderResult {
    // 执行这个方法
    const res = this.fn();
    if (res === 'chainNext') {
      if (this.nodeNext) {
        // 所有的函数都要用 chain 方法包起来,否则没有 this.nodeNext
        this.nodeNext.dataSet(this.ChainData);
        return this.nodeNext.passRequest();
      }
    }
    if (this.nodeNext) {
      this.nodeNext.dataSet(this.ChainData);
    }
​
    return res;
  }
}

使用示例

  • 第一步:校验数据
  • 第二步:业务逻辑
  • 第三步:结束事件
typescript 复制代码
export interface ExtendedChainData extends Chain{
  ChainData:{
    init:{
      xml_id:number,
      model_id:number
    }
    res:any
  }
}
​
/**
 * @des 异步数据
 * @returns 
 */
const sleep = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        code: 200,
        data:[
          {id:1,name:"小明"}
        ]
      });
    }, 1000);
  });
};
​
/**
 * @des 初始化数据
 * @param this 
 * @returns 
 */
function initPost(this:ExtendedChainData) {
  console.log('数据校验:');
  if(!this.ChainData.init){
    this.emit("error","使用者触发error事件")
    return
  }
  return 'chainNext';
}
​
​
/**
 * @des 异步逻辑
 * @param this 
 */
async function asyncPost(this:ExtendedChainData) {
  let data = await sleep() 
  this.ChainData.res = data
  this.asycNext()
}
​
​
/**
 * @des 异步逻辑
 * @param this 
 */
 async function lastPost(this:ExtendedChainData) {
  this.emit("finish","使用者触发finish事件")
}
​
​
​
// 1. 初始化 链条
const chainInitPost = new Chain(initPost);
const chainAsyncPost = new Chain(asyncPost);
const chainLastPost = new Chain(lastPost);
// 2. 设置关系 注意异步 用await 处理逻辑后调用 asyncNext 方法
chainInitPost.nodeSet(chainAsyncPost);
chainAsyncPost.nodeSet(chainLastPost);
// 3. 开始执行。从一开始的链条开始执行
chainInitPost.dataSet({
  init:{
    xml_id:"1111111111",
    model_id:"22222222"
  },
  eventBus:{
    finish:[
      (e:any)=>{
        console.log(e)
        console.log(chainInitPost.ChainData.res)
      }
    ],
    error:[
      (e:any)=>{
        console.log(e)  
      }
    ]
  }
})
chainInitPost.passRequest();

输出

css 复制代码
数据校验:
使用者触发finish事件
{ code: 200, data: [ { id: 1, name: '小明' } ] }

代码链接:github.com/yilaikesi/u...

相关推荐
豆豆43 分钟前
为什么用PageAdmin CMS建设网站?
服务器·开发语言·前端·php·软件构建
JUNAI_Strive_ving1 小时前
番茄小说逆向爬取
javascript·python
看到请催我学习1 小时前
如何实现两个标签页之间的通信
javascript·css·typescript·node.js·html5
twins35202 小时前
解决Vue应用中遇到路由刷新后出现 404 错误
前端·javascript·vue.js
qiyi.sky2 小时前
JavaWeb——Vue组件库Element(3/6):常见组件:Dialog对话框、Form表单(介绍、使用、实际效果)
前端·javascript·vue.js
煸橙干儿~~2 小时前
分析JS Crash(进程崩溃)
java·前端·javascript
哪 吒2 小时前
华为OD机试 - 几何平均值最大子数(Python/JS/C/C++ 2024 E卷 200分)
javascript·python·华为od
安冬的码畜日常2 小时前
【D3.js in Action 3 精译_027】3.4 让 D3 数据适应屏幕(下)—— D3 分段比例尺的用法
前端·javascript·信息可视化·数据可视化·d3.js·d3比例尺·分段比例尺
l1x1n03 小时前
No.3 笔记 | Web安全基础:Web1.0 - 3.0 发展史
前端·http·html
Q_w77423 小时前
一个真实可用的登录界面!
javascript·mysql·php·html5·网站登录