筆記:行動裝置上使用iframe遇到的各種問題

2020-09-07 • 7 min read

因為公司與其他brand合作的方式是將我們的網頁透過iframe嵌入其中,但這導致在行動裝置上產生了一系列的問題,所以在此記錄下這些研究的過程,希望能給其他遇到類似問題的人一點幫助。

情境

我們網頁Designer希望content的scroll bar不要超出它的範圍,也就是希望不要跑到header或者bottom navigation上,因為這個需求導致在行動裝置上就只有一個頁面的高度,而瀏覽器都有top address bar與bottom action bar,預設行為是當user向下滑動頁面時,會自動縮起top address bar與bottom action bar,但由於前面提到的需求,這導致我們無法縮放top address bar與bottom action bar(範例可以用手機看https://app.ft.com),不能縮放的問題被上面的長官接受了,接著就是將iframe至於brand的header底下即可。

問題

在我們放到brand的網站底下後,發現了幾個問題,這裡一一列出遇到的問題。

  1. iOS 12 iframe無法responsive,導致整個網頁跑版。
  2. 如何撐滿iframe的高度保持於一個頁面。
  3. 當user只有開一個tab時,將網頁從portrait轉到landscape再轉回portrait底下會預留一塊bottom action bar的空位(此問題可在https://app.ft.com中reproduce,所以這不單只出現在iframe中)。

我們主要測試的browser有Safari、Chrome、UC Browser,所以以下所提到的解法只有在上述的browser測試過。

mobile-iframe-1

底下的程式碼為最終解決這些問題的結果,下面將一一解釋使用的目的:

<!DOCTYPE html>
<html>
  <head>
    <meta
      name="viewport"
      content="width=device-width,initial-scale=1,shrink-to-fit=no"
    />
    <title>Ifram Testing</title>
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <nav class="navbar">Brand Header</nav>
    <main class="content">
      <iframe
        id="myIframe"
        src="https://app.ft.com"
        allowtransparency="true"
        frameborder="0"
      ></iframe>
    </main>
    <script type="text/javascript" src="iframeModule.js"></script>
    <script type="text/javascript">
      const iframeModule = new IframeModule(50, "myIframe");
      iframeModule.init();
    </script>
  </body>
</html>

style.css:

* {
  box-sizing: border-box;
}

body {
  padding: 0;
  margin: 0;
  height: 100%;
}

.navbar {
  position: fixed;
  top: 0;
  transition: 0.2s ease-in-out;
  width: 100%;
  z-index: 999;
  background-color: #060606;
  height: 50px; /* brand header height */
}

.content {
  position: fixed;
  margin-top: 50px; /* brand header height */
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
}

.content iframe {
  height: 100%;
  width: 100%;
}

.content iframe.ios {
  min-width: 100%;
  width: 1px;
}

iframeModule.js:

function IframeModule(headerHeight, iframeID) {
  const iframe = document.getElementById(iframeID);
  const isiOS = navigator.userAgent.match(/(iPod|iPhone|iPad)/);

  function getIFrameHeight() {
    return window.innerHeight - headerHeight + "px";
  }

  function setIFrameHeight() {
    iframe.style.height = getIFrameHeight();
  }

  function registerOrientationChange(callback) {
    window.addEventListener("orientationchange", function () {
      const afterOrientationChange = function () {
        callback();
        window.removeEventListener("resize", afterOrientationChange);
      };
      window.addEventListener("resize", afterOrientationChange);
    });
  }

  function init() {
    window.addEventListener("DOMContentLoaded", setIFrameHeight);
    if (isiOS) {
      iframe.setAttribute("scrolling", "no");
      iframe.classList.add("ios");
      registerOrientationChange(function () {
        if (window.matchMedia("(orientation: portrait)").matches) {
          document.getElementsByTagName("html")[0].style.height = "100vh";
          setTimeout(function () {
            document.getElementsByTagName("html")[0].style.height = "100%";
          }, 300);
        }
        setIFrameHeight();
      });
    } else {
      registerOrientationChange(function () {
        setIFrameHeight();
      });
    }
  }

  return {
    init,
  };
}

問題1: iOS 12 iframe responsive

此問題的解法比較簡單,若我們能夠控制iframe裡面內容物的CSS,只要在內容html加入下面這段CSS既可:

    width: 1px;
    min-width: 100%;

但若我們無法控制內容物的CSS,只能透過改變iframe的style來解決:

iframe.ios {
    width: 1px;
    min-width: 100%;
}

但只加上面的這樣還不夠,還必須加上scrolling="no"才行

<iframe src="http://example.com" allowtransparency="true" frameborder="0" scrolling="no"></iframe>

最後我們只要偵測是iOS裝置使用者再加上上述這些屬性即可:

...
const iframe = document.getElementById(iframeID);
const isiOS = navigator.userAgent.match(/(iPod|iPhone|iPad)/);
if (isiOS) {
      iframe.setAttribute("scrolling", "no");
      iframe.classList.add("ios");
}
...

問題2: 撐滿iframe高度於一個頁面

起初我嘗試透過動態計算window.innerHeight - {brand header height}設置iframe的高度,當我嘗試在實體手機上測試來回的從portrait轉到landscape再轉回portrait時, 發現有時候高度計算會錯誤,導致我的iframe只有landscape時的高度,所以我就上網找了一下解決方案,剛好stackoverflow有一篇在討論這個問題:Mobile viewport height after orientation change,其中我篩選掉了使用setTimeout的解決方式,因為那時間你很難明確地掌握,如果可以避免使用我都會盡量避免使用, 所以我嘗試了裡面幾個vote比較高的解法,但依舊會旋轉過後計算錯誤的時候,儘管它不是很常發生。

...
window.addEventListener("orientationchange", function () {
  const afterOrientationChange = function () {
    iframe.style.height = window.innerHeight - headerHeight + "px";
    window.removeEventListener("resize", afterOrientationChange);
  };
  window.addEventListener("resize", afterOrientationChange);
});
...

接著我朝CSS的方向尋求答案,看看上述main元素的部分:

...
<main class="content">
  <iframe
    id="myIframe"
    src="https://app.ft.com"
    allowtransparency="true"
    frameborder="0"
  ></iframe>
</main>
...

我嘗試將.content selector設置如下:

.content {
  position: fixed;
  margin-top: 50px; /* brand header height */
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
}

當我只靠配置CSS撐滿畫面高度時,發現了兩個問題:

  1. 當有多個tab、landscape的狀態下,預設會出現top address bar,由於多出這段高度,使得iframe中的bottom navigation會被擠出畫面外。
  2. 在有些Android設備上的UC Browser底下會多一塊空間,在iframe與bottom action bar中間。

最後我將上述兩種方式合併,就解決了這些問題,或許還有其他方式可以解決這類問題,不過目前我只試出這種方法已達到我們的需求。

問題3: 當只有一個tab時,旋轉導致不必要的空白

如下圖所示:

mobile-iframe-2

在最底下會保留一塊空白的部分,那段空白屬於bottom action bar所殘留下來的。起初我想透過觸發scroll的方式來達到彈出bottom action bar已解決那段空白的問題,但最終還是宣告失敗。 後來想透過設定高度的方式將我的iframe達到撐滿整個畫面的效果,但我沒有找到任何一種方式可以獲取瀏覽器top address bar與bottom action bar高度的方法。

最終公司的前輩找到了一個方式解決這個問題:

if (window.matchMedia("(orientation: portrait)").matches) {
    document.getElementsByTagName("html")[0].style.height = "100vh";
    setTimeout(function () {
        document.getElementsByTagName("html")[0].style.height = "100%";
    }, 300);
}

如前面所提,我個人會盡量避免使用setTimout,但由於沒有任何一種event可以讓我監聽bottom action bar是否有出現,所以造就了這種奇怪的解法,但它至少可以解決這問題。

Reference

Copyright © 2023. Papan01