开源可视化引擎 Meta2d.js 实战 - 事件拦截机制的一种实现

meta2d.js:由乐吾乐科技自主研发的一款 2D 可视化引擎。使用该组件,可以定制开发自己的在线画图编辑器软件,应用于物联网组态、办公、设计等场景(平时画图觉着墨刀、ppt、xmind 等不好用的可以考虑自己基于此组件再造个轮子😆)。

事件,是 meta2d.js 提供的一项很实用的功能。通过对图元事件的管理,我们可以设定非常灵活的业务逻辑。在实际的客户业务中,会存在对某些事件在执行前,进行前置条件的判定。根据前置条件是否满足,来决定是否执行图元的事件。这样的需求场景,类似于 WEB 应用中的请求拦截器。meta2d.js 目前的事件处理机制,虽然支持自定义事件,却不能支持这样的场景。这里将介绍一种事件拦截机制的实现。

要想实现自定义的事件拦截,必须先搞清楚 meta2d.js 的消息通知和事件机制。

一、meta2d.js 的消息通知机制

在了解 meta2d 事件机制之前,必须先明白 meta2d.js 的消息通知机制。meta2d 通过实例的 on 和 off 方法,来订阅和取消订阅通知消息。

rust 复制代码
// fn 事件处理函数
const fn = (event, data) => {};
// 监听一个名为 event 的事件,一旦收到该事件的消息通知,就调用 fn 函数
meta2d.on('event', fn);

// 监听全部消息
meta2d.on('*', fn);

// 取消监听
meta2d.off('event', fn);
meta2d.off('*', fn);

其底层依赖于 Mitt 组件 实现。通过注册事件名和监听函数来实现注册机制。Mitt 组件要点如下:

1、Mitt 在应用程序中,实例化一个总线对象,事件采用发布和订阅模式。所有事件都发布到这个总线对象上。

2、在 Mitt 中,事件处理程序是按照订阅的顺序执行的,并且在执行一个事件处理程序时,无法直接阻止其他事件的触发。 Mitt 并没有提供内置的机制来阻止其他事件的触发。

3、在 Mitt 中,如果使用相同的事件名称多次注册事件处理程序,后续的注册将不会覆盖先前的注册。

从 Mitt 组件的要点,结合上面代码来说明 meta2d 消息通知的逻辑就是:

1、meta2d 的实例,向 Mitt 事件总线订阅了一个名为 event 的事件,一旦收到该消息,将调用 fn 方法。

2、当某个图元组件触发名为 event 的事件时,会向 Mitt 事件总线发布一个名为 event 的消息通知。

3、Mitt 事件总线将消息发送给 event 的订阅者 - meta2d 的实例。

二、meta2d.js 的事件机制

1、事件的注册

每一个 pen 对象,都有一个类型为对象数组的 events 属性。注册事件,本质上就是往 events 里写数据。

rust 复制代码
const pen = {
  events: [
    {
      name: "click",            // 事件名,也可以理解为事件发生时,发送到消息总线的消息名
      action: EventAction.Link, // 执行动作
      where: { type:'comparison',  key: "text", comparison: "==", value: "矩形" }, // 触发条件
      value: "2d.le5le.com",
    },
  ],
};

2、事件的处理

我们结合 meta2d.js 的源码,来理解图元事件的处理流程。

类 Meta2d 的构造函数代码如下:

ini 复制代码
export class Meta2d {
  store: Meta2dStore;
  canvas: Canvas;
  websocket: WebSocket;
  mqttClient: MqttClient;
  websockets: WebSocket[];
  mqttClients: MqttClient[];
  socketFn: (
    e: string,
    // topic: string,
    context?: {
      meta2d?: Meta2d;
      type?: string;
      topic?: string;
      url?: string;
    }
  ) => boolean;
  events: Record<number, (pen: Pen, e: Event) => void> = {};
  map: ViewMap;
  mapTimer: any;
  constructor(parent: string | HTMLElement, opts: Options = {}) {
    this.store = useStore(s8());
    this.setOptions(opts);
    this.setDatabyOptions(opts);
    this.init(parent);
    this.register(commonPens());
    this.registerCanvasDraw({ cube });
    this.registerAnchors(commonAnchors());
    globalThis.meta2d = this;
    this.initEventFns();
    this.store.emitter.on('*', this.onEvent);
  }

  ......
}

这说明,在实例化 meta2d 时,注册了所有事件。只要监听到事件,就调用 this.onEvent 函数。再来看看这个 onEvent 函数的部分内容,以 click 事件为例:

typescript 复制代码
private onEvent = (eventName: string, e: any) => {
    switch (eventName) {

      ......

      case 'click':
        e.pen && e.pen.onClick && e.pen.onClick(e.pen, this.canvas.mousePos);
        this.store.data.locked && e.pen && this.doEvent(e.pen, eventName);
        break;
      
        ......

    }
  };

说明,在收到 click 事件通知时,会调用 对象的 onClick 方法,以及 执行一个 this.doEvent 函数。doEvent 部分代码如下:

