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...

相关推荐
悟空瞎说2 分钟前
Git 协作工作流详解:从个人单打独斗到规模化团队协同
前端·git
颜进强5 分钟前
20-Spec-Kit Tasks 是怎么把技术方案拆成可执行任务的?
前端·后端·ai编程
程序员鱼皮5 分钟前
Cursor 零基础实战教程,夯爆了!带你速通 6 大核心能力
前端·后端·ai编程
颜进强6 分钟前
14-Spec-Kit、SDD 和 OpenSpec 到底有什么区别?其实核心思想都一样:先写清楚,再让 AI 干活
前端·后端·ai编程
颜进强6 分钟前
16-Spec-Kit 是什么?先从整体流程机制讲起
前端·后端·ai编程
悟空瞎说10 分钟前
QML 集成 WebView 开发桌面内嵌浏览器实战
前端
Delicate16 分钟前
揭开JS深拷贝的底裤:从递归到循环引用的终极解法
javascript
前端与小赵24 分钟前
快速生成安卓证书并打包生成安卓apk(保姆教程)
android·前端
Cxiaomu29 分钟前
MentorPi A1 底盘接入开发实践:让自研Web系统接管机器人底盘
前端·机器人
创业之路&下一个五年1 小时前
mvvm中v和vm关系,vm中v和m的关系?
java·开发语言·javascript