评论

iframe滚动踩坑总结

讲述iframe在iOS、Android上滚动的各种表现差异,带你一步步找出原因并消除差异。

背景

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

最后一次编辑于  2019-04-02  
点赞 7
收藏
评论

3 个评论

  • 阿巴阿巴
    阿巴阿巴
    2019-04-11

    微信多少,加你下。

    2019-04-11
    赞同
    回复 3
    • Jamin
      Jamin
      2019-04-11

      啊哈?

      2019-04-11
      回复
    • 阿巴阿巴
      阿巴阿巴
      2019-04-12回复Jamin

      我是开发者,交流技术,没有其他意图,看你文章写的思路清晰排版整齐。。。。

      2019-04-12
      回复
    • Jamin
      Jamin
      2019-04-12回复阿巴阿巴

      我加你了

      2019-04-12
      回复
  • 阿巴阿巴
    阿巴阿巴
    2019-04-11

    配图🐮B

    2019-04-11
    赞同
    回复
  • 永恒君
    永恒君
    2019-04-02

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

    2019-04-02
    赞同
    回复 6
    • Jamin
      Jamin
      2019-04-02

      那用什么方式做隐藏呢?visibility: hidden 和 opacity: 0 都会保留盒模型在文档流中,导致页面留白

      2019-04-02
      回复
    • 哈罗哈皮
      哈罗哈皮
      2019-04-02回复Jamin

      如果我使用了visibility: hidden 或者 opacity: 0 ,然后用hight:0这样设计可行否?

      2019-04-02
      1
      回复
    • 永恒君
      永恒君
      2019-04-02

      position: absolute; left: -9999px 之类的

      2019-04-02
      回复
    • Jamin
      Jamin
      2019-04-02

      没有尝试过,不过目测不行,问题不是出在隐藏方式那

      2019-04-02
      回复
    • 永恒君
      永恒君
      2019-04-02回复Jamin

      唔,display: none 感觉有可能会丢失 scrollTop,

      但 absolute 的两个 iframe 除了 position 就没变过,加油大宝贝。

      2019-04-02
      回复
    查看更多(1)
登录 后发表内容