父子窗口通信需求背景
最近在实现一个关联谷歌账户的需求。看到网上的大部分涉及前端方面的实现都有这么个OAuth标准下的流程:
- 打开一个子窗口
- 在子窗口重定向到授权页
- 用户点击授权按钮
- 用户授权后授权页会重定向到一个默认的或者用户自定义的uri。
- 授权完成
一般在授权页被授权后子窗口(授权窗口)都会被关闭。浏览器的每个窗口是禁止关闭当前窗口的,只能关闭由当前窗口打开的其他窗口。有图有真相:
所以想要关闭子窗口就需要父窗口来操作。而何时让父窗口关闭子窗口就需要子窗口在完成用户授权后通知父窗口来关闭自己。这就涉及到父子间窗口的通信。
父子间窗口通信分两种情况
父子窗口同源
浏览器的同源策略还没了解到就请自行Google了。
按照OAuth
流程是不会出现父子窗口同源的现象。但是这里也做一下总结。 父窗口向子窗口通信
子窗口是由父窗口创建的。父窗口可以在打开子窗口后获取到子窗口的引用,通过这个引用可以触发子窗口的方法以此向子窗口传递消息
// parent codelet child_window_handle = null;$('#open-child-win-btn').on('click', () => { child_window_handle = window.open('target_url.html', '_blank', 'width=700, height=500, left=200');})
这个时候有一个子窗口的句柄了(handler)。
而子窗口的页面下有如下方法// child codefunction ProcessParentMsg(msg) { // do something with the msg}
父窗口只需要在调用子窗口的对应方法就可以和子窗口完成通信
// parent codechild_window_handle.ProcessParentMsg('msg_form_parent_window');
子窗口向父窗口通信
子窗口可以通过window对象的opener属性访问到父窗口。并且调用父窗口的方法来完成向上通信。
// child codewindow.opener.ProcessChildMsg();
// parent codefunction ProcessChildMsg(msg) { // do something with msg}
父子窗口同源的情况下,父窗口是可以很大程度的控制子窗口的。除了可以触发子窗口的方法,也可以监听子窗口的事件,onbeforeunload
、onresize
, focus
等等, 但是父子窗口不同源的情况下。父窗口无法执行子窗口下的方法,也无法监听窗口下的事件。之前设想的关闭子窗口的实现方式是在父窗口获得子窗口的句柄然后监听子窗口的onload
,onload之后就调用父窗口的用于关闭子窗口的方法。显然这只能在同源的情况下发生了。
父子窗口不同源
这种情况下父子窗口要通信就需要借助HTML5的message passing
功能了。
父窗口向子窗口通信
直接看示例?,
在父窗口中向子窗口派发消息// parent windowlet child_window_handle = window.open('child_target.html', '_blank', 'width=700, height=500');child_window_handle.postMessage('Msg to the child window', '*');
在子窗口下监听消息
// child windowwindow.addEventListener('message', (e) => { ProcessParentMsg(e.data);});function ProcessParentMsg(msg) { // do something with the msg}
子窗口向父窗口通信
// child windowwindow.opener.postMessage("Message to parent", "*");
// parent windowwindow.addEventListener('message', function(e) { ProcessChildMsg(e.data);}, false);function processChildMsg() { // do something with the message}
总结
当我在实现点击按钮打开授权窗口的时候一直出现窗口被拦截的提示,无法直接打开授权弹窗口。这是因为点击window.open
这个操作是在异步操作的回调里面执行的。默认这种情况下浏览器都会拦截这个新窗口,除非用户设定对这个域名允许任何弹窗。
上可以看到这个解释
The general rule is that popup blockers will engage if window.open or similar is invoked from javascript that is not invoked by direct user action. That is, you can call window.open in response to a button click without getting hit by the popup blocker, but if you put the same code in a timer event it will be blocked. Depth of call chain is also a factor - some older browsers only look at the immediate caller, newer browsers can backtrack a little to see if the caller's caller was a mouse click etc. Keep it as shallow as you can to avoid the popup blockers.
起先当我点击按钮的时候我先去通过网络请求接口获取授权页的连接。在异步回调里获取到了授权页链接。此时再去用window.open
去打开这个链接。这个不是 direct user action
。即使可以相信也是一个比较差的用户体验,因为造成了延迟。所以修改后的方案就是用户点击了关联按钮马上打开一个blank
窗口。同时异步去获取授权页链接。获取后reload打开的授权窗口的地址为获取到的连接。这就不会导致 popup blocked
的现象发生了。
描述到这里可能如果没有授权页开发经验的人可能还是无法理解是怎么关闭子页面的。当用户打开授权页后,用户点击授权按钮
此时页面会跳转到一个用户指定的uri。如果未指定的话,会直接显示authorize code
在窗口中,这通常不是我们想要的。我们需要用这个授权码去换取token
,token是真正可以登录用户账户的临时凭证。所以通常是用户指定一个uri,这个uri可以是一个后端接口,授权窗口被用户授权后会以querystring
的形式带上code的参数跳转到我们提供的uri。这个时候后端接口可以获取到授权码去执行换token的操作。之后接口返回一个text/html
的response,response 返回的内容大致如下:
test 账号关联成功