postMessage 事件触发流向初探

1,827 阅读4分钟

起因

接到一个需求,两个不同源的网页需要同步共享 localStorage,于是我想到了使用 iframe 来嵌入一个中间页,这个中间页就用来共享 localStorage,然后利用 postMessage 来向这两个不同源的网页转发数据。

可是后续做数据转发时遇到的问题却让我又学到了新的知识,虽然我还没找到资料学习完整,但想先提前把这些零散的内容传递给大家。

如何发现的

从这个需求入手,需要三个服务,分别是:A 站点、B 站点和一个中间页 C 站点,其中 A 和 B 都用 iframe 把 C 嵌入进来。

A 发送设置数据的消息给 C,大致内容如下:

// 假设点击某个按钮执行了这个逻辑,put 表示要写数据
const iframePage = document.getElementById('iframe-page');
iframePage.contentWindow.postMessage(
  {
    type: 'put',
    value: 'hello world',
  },
  '*'
);

// 监听消息
window.addEventListener('message', function (e) {
  console.log('A', e)
})

B 用来发送清空数据的消息,大致内容如下:

// 假设点击某个按钮执行这个逻辑,clear 表示要删除数据
const iframePage = document.getElementById('iframe-page');
iframePage.contentWindow.postMessage(
  {
    type: 'clear',
  },
  '*'
);

// 监听消息
window.addEventListener('message', function (e) {
  console.log('B', e)
})

C 接到消息后,操作 localStorage,大致内容如下:

window.addEventListener('message', function (e) {
  if (e.data.type === 'put') {
    window.localStorage.setItem('mydata', e.data.value);
  }
  
  if (e.data.type === 'clear') {
    window.localStorage.removeItem('mydata');
  }
});

// 在这里我需要监听 localStorage 的改动并向父窗口发送消息通知,以便父窗口能够及时做处理
window.addEventListener('storage', function (e) {
    console.log('storage');
    window.parent.postMessage(
      {
        type: 'response',
        value: 'done',
      },
      '*'
    );
});

这时,我点 A 的按钮,发现只有 B 会收到回应消息,点 B 的按钮,只有 A 收到回应消息

我有点莫名其妙,消息不应该是广播给所有父窗口的吗?

然后查文档才看到有这么个提示:

该事件不在导致数据变化的当前页面触发(如果浏览器同时打开一个域名下面的多个页面,当其中的一个页面改变 sessionStorage 或 localStorage 的数据时,其他所有页面的 storage 事件会被触发,而原始页面并不触发 storage 事件)

这波操作可以啊!

可是,我是利用 A 发的消息让 C 改的,数据变化的页面是 C,为啥 A 却收不到呢?现在没查到相关资料,还搞不懂。

接下来越发让我不明白的事情又来了,我以为 storage 的这种表现不稳定,有点不受自己控制的感觉,于是又想到了重写 localStorage 的方法来触发我想要的事件,于是又在 C 站点加上了以下代码:

// 重写 setItem 方法
var orignalSetItem = localStorage.setItem;
localStorage.setItem = function (key, newValue) {
    var setItemEvent = new Event('setItemEvent');
    setItemEvent.newValue = newValue;
    window.dispatchEvent(setItemEvent);
    orignalSetItem.apply(this, arguments);
};

// 重写 removeItem 方法
var orignalRemoveItem = localStorage.removeItem;
    localStorage.removeItem = function () {
    var removeEvent = new Event('removeEvent');
    window.dispatchEvent(removeEvent);
    orignalRemoveItem.apply(this, arguments);
};

// 监听自定义事件
window.addEventListener('setItemEvent', function (e) {
    console.log('setItemEvent');
    window.parent.postMessage(
      {
        type: 'response',
        value: '',
      },
      '*'
    );
});

// 监听自定义事件
window.addEventListener('removeEvent', function (e) {
    console.log('removeEvent');
    window.parent.postMessage(
      {
        type: 'response',
        value: '',
      },
      '*'
    );
});

这时,我点 A 的按钮,发现只有 A 自己能收到回应消息,点 B 的按钮只有 B 自己能收到回应消息。

我突然有种天旋地转的感觉,又好像发现了点什么东西,无奈文档上又没有多余的信息。

我想 storage 事件和自定义事件等等这些事件在 postMessage 中会不会有一种特别的流向机制?

按我的想法,执行 setItem 触发了自定义事件后,window.parent.postMessage 向父窗口发消息时,所有窗口都能收到才对呀,为啥自己发的只有自己能收,而 storage 事件却是自己发的只有别人能收,postMessage 到底是一种什么样的机制呢?


postMessage 这个问题,从表现上来看应该只有 storage 事件是一个特例(A 发消息,C 通过它会发送给其它页面),其它情况下谁发给 C,C 就回应给谁。

因为页面里面的事件类型太多没心思一个个捋,看 HTML 标准的 postMessage 也没有找到,所以就此结案。😬

可以利用 storage 这个特性,使用 localStorage 做两个页面间的数据同步,是解决不同域下 localStorage 共享问题的一个相对简单的方案。

「如果有大佬知道这个问题的个中原理,还望指点」