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

相关推荐
岳哥i3 小时前
vue鼠标单机复制文本
javascript
jacGJ3 小时前
记录学习--文件读写
java·前端·学习
毕设源码-赖学姐3 小时前
【开题答辩全过程】以 基于WEB的实验室开放式管理系统的设计与实现为例,包含答辩的问题和答案
前端
幻云20103 小时前
Python深度学习:从筑基到登仙
前端·javascript·vue.js·人工智能·python
我即将远走丶或许也能高飞5 小时前
vuex 和 pinia 的学习使用
开发语言·前端·javascript
钟离墨笺6 小时前
Go语言--2go基础-->基本数据类型
开发语言·前端·后端·golang
爱吃泡芙的小白白6 小时前
Vue 3 核心原理与实战:从响应式到企业级应用
前端·javascript·vue.js
卓怡学长6 小时前
m115乐购游戏商城系统
java·前端·数据库·spring boot·spring·游戏
码上成长7 小时前
JavaScript 数组合并性能优化:扩展运算符 vs concat vs 循环 push
开发语言·javascript·ecmascript
国强_dev7 小时前
量体裁衣在技术方案中的应用
设计模式·系统架构