postMessage 消息通信如何 Promise 化?

前言

postMessage Api 想必大家都不陌生,WebWorker 通信会用到,iframe 窗口之间通信也会用到,尤其像一些通过 iframe 嵌入其他项目产品的应用,想要实现实时通信,就少不了它。

但是,监听消息基本都是全局注册事件来接收对应消息,然后在做分发处理的。

假如业务比较复杂,流程比较长,消息通信的频率高,而且通信场景繁琐,那么通过全局事件处理就显得不太合适了。

那么问题来了,我们能不能将 postMessage 进行一次转化,把他变成类似 Promise 的使用方式,这样业务里使用 postMessage 进行通信的时候,就不需要考虑全局回调事件了。而且 Promise 和他的语法糖 await 都可以很好的消除回调地狱的情况。

思考

Promise 化,需要解决那些问题?

  • 业务中发起的事件,如何注册并消费?
  • 相同的事件多次发起,应该如何处理响应结果?

我们先来看业务中发起的事件,如何注册并消费?

举个实际的例子:我们假定有子系统向父级 iframe 获取 loginToken 的行为。

正常情况我们会这样写:

ts 复制代码
const messageEventHandler = (event: MessageEvent) => {
  const data = event.data;
  // 分发事件
  emit(data?.api, data);
}
// 接收
window.addEventListener("message", messageEventHandler);
// 发送
window.parent.postMessage({ api: "getLoginToken" }, "*");

这样写逻辑有问题吗?没有问题!逻辑可以正确的执行,事件也成功的被分发了

通过消息订阅来接收通知,单发的时候看起来没什么问题,假如 getLoginToken 被多次触发,或者说,同一个事件被多次注册,那么还需要考虑业务侧事件执行后销毁,避免重复触发

有没有办法可以减轻业务侧的心智负担呢?

办法总比困难多嘛~

我们可以使用一个全局变量 PostMessageCallBackMap 来存放注册的事件及回调,Promise 中的 resolve 正好有阅后即焚的特性,那么我们是不是可以考虑把创建出来的 resolve 作为 callback 放到 Map 中呢?

想到就立即行动!

实现

改造后的代码如下:

ts 复制代码
export const PostMessageCallBackMap = new Map();
const messageEventHandler = (event: MessageEvent) => {
  // 事件分发
  const data = event.data;
  let eventKey = data?.api;

  if (PostMessageCallBackMap.has(eventKey)) {
    PostMessageCallBackMap.get(eventKey)(data);
  }
};

// 接收
window.addEventListener("message", messageEventHandler);

接收我们写好了,发送这块怎么实现呢?

ts 复制代码
// 发送
function sendMessage<T>(param: JSBridgeReq): Promise<JSBridgeRes<T>> {
  return new Promise((resolve, reject) => {
    if (window.parent) {
      window.parent.postMessage(param, "*");
      // 将当前的 resolve 添加到 Map 中,等待返回事件触发
      let eventKey = param.api;
      PostMessageCallBackMap.set(eventKey, resolve);
    }
  });
}
// 这样封装一下,业务中使用就十分方便了
const { token } = await sendMessage({ api: "getLoginToken" })
console.log(token);

这样看起来舒服了很多,业务中应用起来也顺手了很多,完结撒花~

等等!

还有个问题呐。

相同的事件多次发起,应该如何处理响应结果?

陷入沉思

emm,按上面的写法,多次发送 getLoginToken 事件,Map 中的 Key 是唯一的,之前的事件会被覆盖掉,再加上异步事件返回时间不确定的话,完蛋了啊!

摆烂!

注释加上,不要调多次!

结束!

测试:哦?是吗?(狂点、狂点、狂点)

好啦好啦,虽然我们可以通过防抖来避免测试疯狂轰炸,但是某些场景下真的会有连续触发 api 的情况,那我们怎么解决呢?

每一个回调给他一个唯一id ,然后通过事件id来进行匹配,不管事件执行的时间长短,通过事件id总可以找到另一半。

唯一id,最简单的我们就用时间戳吧。

改造!改造!改造!

ts 复制代码
export const PostMessageCallBackMap = new Map();

// 发送
function sendMessage<T>(param: JSBridgeReq): Promise<JSBridgeRes<T>> {
  return new Promise((resolve, reject) => {
    if (window.parent) {
      param.timestamp = Date.now();
      window.parent.postMessage(param, "*");
      
      let eventKey = param.api + param.timestamp;
      PostMessageCallBackMap.set(eventKey, resolve);
    }
  });
}

// 接收
const messageEventHandler = (event: MessageEvent) => {
  // 事件分发
  const data = event.data;
  let eventKey = data?.api;
  // timestamp 来保证对应关系
  eventKey = data?.api + (data?.timestamp || "");

  if (PostMessageCallBackMap.has(eventKey)) {
    PostMessageCallBackMap.get(eventKey)(data);
    // 由于事件id的存在,Map会越来越大,清除字典防止内存泄漏
    setTimeout(() => {
      PostMessageCallBackMap.delete(eventKey);
    }, 0);
  }
};
window.addEventListener("message", messageEventHandler);

总结

通过这样一番分析改造,终于让难以控制的全局注册事件 postMessage 变成了可以链式调用的 Promise,又可以愉快的写业务代码啦~

演示地址:ztstory.github.io/vue-composi...

源码地址:github.com/ZTStory/vue...

相关推荐
Watermelo6179 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_7482489411 分钟前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5
m0_7482356122 分钟前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O2 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
大圣数据星球3 小时前
Fluss 写入数据湖实战
大数据·设计模式·flink
思忖小下4 小时前
梳理你的思路(从OOP到架构设计)_设计模式Template Method模式
设计模式·模板方法模式·eit
hackeroink5 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者7 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-7 小时前
验证码机制
前端·后端
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js