yinguangyao / blog

关于 JavaScript 前端开发、工作经验的一点点总结。

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

前端跨域页面通信的终极方案

yinguangyao opened this issue · comments

commented

背景

最近开发中遇到了这么一个需求。在我们这边,有一种快捷支付的场景。但是呢,准备支付的时候可能会遇到用户没有绑定银行卡的情况,这样就需要我们帮用户定向到银行的页面,让用户填完绑卡信息后,再回到我们自己的页面。

对于 PC 端来说,这些倒还好,只是产品提了一个需求,就是在银行页面填完信息后,就关闭当前银行页面,并且通知前一个打开的页面更新绑卡状态。

对于 APP 端来说,由于银行页面不是我们能控制的,所以我们会在跳到银行页面之前给银行带一个回调页面地址,填完绑卡信息后银行会自己打开这个地址。

所以这样就很明确了,我们是无法决定银行该怎么跳,由于对接了很多家银行,所以就需要提供给银行一个统一的中间页来做后续处理。

绑卡页面 --> 选择要绑定的银行卡(带给银行一个回调地址) -->  银行页面 --> 打开回调地址 --> 跳转回绑卡页面

其实 APP 里面的通知倒是好做,客户端给提供 JS Bridge 就行了,我调用 bridge 回退到之前的页面。难点在于 PC 端怎么通知上一个页面更新状态,毕竟中间页(兼容H5和PC)和 PC 页面是跨域的。

苦思冥想,想到了几种方案。

websocket 和 EventSource

这个没啥好说的,需要我这边开个服务和 PC 页面通信,负责开发 PC 页面的我同事也觉得不太行,直接 PASS。

监听 storage 事件

其实我们也不清楚这两个项目最后会不会发到同域名下面,但感觉大概率不是同一个域名。如果是同一个域名下面的话,可以在我的中间页修改 localStorage,在他的 PC 页面监听 localStorage 变化的事件,一旦变化了就判断是否有某个字段,然后解析这个字段,在 PC 页面做响应。代码大概如下。

window.addEventListener('storage', () => {
  if (localStorage.getItem('flag')) {
    console.log(JSON.parse(window.localStorage.getItem('flag')));    
  }
});

但我们查了查,这个 storage 事件不支持 IE 浏览器,我们需要支持到 IE10,所以直接 PASS。

跨域共享 storage

这个听着你会觉得不可能吧?localStorage明明不支持跨域,怎么共享?其实这里还真的有办法实现共享。

postMessage + iframe 跨域通信

其实吧,就算是跨域,也可以实现 localStorage 共享,只不过麻烦了那么一点儿。
假如我有两个页面 P 和 T,通过 iframe + postMessage 也完全可以实现跨域共享 storage。

这个原理是什么呢?首先,将 P 页面当做一个 iframe 嵌入到 T 页面中,在 iframe 的 onload 事件中,通过 postMessage 的形式,将数据传给 P 页面。当然 P 页面收到这个数据后怎么展示也不会影响到我们已经在浏览器中打开的 P 页面,只会影响到 iframe 里面的 P 页面。

但是呢,如果你在 P 页面里面设置 localStorage 呢?这样 iframe 的 P 页面和已经打开的 P 页面 localStorage 就同步了。

image.png-19800.1kB

所以我这里用 create-react-app 创建了两个项目,分别让页面 T 和 P 监听了 localhost:3000localhost:3001
T 页面代码(react):

function App() {
  const iframeLoaded = () => {
    let origin = 'http://localhost:3001';
    const target = document.querySelector('#target').contentWindow;
    target.postMessage('success', origin); // 发送信息
  }
  return (
    <div className="App">
        <iframe src="http://localhost:3001" name="hello, world"frameBorder="0" id="b" style={{'display': 'none'}} id="target" onLoad={iframeLoaded}></iframe>
    </div>
  );
}

P 页面代码:

function App() {
  useEffect(() => {
    window.addEventListener("message", function(event) {
      this.localStorage.setItem('flag', event.data); // 获取到状态后设置 localStorage
    }, false);
  }, []);
  return (
    <div className="App">
    </div>
  );
}

这样就实现了跨域通信,当然真正的跨域 storage 共享不仅是这样的,还需要从 P 页面发送消息给 T 页面,在 T 页面手动设置自己的 localStorage,这样就能保持两端 localStorage 一致,表面上实现了 localStorage 共享。
当然了,前提是 http 头别设置 X-Frame-Options

轮询 storage

当然,你会说,共享了有什么用啊?P 页面又不知道 localStorage 变化了。
所以就回到了上一个问题,在浏览器兼容的情况下可以监听 storage 事件,但像现在这种不兼容的情况下该怎么办呢?

其实我也没有太好的办法,我和同事说,不如使用 Web Worker 来轮询 localStorage 吧,判断是否有 flag 属性,如果有的话就是已经通知了。

为什么用 Web Worker 呢?因为轮询是比较消耗性能和时间的操作,需要一直在后台跑 setInterval,使用 Web Worker 就能保持占用主线程(虽说是异步,可任务队列早晚还是要执行的,是不是?)

visibilitychange

我同事告诉我说,可以监听 Tab 切换,比如 visibilitychange 事件,当前页面出现的时候然后他会去调用后台的接口,来判断是否成功了。我想了想,这还真的是个好主意。

document.addEventListener('visibilitychange',function(){ //浏览器tab切换监听事件
    if(document.visibilityState == 'visible') { //状态判断:显示(切换到当前页面)
        // 切换到页面执行事件
        fetch('/bank_account');
    }else if(document.visibilityState == 'hidden'){//状态判断:隐藏(离开当前页面)
         // 离开页面执行事件
    }
});

后来和成熙讨论的时候,我们都觉得这种方案是比较好的一种,就决定使用这个方案。

visibilitychange + 共享 storage

后来,我在回家路上想,为什么还要调用一次后端接口呢?如果在切换的时候去读取已经设置好的 localStorage 怎么样?貌似也是一种不错的办法。

对,这个思路是这样的,基本上延续了上面的共享 storage 思路。
当用户在银行页面填完信息之后,银行会跳转到中间页,中间页 T 会设置 P 为 iframe,然后用 postMessage 通信,此时 P 获取到数据后设置到本地的 localStorage 中。

当然,在一开始的时候 P 页面也会监听 visibilitychange 事件,在回调里面判断 localStorage 中是否有 flag 属性,如果有,就默认是银行绑卡消息通知。
代码如下:

function App() {
    useEffect(() => {
        const visibilitychange = function(){             //浏览器tab切换监听事件
            const flag = localStorage.getItem('flag');
            if (flag !== undefined) {
                // 更新页面
                localStorage.removeItem('flag'); // 获取到数据后需要销毁 localStorage 中的
            }
        };
        const messageHandler = function(event) {
            this.localStorage.setItem('flag', event.data); // 获取到状态后设置           localStorage
        }
        document.addEventListener('visibilitychange', visibilitychange);
        window.addEventListener("message", messageHandler);
        return () => {
            window.removeEventListener('message', messageHandler);
            document.removeEventListener('visibilitychange', visibilitychange);
        }
    }, [])
}

总结

这个需求其实最大的难点并不是这个,而是和客户端之间的各种 js bridge 通信,但是这个技术点呢,是我以前一直都没认真研究过的,这次去好好补了一下关于 postMessage 和 iframe 等相关知识,也算是一种收获了。
已经很久没时间写文章了,其实最近学到了很多东西,jenkins、react hooks、vue、node 等等,有空了可以整理一些文章。