JavaScript的一种跨标签页的通信方式
JavaScript
的一种跨标签页的通信方式。
刚好这个星期在搞第三方账号接入我们的一个系统。
原型很简单,就是点击某个按钮,弹出一个第三方的账号接入页面,可能是输入账号,也可能是扫码等等。
执行完授权操作之后,自动关闭授权窗口,然后局部刷新原来页面的授权账号列表。
这个可以用掘金的授权登录来看,就类似下面这种类型:
刚开始完全懵逼的好吧,没做过这种需求,只会登录注册的我流下了悔恨的泪水,呜呜呜😭。
从原型来看,转为代码需要解决的点有三个:
- 怎么生成这样的小窗口;
- 怎么在授权之后自动关闭;
- 怎么往授权页传输数据;
- 怎么在授权之后通知到原来的页面;
生成一个小窗口
平时可能为了新开一个标签页,可能会使用两种方式:
- 使用
window.open(href, "_blank")
来新开一个窗口; - 构建一个
<a href="href" target="_blank"/>
标签,然后使用click
方法模拟点击,新开一个窗口;
第二个感觉没戏,所以就把目光转向window.open
,去MDN
查了下,发现window.open
可以打开小窗口!
通过window.open
的第三个参数,可以选择窗口的一些配置,常用的如下:
width
指定窗口宽度height
指定窗口高度top
窗口左上角距离屏幕左上角的高度偏移left
窗口左上角距离屏幕左上角的宽度偏移menubar
是否显示菜单栏toolbar
是否显示工具栏status
是否显示状态栏scrollbars
是否显示滚动条- …
由于第三个参数需要以字符串的形式来传入,比如指定宽高的话就为'width=1000,height=500'
。
字符串还是比较不方便的,我们可以使用对象来传入,然后稍微处理下:
1 | function openUrl(url, name, config = {}) { |
然后我们可以尝试一下:
1 | openUrl("https://www.baidu.com", "百度", { |
效果如下:
看起来还不错,那么第一个问题我们就解决了。
授权之后自动关闭
有window.open
,感觉应该就有window.close
,去MDN
搜了搜,发现还真有,真好对应了窗口的关闭。
MDN
对window.close
的解释如下:
该方法只能由
window.open
方法打开的窗口的window
对象来调用。如果一个窗口不是由脚本打开的,那么,在调用该方法时,JavaScript
控制台会出现类似下面的错误:
不能使用脚本关闭一个不是由脚本打开的窗口。 或Scripts may not close windows that were not opened by script.
。
也就是说,如果我们想关闭一个打开的窗口,前提是这个窗口是由我打开的
那么我打开的这个窗口的 window
对象怎么获取呢,很简单,就是 window.open
的返回值
我们可以打印下
确实是一个 window
对象,而且也存在 close
方法
我们试下调用 close
方法能不能关闭这个窗口
贴下整体的代码(使用Vue3)
1 | <script setup lang="ts"> |
没有问题,如果我们对自身直接使用 window.close
会有什么现象呢
直接开控制台试一下
发现出现了警告⚠️,而且标签页也没有被关闭,警告的大致意思就是只能又打开的窗口进行关闭
那么第二个问题基本解决,接下来就是最后一个问题
怎么往授权页传输数据
在授权页我们进行登录,登录成功或者失败,我们都要通知到打开它的那个窗口
比如登录成功了,通知父窗口局部刷新,或者发送数据给父窗口,父窗口根据这个数据获取用户信息等等
刚开始,我想,既然父窗口拿到了子窗口的 window
对象,那么是否可以直接在上面挂在属性呢
类似与 window.val = 1
的形式
so,我们写出了如下的代码
1 | // ... |
发现确实可以往上面挂在属性,控制台也正确地输出了
正当我觉得应该可以实现的时候,在子窗口里面是打印不出来这个值的
这条路行不通,那怎么办呢,其实方法很简单,我们通过url来传递我们需要传递的数据,然后在子页面中对 url
进行解析即可
比如我们想往授权页传输用户的 id
,我们可以打开一个 'http://target.com?userId=1'
我们可以稍稍的封装一下函数
1 | const openUrl = ( |
打开之后
发现 url
后面带上了参数
然后我们在子页面中通过 window.location.search
即可拿到参数部分,然后进行解析就 ok
了
怎么在授权之后通知到原来的页面
父窗口向子窗口传递数据搞定了,那么子窗口怎么向父窗口传递数据呢
像这次的这个第三方接入的需求,需要第三方账号登录成功后通知父窗口发送请求来刷新局部的数据
通过父向子传递数据我们知道,直接往 window
上挂在可能是行不通的
不过光说无用,得试试,那么首先要解决怎么在子窗口中拿到父窗口的 window
对象
这时候就要使用 window
上的 opener
属性了,它会返回打开它自身的 window
对象
如果这个窗口不是由另一个窗口打开的,那么这个值为 null
那么怎么证明这子窗口里面这个 window 就是父窗口的 window 呢
可以先在父窗口挂个属性,看子窗口能不能拿到
可是当我想直接在子窗口看看能不能读到父窗口的属性的时候,报错了
从文字不难看出,是跨域错误,两个不同域的窗体对象不能访问对方属性
从而也就证实了往 window 上挂载属性是不可行的
当然即使往 window 上挂载数据可行,从另一个角度想,用户在子窗口进行登录注册的动作是不确定的
也就是说父窗口无法精确地判断子窗口究竟有没有完成动作
也就是需要启动一个定时器来轮询子窗口 window 对象上的某个属性
这样的代码看起来就不符合逻辑
应该有点类似于发布订阅的类型,子窗口发送一个事件,父窗口往这个事件上注册回调,从而精确地判断子窗口完成动作的时间
那么在 js 里面,有没有这样的 api 呢
有的,它就是 message
事件和 postMessage
方法
postMessage
允许我们往另一个窗体上发送 message
事件以及传递相关的数据
window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为https),端口号(443为https的默认值),以及主机 (两个页面的模数 Document.domain设置为相同的值) 时,这两个脚本才能相互通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。 - MDN
它的函数签名有两个
postMessage(message: any, targetOrigin: string, transfer?: Transferable[]): void
postMessage(message: any, options?: WindowPostMessageOptions): void
这两个其实一样,只不过把参数 targetOrigin
和 transfer
放到 options
里面而已
发送 ok 了,那么就是监听 message 事件了
通过 window.addEventListener('message', callback)
来监听
回调的 event
参数有三个属性
data
: 从其他window
中传递过来的对象。origin
: 调用postMessage
时消息发送方窗口的origin
. 这个字符串由 协议、”://“、域名、”:端口号”拼接而成。source
: 对发送消息的窗口对象的引用; 您可以使用此来在具有不同origin
的两个窗口之间建立双向通信。
那么现在就可以试一试了
前面我们已经在子窗口中拿到父窗口的对象了
我们先监听父窗口的 message
事件
接着我们使用子窗口的 postMessage
来往父窗口发送 message
事件
发现成功拿到了数据
这里需要注意是以 window.opener
返回的其他窗口的对象来使用 postMessage
方法