『译』window.close 限制

3,544 阅读7分钟

【原文】:textslashplain.com/2021/02/04/…

小声BB

本文在翻译过程中确保意思传达准确的前提下,会加入很多本人的个人解释(会在括号中写,像这样)和自己的一些吐槽

最近做业务的时候需要去定位 window.close 无响应的问题,谁知道里面涉及到了一些 html 标准与浏览器实现的一些相关纠葛,觉得甚是有趣,所以决定周末花点时间翻译出来和大家分享一下。

文中并不是逐字逐句翻译的,有时候会为了方便大家理解会自己截点图或者做一些自己的解释,不能保证一定是正确的,大家感兴趣的可以参照原文对照阅读。

译文

有时候,前端开发者会非常惊奇地发现 window.close() API 并不能每次都成功关闭浏览器窗口。

当你打开开发者工具里面的 console,他们会发现一条信息

Scripts may close only the windows that were opened by them.

为什么浏览器要限制 close()?

在我们去深入了解哪些因素会制约 close() 方法之前,我们先要明白为什么需要对它有限制。

有时候这种限制会被含糊地解释为 “安全问题“,但核心其实是简单的 “用户体验“ 问题 - 如果一个标签页或者窗口被突然的关掉,用户可能会丢失一些浏览器中一些重要的状态值或者 “前进/后退” 历史记录栈(在 IE 中,我们称之为 TravelLog)。用户可能使用标签页在浏览一系列的搜索记录(搜索一下-打开,回退到搜索结果页面,再打开另一个,类似这种操作),如果某个搜索结果页面可以直接干掉自己和前面的搜索结果页,用户会非常苦恼。

也有一个 反滥用/安全 观点,如果一个浏览器页签可以随便关闭自己,对于一些恐吓软件或一些用户体验滥用(软件)来说,这个规则可能很有用。

标准怎么说?

下面是 HTML 标准部分 dom-window-close 说到:

一个浏览上下文可以用脚本关闭,有两种情况:

  • 这是一个由脚本打开的(区别于用户自己打开) 辅助浏览上下文
  • 这是一个顶层浏览上下文,检索历史记录只有一个 document

这个规则看起来很简单,尽管我加粗的部分隐藏了一点点细节问题。(显而易见,"如果脚本的运行是为了响应用户的一个动作,我们应该怎么做?")

浏览器是怎么做的?

很不幸的是,每个浏览器有不同的行为(点击这里探索),部分原因是标准出来之前,它们已经有各自的实现了。

IE 浏览器

在 IE 浏览器中,使用 window.close 可以悄咪咪地直接关掉一个用 window.open() 打开的 标签页/窗口,IE 不会去尝试检查历史记录是否只有一个 document:一个有大量 TravelLog 的标签页,只要是由 window.open() 打开的,就可以悄咪咪地关。(IE 还允许 HTA documents )无任何限制的关闭自己。

其他情况下,标签页/窗口 不会悄咪咪地关闭:而是弹出下面两种弹框中的一种,弹出哪种取决于要关闭的是否是浏览器窗口的最后一个标签页。

Chromium (Microsoft Edge / Google Chrome / etc)

至于 Chromium 88,如果 新标签页/窗口 有一个 opener 或者 “前进/后退” 历史记录栈 只有一个entrywindow.close()就可以成功关闭。

可以看到,标准要求浏览器的实现之间有微小的差别。

首先,注意我说的是 “有一个 opener”,而不是 “由 script 创建”。回顾一下,opener 值允许一个弹出窗口指向创建它的标签页。

  • 如果用户通过点击一个按钮,Ctrl+Tshift+Click 点击一个链接,或者使用 shell 打开一个 url 等方式打开的新标签页 opener 的值都不会设置。

  • 相反,如果使用 open() 或者一个具名 target(非 _blank)超连接打开的标签,则有 opener 的值。

  • 任何超链接可以通过rel=opener 或 rel=noopener 去指定新标签页的 opener 是否有值。

  • 使用 open() 打开的标签页可以在windowFeatures参数中设置 opener  的值为 null.

