背景
由于产品想要在一个webview里以切换tab的方式同时阅读两篇图文来判断是否存在洗稿,鉴于重复实现一次图文逻辑比较蛋疼且难以维护,所以打算采用嵌入两个iframe的方案来实现,理想结果是记住两个iframe各自的滚动位置,在切换时可以回到原来的位置继续阅读。
理想是丰满的,可惜现实总是骨感的,在下文会阐述这个骨感的踩坑之旅。
iframe自身的滚动表现
如果iframe本身已支持记住滚动位置的话,那么这个case就可以快乐地结束了
<body>
<iframe id="js_iframe1" scrolling="yes" src="article1.html"></iframe>
<iframe id="js_iframe2" scrolling="yes" src="article2.html" style="display: none;"></iframe>
</body>
可以扫这个二维码看真机表现:
结果当然是不符合预期的(不然这篇文章也不用写了)
不过iOS和Android的表现并不一致,具体差异看下表:
iOS | Android |
---|---|
滚动后切换另一个iframe时,滚动位置和切换前一致 | 滚动后切换另一个iframe时,滚动位置会重置到顶部(即scrollTop=0) |
手动修改scrollTop值
理所当然的,聪明的我立马想到一个“完美”的方案:切换时先记住当前iframe的scrollTop,然后再取出另一个iframe的scrollTop(如果有的话)并赋值。
// 获取iframe的scrollTop
const getIframeScrollTop = iframe => iframe.contentWindow.pageYOffset || iframe.contentDocument.documentElement.scrollTop || iframe.contentDocument.body.scrollTop || 0;
// 设置iframe的scrollTop
const setIframeScrollTop = (iframe, val) => {
iframe.contentDocument.documentElement.scrollTop = val;
iframe.contentDocument.body.scrollTop = val;
};
其它代码由于过于简单就不贴了
可以扫这个二维码看真机表现:
结果在Android上表现符合预期,但是在iOS上无效,而且取出来的scrollTop恒等于0
iOS | Android |
---|---|
滚动后切换另一个iframe时,滚动位置和切换前一致,scrollTop=0 | 符合预期 |
怀疑iOS是否共用同一个滚动条
人生遇到瓶颈后就开始怀疑世间万物于是我开始怀疑iOS的iframe是不是共用了同一个滚动条,不然滚动位置怎么会保持一致?
抱着死马当活马医的心态,我做了这样一个测试,给两个iframe以及page绑定了scroll事件,在事件callback里分别打印’iframe1’、‘iframe2’和’page’。
可以扫这个二维码看真机表现:
实验结果使我更确信iOS是共用了同一个滚动条
iOS | Android |
---|---|
无论在哪里滚动,都打印’page’ | 打印结果符合预期 |
在iOS中改用page滚动条的scrollTop
判一下ua对iOS做特殊处理,如果是iOS,就对page做scrollTop取值/赋值操作
const isIOS = /(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent);
// 获取iframe的scrollTop(升级版)
const getIframeScrollTop = iframe => {
if (isIOS) { // iOS下所有iframe共用同一个滚动条
return window.pageYOffset ||
document.documentElement.scrollTop ||
document.body.scrollTop ||
0;
}
// 其余的有各自的滚动条
return iframe.contentWindow.pageYOffset ||
iframe.contentDocument.documentElement.scrollTop ||
iframe.contentDocument.body.scrollTop ||
0;
}
// 设置iframe的scrollTop(升级版)
const setIframeScrollTop = (iframe, val) => {
if (isIOS) { // iOS下所有iframe共用同一个滚动条
document.documentElement.scrollTop = val;
document.body.scrollTop = val;
} else {
// 其余的有各自的滚动条
iframe.contentDocument.documentElement.scrollTop = val;
iframe.contentDocument.body.scrollTop = val;
}
}
可以扫这个二维码看真机表现:
iOS | Android |
---|---|
符合预期 | 符合预期 |
幸不辱命,终于可以上线了,哈哈
需求上线后重新整理了下思路
由于那天被这个问题折腾了很久,大脑非常疲惫,产品又在催上线,看到问题解决后就立马上线然后下班回家了。隔天回来后,重新整理在网上搜索到关于iOS iframe的文章,又有了新的发现:
iOS iframe之所以会和page共用同一个滚动条,是因为overflow: scroll和height(css属性)对iOS iframe不起作用(由于当时需求内容是iframe占全屏,即height: 100vh,所以一直没发现这个问题),所以在iOS里,iframe没有命中overflow逻辑,内容完整地呈现出来,因而和page使用同一个滚动条。
那么,如何使overflow: scroll生效?
其实很简单
iframe不能用overflow: scroll,那我就让它“爸爸”滚:用div将iframe包裹起来,给这个div加上overflow: scroll和height(css属性)。
不过这时候你会发现当你滚动iframe时,page会动了起来,而iframe毫无反应,就好像“点透”一样。
不怕,贴心的谷歌爸爸给了解决方案:再给这个div加上-webkit-overflow-scrolling: touch,如此一来,iOS iframe的overflow之力终于被解放出来了。
<div id="js_iframe_wrap">
<iframe id="js_iframe" scrolling="yes" src="article1.html"></iframe>
</div>
#js_iframe_wrap {
-webkit-overflow-scrolling: touch;
overflow-y: scroll;
height: 30vh;
}
可以扫这个二维码看真机表现:
在这个demo里我将iframe fixed在页面中部,然后将page的高度设置成300vh,最后在页面上放了一个按钮,点击按钮会打印page、iframe以及iframe wrap的scrollTop值。
const qs = (selector, el) => (el || document).querySelector(selector);
const getDocumentScrollTop = () => window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
const getIframeScrollTop = iframe => iframe.contentWindow.pageYOffset || iframe.contentDocument.documentElement.scrollTop || iframe.contentDocument.body.scrollTop || 0;
$('#js_log').click(() => {
console.log(`document scrollTop: ${getDocumentScrollTop()}`);
console.log(`iframe scrollTop: ${getIframeScrollTop(qs('#js_iframe'))}`);
console.log(`iframe wrap scrollTop: ${qs('#js_iframe_wrap').scrollTop}`);
});
可以看到,iframe已经不再和page共用同一个滚动条了,而且iframe的滚动值等于iframe wrap的scrollTop值。
写在后面
最后我也没把产品的需求改成-webkit-overflow-scrolling: touch方案,不是方案有坑,而是我懒得改了