[关闭]
@gyyin 2020-02-06T15:12:21.000000Z 字数 3691 阅读 333

一个需求引发的对跨域页面通信的一些思索

工作


最近开发中遇到了这么一个需求。在我们这边,有一种快捷支付的场景。但是呢,准备支付的时候可能会遇到用户没有绑定银行卡的情况,这样就需要我们帮用户定向到银行的页面,让用户填完绑卡信息后,再回到我们自己的页面。
对于 PC 端来说,这些倒还好,只是产品提了一个需求,就是在银行页面填完信息后,就关闭银行页面的 Tab(浏览器 Tab),然后通知上一个页面更新状态,而不是重新打开一个新的 Tab 标签页。
对于 APP 端来说,由于银行页面不是我们能控制的,所以我们会在跳到银行页面的时候呢,给银行带一个 url,填完信息后银行会打开这个 url 页面。
所以这样就很明确了,由于我们无法决定银行该怎么跳,而且对接了那么多家银行,到银行页面的入口也会有很多个,所以就需要银行跳到一个中间页,这个中间页是由我们控制的,可以决定跳到我们自己的哪个页面。

  1. 快捷支付页面(也可能是绑卡页面等等) --> 选择绑定要银行卡(带给银行一个 url --> 银行页面 --> 带给银行的 url 页面 --> 跳转回第一个页面

其实 APP 里面的通知倒是好做,客户端给提供 JS Bridge 就行了,我调用 bridge 去跳转之前的页面,难点在于 PC 端。这个中间页在 H5 项目中,和 PC 项目的页面是跨域,再者,要求只通信,而不是新开一个页面。
苦思冥想,想到了几种方案。

websocket 和 EventSource

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

监听 storage 事件

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

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

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

跨域共享 storage

postMessage + iframe 跨域通信

其实吧,就算是跨域,也可以实现 localStorage 共享,只不过麻烦了那么一点儿。
假如我的中间页是 T,那个 PC 页面是 P,通过 iframe + postMessage 也完全可以实现跨域共享 storage。
这个原理是什么呢?首先,将 P 页面当做一个 iframe 嵌入到 T 页面中,在 iframe 的 onload 事件中,通过 postMessage 的形式,将数据传给 P 页面。当然 P 页面收到这个数据后怎么展示也不会影响到我们已经在浏览器中打开的 P 页面,只会影响到 iframe 里面的 P 页面。
但是呢,如果你在 P 页面里面设置 localStorage 呢?这样不管是不是 iframe 里面的页面,都能拿到设置后的 localStorage 了。

image.png-19800.1kB

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

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

P 页面代码:

  1. function App() {
  2. useEffect(() => {
  3. window.addEventListener("message", function(event) {
  4. this.localStorage.setItem('flag', event.data); // 获取到状态后设置 localStorage
  5. }, false);
  6. }, []);
  7. return (
  8. <div className="App">
  9. </div>
  10. );
  11. }

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

轮询 storage

当然,你会说,共享了有什么用啊?P 页面又不知道 localStorage 变化了。
所以就回到了上一个问题,在浏览器兼容的情况下可以监听 storage 事件,但像现在这种不兼容的情况下该怎么办呢?
其实我也没有太好的办法,我和同事说,不如使用 Web Worker 来轮询 localStorage 吧,判断是否有 flag 属性,如果有的话就是已经通知了。
为什么用 Web Worker 呢?因为轮询是比较消耗性能和时间的操作,需要一直在后台跑 setInterval,使用 Web Worker 就能保持占用主线程(虽说是异步,可任务队列早晚还是要执行的,是不是?)

visibilitychange

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

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

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

visibilitychange + 共享 storage

后来,我在回家路上想,为什么还要调用一次后端接口呢?如果在切换的时候去读取已经设置好的 localStorage 怎么样?貌似也是一种不错的办法。
对,这个思路是这样的,基本上延续了上面的共享 storage 思路。
当用户在银行页面填完信息之后,银行会跳转到中间页,中间页 T 会设置 P 为 iframe,然后用 postMessage 通信,此时 P 获取到数据后设置到本地的 localStorage 中。
当然,在一开始的时候 P 页面也会监听 visibilitychange 事件,在回调里面判断 localStorage 中是否有 flag 属性,如果有,就默认是银行绑卡消息通知。
代码如下:

  1. function App() {
  2. useEffect(() => {
  3. const visibilitychange = function(){ //浏览器tab切换监听事件
  4. const flag = localStorage.getItem('flag');
  5. if (flag !== undefined) {
  6. // 更新页面
  7. localStorage.removeItem('flag'); // 获取到数据后需要销毁 localStorage 中的
  8. }
  9. };
  10. const messageHandler = function(event) {
  11. this.localStorage.setItem('flag', event.data); // 获取到状态后设置 localStorage
  12. }
  13. document.addEventListener('visibilitychange', visibilitychange);
  14. window.addEventListener("message", messageHandler);
  15. return () => {
  16. window.removeEventListener('message', messageHandler);
  17. document.removeEventListener('visibilitychange', visibilitychange);
  18. }
  19. }, [])
  20. }

总结

这个需求其实最大的难点并不是这个,而是和客户端之间的各种 js bridge 通信,但是这个技术点呢,是我以前一直都没认真研究过的,这次去好好补了一下关于 postMessage 和 iframe 等相关知识,也算是一种收获了。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注