开源可视化引擎 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 对象。

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

相关推荐
百万蹄蹄向前冲5 分钟前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳58143 分钟前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路1 小时前
GeoTools 读取影像元数据
前端
ssshooter1 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry2 小时前
Jetpack Compose 中的状态
前端
dae bal3 小时前
关于RSA和AES加密
前端·vue.js
柳杉3 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化
lynn8570_blog3 小时前
低端设备加载webp ANR
前端·算法
LKAI.3 小时前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi
刺客-Andy4 小时前
React 第七十节 Router中matchRoutes的使用详解及注意事项
前端·javascript·react.js