谷歌决定删除在跨域iframe中触发alert()、confirm()和prompt()的功能,这是一个荒谬而主观的决定,他们称之为“特性”,并且解释非常粗糙——请参见下面的“动机”。删除如此重要的功能的理由非常微弱!社区和开发者应该抗议!
问题
https://www.chromestatus.com/feature/5148698084376576
特性:移除跨域iframe中的alert()、confirm()和prompt()
Chrome允许iframes触发JavaScript对话框,当iframe与顶级框架相同时,它会显示“ says ...”,当iframe跨源时,它会显示“嵌入式页面在该页面上说...”。当前的UX很不清晰,并且以前曾导致过虚伪,其中网站假装消息来自Chrome或另一个网站。删除跨域iframe触发UI的支持将防止这种欺骗,以及阻止进一步的UI简化。
动机
当前的JS对话框UI(不仅限于跨域子框架情况)很不清晰,因为消息看起来像浏览器自己的UI。这导致了虚伪(尤其是使用window.prompt时),其中网站假装特定消息来自Chrome(例如1,2,3)。 Chrome通过在消息前加上“ says…”来减轻这些虚伪。但是,当这些警报来自跨域iframe时,UI甚至更加混乱,因为Chrome试图解释对话框不是来自浏览器本身或顶层页面。鉴于跨域iframe JS对话框的低使用率,当使用JS对话框时,通常不需要用于站点的主要功能,并且难以可靠地解释对话框的来源,因此我们建议删除跨域iframe的JS对话框。这也将解除我们进一步简化对话框的能力,例如删除主机名指示并使对话框更明显地成为页面(而不是浏览器)的一部分,方法是将其移到内容区域的中心。这些变化被阻止,因为我们需要先删除JS对话框的跨域支持,否则这些子框架可以假装他们的对话框来自父页面。
解决方案
通过Window.postMessage()
在 iframe 中向父页面发送消息并通过父页面显示对话框,这是一种非常优雅的解决方法。谷歌的做法很可耻,因为在 Chrome 92 版本之前,客户端会看到正确的警告对话框,例如:“嵌入式页面iframe.com
说:...”(客户端看到了调用警告的真实域),但现在使用 postMessage 方式,客户端将会看到一个谎言,例如:“页面example.com
说:...”,但警报不是由 example.com 调用的。愚蠢的谷歌决定导致他们取得了相反的效果——客户端现在会更加困惑。谷歌的决定过于草率,没有考虑后果。对于 prompt()
和 confirm()
,通过 Window.postMessage()
实现有点棘手,因为我们需要从顶部将结果发送回 iframe。
谷歌接下来会怎么做?禁用 Window.postMessage()
吗?似曾相识。我们又回到了 IE 时代......开发人员浪费时间进行愚蠢的黑客行为。
TL;DR: 演示
https://domain-a.netlify.app/parent.html
代码
通过下面的代码,你可以在跨源 iframe 中使用重写后的本机 alert()
、confirm()
和 prompt()
,并且最小化修改代码。对于 alert()
的使用没有任何更改。对于 confirm()
和 prompt()
,只需在它们之前添加“await”关键字,或者在无法轻松将同步函数切换为异步函数的情况下,可以自由使用回调方式。请参见下面的 iframe.html
中的所有用法示例。
每一件坏事都伴随着一些好处——现在我通过这种解决方案获得了一个优势,即 iframe 域未公开(对话框中现在使用地址栏中的域)。
https://example-a.com/parent.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Parent (domain A)</title>
<script type="text/javascript" src="dialogs.js"></script>
</head>
<body>
<h1>Parent (domain A)</h1>
<iframe src="https://example-b.com/iframe.html">
</body>
</html>
https://example-b.com/iframe.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Iframe (domain B)</title>
<script type="text/javascript" src="dialogs.js"></script>
</head>
<body>
<h1>Iframe (domain B)</h1>
<script type="text/javascript">
alert('alert() forwarded from iframe.html');
confirm('confirm() forwarded from iframe.html via callback', (result) => {
console.log('confirm() result via callback: ', result);
});
prompt('prompt() forwarded from iframe.html via callback', null, (result) => {
console.log('prompt() result via callback: ', result);
});
(async () => {
var result1 = await confirm('confirm() forwarded from iframe.html via promise');
console.log('confirm() result via promise: ', result1);
var result2 = await prompt('prompt() forwarded from iframe.html via promise');
console.log('prompt() result via promise: ', result2);
})();
</script>
</body>
</html>
dialogs.js
(function() {
var id = 1,
store = {},
isIframe = (window === window.parent || window.opener) ? false : true;
var sendMessage = function(windowToSend, data) {
windowToSend.postMessage(JSON.stringify(data), '*');
};
var processInteractiveDialog = function(data, callback) {
sendMessage(parent, data);
if (callback)
store[data.id] = callback;
else
return new Promise(resolve => { store[data.id] = resolve; })
};
if (isIframe) {
window.alert = function(message) {
var data = { event : 'dialog', type : 'alert', message : message };
sendMessage(parent, data);
};
window.confirm = function(message, callback) {
var data = { event : 'dialog', type : 'confirm', id : id++, message : message };
return processInteractiveDialog(data, callback);
};
window.prompt = function(message, value, callback) {
var data = { event : 'dialog', type : 'prompt', id : id++, message : message, value : value || '' };
return processInteractiveDialog(data, callback);
};
}
window.addEventListener('message', function(event) {
try {
var data = JSON.parse(event.data);
}
catch (error) {
return;
}
if (!data || typeof data != 'object')
return;
if (data.event != 'dialog' || !data.type)
return;
if (!isIframe) {
if (data.type == 'alert')
alert(data.message)
else if (data.type == 'confirm') {
var data = { event : 'dialog', type : 'confirm', id : data.id, result : confirm(data.message) };
sendMessage(event.source, data);
}
else if (data.type == 'prompt') {
var data = { event : 'dialog', type : 'prompt', id : data.id, result : prompt(data.message, data.value) };
sendMessage(event.source, data);
}
}
else {
if (data.type == 'confirm') {
store[data.id](data.result);
delete store[data.id];
}
else if (data.type == 'prompt') {
store[data.id](data.result);
delete store[data.id];
}
}
}, false);
})();