Safari未安装自定义协议却触发onblur事件的解决方案咨询
解决Safari中自定义协议检测的onblur误判问题
这个问题我之前做自定义协议检测的时候也踩过坑,Safari弹出的那个「无法打开页面」系统提示框,会直接夺走页面焦点触发onblur,导致我们误判成协议已成功打开(毕竟正常打开外部应用也会触发onblur)。下面是几个我亲测有效的解决方案:
方案1:优化焦点判断逻辑(兼容多浏览器)
核心思路是:onblur触发后别急着下定论,延迟一小段时间看看页面会不会重新获得焦点。如果是Safari的弹窗导致的失焦,弹窗关闭后焦点会自动回到页面;如果真的打开了外部应用,页面会一直处于失焦状态。
修改后的代码示例(结合jQuery):
function checkProtocol() { // 创建隐藏的输入框用来聚焦(避免影响页面布局) $('<input>', { id: 'focusInput', type: 'text', css: { background: 'transparent', border: 'none', height: 0, width: 0, position: 'absolute', top: '-100px', // 彻底移出可视区域 outline: 'none' } }).appendTo('body'); const focusEl = $('#focusInput')[0]; let isProtocolValid = false; let blurTimer = null; // 监听页面重新获得焦点 $(window).on('focus', function() { if (blurTimer) { clearTimeout(blurTimer); // 焦点回来了,说明是弹窗搞的鬼,协议没安装 updateResult(false); cleanup(); } }); focusEl.focus(); focusEl.onblur = function() { // 延迟800ms判断,给Safari弹窗留足关闭时间 blurTimer = setTimeout(() => { // 过了延迟还没回焦点,说明真的打开了外部应用 isProtocolValid = true; updateResult(true); cleanup(); }, 800); }; // 触发协议跳转 location.href = protocolStr; // 兜底超时逻辑,防止极端情况 setTimeout(() => { if (!isProtocolValid) { updateResult(false); cleanup(); } }, 1500); // 清理临时元素和事件监听 function cleanup() { focusEl.onblur = null; $(window).off('focus'); $('#focusInput').remove(); clearTimeout(blurTimer); } }
方案2:Safari专属优化——用window.open监听窗口状态
Safari对window.open的处理有个特性:如果协议未安装,调用window.open(protocolStr)要么返回null,要么打开的窗口会立刻关闭;如果协议已安装,外部应用启动后,这个临时窗口会很快进入关闭状态。我们可以通过监听窗口的关闭事件来判断:
function checkSafariProtocol() { // 先判断是不是Safari(排除Chrome伪装的Safari UA) const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); if (!isSafari) { checkChrome(); // 非Safari用你原来的逻辑 return; } const protocolWindow = window.open(protocolStr); let checkTimer = null; checkTimer = setInterval(() => { if (!protocolWindow || protocolWindow.closed) { clearInterval(checkTimer); // 窗口直接关闭或打不开,说明协议没安装 updateResult(false); } else { try { // 尝试关闭窗口,如果能关掉,说明是无效协议的弹窗 protocolWindow.close(); clearInterval(checkTimer); updateResult(false); } catch (e) { // 关不掉,说明外部应用已经启动了,协议有效 clearInterval(checkTimer); updateResult(true); } } }, 300); // 兜底超时,防止定时器卡死 setTimeout(() => { clearInterval(checkTimer); if (protocolWindow && !protocolWindow.closed) { try { protocolWindow.close(); } catch (e) {} } updateResult(false); }, 2000); }
方案3:补充方案——利用iframe的错误事件(可靠性稍弱)
这个方法在部分Safari版本中可能因为安全限制不触发error事件,但可以作为兜底尝试:
function checkProtocolWithIframe() { const iframe = $('<iframe>', { src: protocolStr, css: {display: 'none'} }).appendTo('body'); iframe.on('error', function() { // 加载出错,大概率是协议未安装 updateResult(false); iframe.remove(); }); setTimeout(() => { // 超时没报错,假设协议有效 updateResult(true); iframe.remove(); }, 1000); }
注意:这个方法在iOS Safari上的兼容性不如桌面端,谨慎使用。
总结
- 优先选方案1,兼容多浏览器,能完美避开Safari弹窗的干扰;
- 如果只需要针对Safari做优化,方案2更直接,利用Safari的特性判断更准确;
- 方案3可以作为兜底,但可靠性不如前两个。
内容的提问来源于stack exchange,提问作者alasdair009