你可以从上面列表中看到,链接点击或使用open()调用打开的新标签页,它的opener属性可能有值也无值。这就很让人头秃:Shift+click 点击一个链接,新标签页不能自闭(自己关闭自己),但是用 Left-click 点击同一个链接,就可以自闭。

其次,注意我用词是“entry”而不是 “documents”。大多数情况下,它们指的是同一个东西,但是其实不是。考虑下面这种情况,一个 HTML 顶部有一个包含了页面内容的导航栏,用户点击导航栏中的 "#Section3 "链接,浏览器就会依次向下滚动到该部分。后退/前进栈现在包含两个 entry,都指向同一个 doceumnet。这种情况Chromium阻止了window.close()关闭操作,但(按照标准来说)它不应该。这个长期存在的缺陷在 Chromium 88 中变得更加明显,它现在默认给针对_blank的链接提供noopener行为(意味着无法使用window.close()关闭_blank 标签)。

crbug.com/1170131 追踪了使用统计 “前进/后退” 历史记录栈 内 Document 数量来解决这个 issue 的过程,但是用了一点点 小魔法,因为目前 JavaScript 渲染进程只能拿到 “前进/后退” 历史记录栈 中 entries 数量,拿不到里面的 URL。

Chromium: 用户体验

当 Chrome 拦截了 close() 后,会在 console 中发送一个通知

Scripts may close only the windows that were opened by them.

...但是,不会有任何通知给到用户(不是程序员没事不会开 console 去看),当用户在网页内点击一个叫做 ”关闭“ 按钮,但是页面没有反应,用户可能会一脸懵逼。最近提交的 crbug.com/1170034 建议引入一个类似 IE 的对话框。  (顺带一提,它还设置了一个新的bug归档标准,用漫画形象的表示如果建议的功能落地,不快乐的用户将转化为快乐的用户。 😀

Chromium: 诡异的小 bug

这里有个特别边缘的场景。然而,在过去的五年里,我看到过针对 Chrome 和 Edge 的独立报道,给我整笑了。

如果你 Chromium 设置 ”启动时“ 为 ”继续打开上次打开的网页“,然后打开了一个尝试关闭自己的网页,然后关闭窗口,浏览器就会在每次启动的时候关掉自己。(译者没敢试,有兴趣的可以作死一下) image.png 很难进入这种场景,但是可能会在 Chrome/Edge 90 中复现。

复现步骤:访问 webdbg.com/test/opener…. 点击 Page that tries to close itself 链接。使用 Ctrl+Shift+Delete 然后 清除浏览记录(为了清空 “前进/后退” 历史记录栈)。点右上角的叉叉关闭浏览器。现在,尝试从开始菜单里面打开你的浏览器,浏览器会狂野地把自己关掉。

Safari/WebKit

WebKit 的 代码 和 Chromium 的很类似。(不必惊讶,它们有一脉相承) 除了  不同点在于,它对于由 noopener 触发的导航和由浏览器 UI 创建的导航表现不一致。因此,在Safari中,用户可以在多个不同的同源页面之间点击,但仍然允许使用close()。如果 close() 被阻止,Safariconsole 中会显示:

Can't close the window since it was not opened by JavaScript

Firefox

Chromium 不同,Firefox 正确的实现了标准中的 ”只有一个 Document“。Firefox 调用 IsOnlyTopLevelDocumentInSHistory 然后调用IsEmptyOrHasEntriesForSingleTopLevelPage()枚举了历史列表去检查。如果有超过一个 entry,就会判断这些 entry 是否属于一个 document,如果是,则可以用 close() 关闭。

Firefox 提供了一个about:config的用户设置,名为dom.allow_scripts_to_close_windows,可以绕过 close的默认的限制。

Firefox 阻止close()时,它会向控制台发出一个通知。

Scripts may not close windows that were not opened by script.

这里也有个 18 year-old feature request for Firefox 用弹框代替默认通知的 issue。

结语

umm…. 浏览器可真复杂?

译者结语

截止到发文前,仍然未找到如何解决点击关闭按钮,使用脚本关闭当前标签页的好办法。网上有推荐通过打开空白页的再关闭空白页的 “中转方法”,试试实际效果是空白页也无法使用 close() 关闭。如果大家有什么好的解决方案,可以分享一下。