iframe滚动踩坑总结
发布于 4 年前 作者 leiyan 680 次浏览 来自 分享

背景

由于产品想要在一个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方案,不是方案有坑,而是我懒得改了

3 回复

微信多少,加你下。

假如我不是用 display: none 来做隐藏是不是就不会遇到这样的问题了呢

回到顶部