ini 复制代码
private doEvent = (pen: Pen, eventName: EventName) => {
    if (!pen) {
      return;
    }

    pen.events?.forEach((event) => {
      if (event.actions && event.actions.length) {
        event.actions.forEach((action) => {
          if (this.events[action.action] && event.name === eventName) {
            this.events[action.action](pen, action);
          }
        });
      } else {
        if (this.events[event.action] && event.name === eventName) {
          let can = !event.where?.type;
          if (event.where) {
            
            ......

          }
          can && this.events[event.action](pen, event);
        }
      }
    });

    ......

    // 事件冒泡,子执行完,父执行
    this.doEvent(this.store.pens[pen.parentId], eventName);
  };

上面代码的核心逻辑,就是循环执行 pen 对象中事件数组中的每个对象的 action 处理:

this.events[event.action](pen, event);

meta2d 内置了一些 action 的处理,在其构造函数中:this.initEventFns (); 代码定义了内置的 action 的具体实现。

了解了消息通知和事件处理机制,我们总结一下事件处理的流程:

1)、进入图纸时,构造一个 meta2d 对象的实例。该实例注册了所有事件的通知。

2)、当图元触发事件时(如 单击事件 click),调用实例的 onEvent 方法,传入了消息名称: click 和 业务数据

3)、调用 doEvent 方法,循环图元的每一个事件,并根据传入的事件名称,来定位和执行事件中定义的 action 实现。

三、事件拦截的实现思路

meta2d 的事件处理本质上是执行一个 pen 对象的所有事件的 action,实现拦截实际上就是拦截这些 action 的执行。但是由于 mitt 的事件机制是一旦有事件,就会执行。因此我们在拦截时,必须禁止其所有的 aciton 执行。那么如何做到这一点?一个实现思路是:执行拦截的事件处理时,先删除其他的事件。在执行完拦截事件后,再恢复其他的事件,并执行。

核心逻辑和代码如下:

1、注册消息监听,这里以单机事件 click 为例:

csharp 复制代码
meta2dInstance.on('click', customEventListener);

2、定义自定义事件监听程序

typescript 复制代码
async function customEventListener(data: any) {
    // 点击图纸时,也会触发click事件,此时pen属性为空
    if (data.pen && meta2dInstance.data().locked > LockState.None) {

        let pen = data.pen as Pen;

        // 获取图元事件列表
        let events = pen.events;

        // 检查是否包含目标事件的处理行为
        let index = indexOfPasswordVerifyAction(events);
        if (index != -1) {

            // 缓存事件列表
            cachedEvents = JSON.parse(JSON.stringify(events));

            // 删除拦截事件,以避免再次触发相同的事件时,造成死循环。将剩余事件赋值给临时变量 otherClientEvents
            events.splice(index, 1);
            let otherClientEvents = events

            // 清空图元的事件列表,以避免执行注册的各个事件。
            pen.events = [];

            ......

            try {
                // 拦截业务方法,在30秒内没反馈结果,则认为拦截失败。
                let nv = await Promise.race([verify(), new Promise((resolve) => {
                    // 在内部设定一个定时器,以防止长期不操作,或者操作超时的情况下,流程可以正常执行下去。
                    setTimeout(() => {
                        // 如果超时,直接返回
                        resolve(VerifyStatus.NO);
                    }, VERIFY_TIMEOUT);
                })]) as number;

                // 拦截通过
                if (nv === VerifyStatus.YES) {
                    // 将除拦截事件的其他事件,赋值给当前图元,并主动触发的事件
                    pen.events = otherClientEvents;
                    // 此处以click为例
                    meta2dInstance.value.emit('click', data);
                    
                }
            } catch (error) {
                console.log('Error:', error);
            }

            // 无论如何,当前流程完成后,都需要重置状态数据,以保证下次点击可以再次执行当前逻辑。
            reset();
        }
    }
}

indexOfPasswordVerifyAction 函数,逻辑是根据拦截事件的名称,查找在 pen 图元的事件列表中,是否存在这个事件,而且事件的 action 是否就是你需要执行的名称,存在返回该事件在数组中的索引。

reset 代码:

ini 复制代码
function reset() {
    .....

    if (cachedEvents.length > 0 && meta2dInstance.value && meta2dInstance.value.store.active[0]) {
        meta2dInstance.value.store.active[0].events = cachedEvents;
    }
}

核心逻辑就是将缓存的原事件列表,重新赋值给当前激活的 pen 对象。

至此,就实现了自定义的事件拦截。

相关推荐
啊~哈20 分钟前
vue3+elementplus表格表头加图标及文字提示
前端·javascript·vue.js
小小小小宇1 小时前
前端小tips
前端
小小小小宇1 小时前
二维数组按顺时针螺旋顺序
前端
安木夕1 小时前
C#-Visual Studio宇宙第一IDE使用实践
前端·c#·.net
努力敲代码呀~1 小时前
前端高频面试题2:浏览器/计算机网络
前端·计算机网络·html
高山我梦口香糖2 小时前
[electron]预脚本不显示内联script
前端·javascript·electron
神探小白牙2 小时前
vue-video-player视频保活成功确无法推送问题
前端·vue.js·音视频
Angel_girl3192 小时前
vue项目使用svg图标
前端·vue.js
難釋懷2 小时前
vue 项目中常用的 2 个 Ajax 库
前端·vue.js·ajax
Qian Xiaoo2 小时前
Ajax入门
前端·ajax·okhttp