[Web][JavaScript] 有安裝 App 便開啟 App (透過 App URL scheme),若無則導去下載 (Google Play/App Store) 的 workaround

Standard

前言

製作案件上遇到的需求,若使用者有安裝 App 便開啟 App (透過 App URL scheme),若無則導去下載 (Google Play/App Store)。

在 App 內這件事應該是可掌控的(在自己做的 App 中攔截特定網址做處理),但在網頁上要做到這件事就有點麻煩了。一般常見的作法是擺個 Smart Banner,不過只有 iOS Safari 原生就支援,其他的得靠 jQuery Smart Banner(前陣子我也貢獻了修正 Windows Phone 相關錯誤的 pull request :P) 這類的第三方 plugin。

但 Smart Banner 和案件的需求還是有些差異,得靠其他方法解決,在實作的過程中遇到一些小問題與相應的 workaround,特此記錄。

實測環境

跟公司 QA 部門借了幾台機器,以及測試的瀏覽器分別是:

  • iPhone 5S(iOS 8.1.0) – Safari
  • LG G3(Android 5.0) – 內建瀏覽器、Chrome Android 43.0.2357.93、Facebook inapp browser(核心為 Chrome 30.0.0.0)
  • ASUS ZenFone 5 – 內建瀏覽器、Chrome Android 40.0.2214.89
  • SONY Z3(Android 5.0.2) – Chrome Android 43.0.2357.93

服用本文前請注意:也許瀏覽器版本更新後,本文的方法就失效了,所以請作為參考就好哦。

實作與問題

之前在案件中,使用下面這段程式碼作為 workaround:

setTimeout(function() {
  window.location = scheme_link;
}, 25);
window.location = download_link;

註:其中 scheme_link 是 app 的 scheme,可以是單純開啟 App,或是直接進入 App 的某一個畫面。而 download_link 是下載 App 的 URL 連結,像是 Google Play 或是 App Store 的連結。

之前實測時,在裝置上大部分情況都能如預期的運作,但仍有特殊情況(如特定版本的 Chrome、Android Stock Browser)有點小問題。

而隨著瀏覽器版本更新,這次案件再實測似乎就會出現更詭異的情況了,例如同時打開 App 與 Google Play/App Store 的連結。

於是再度至 stackoverflow.com 尋找其他解決方案,找到 一個 2014 年 8 月左右被更新的解答

我針對該解答再做些微調後的 workground 整理說明如下:

Workaround

我們先定義 scheme_linkdownload_link 兩個變數,分別是 App 的 URL Scheme 與 Google Play/App Store 的下載連結。

var scheme_link = $(this).data("app-scheme");
var download_link = $(this).data("download-url");

接下來設定一個已跳轉的旗標變數,用以記錄是否跳轉過了。

var redirected = false;

接下來主要分成四大分支,Windows Phone、Android、iOS 與其他。而 Android 中又分成 Chrome 與非 Chrome 的瀏覽器(可能是 Android Stock Browser 內建瀏覽器、inapp 瀏覽器之類的)。

Windows Phone 得先判斷,因為它的 User Agent 相當奇特:

// WP 8.1 的時候還正常
Mozilla/5.0 (Windows Phone 8.1; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 620) like Gecko

// WP 8.1.1 就各種身分了...XD
Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 1520) like iPhone OS 7_0_3 Mac OS X AppleWebKit/537 (KHTML, like Gecko) Mobile Safari/537

可怕吧!所以 Windows Phone 需要優先判斷。

if (navigator.userAgent.match(/Windows Phone/i)) {
  // Windows Phone
} else if (navigator.userAgent.match(/Android/i)) {
  if (navigator.userAgent.match(/Chrome/i)) {
    // Android + Chrome
  } else {
    // Android + 非 Chrome
  }
} else if (navigator.userAgent.match(/(iPhone|iPad|iPod)/i)) {
  // iOS
} else {
  // 其他
}

寫好判斷架構之後,我們分別填入內容。首先處理 Windows Phone 的部份:

  loadDateTime = new Date();
  setTimeout(function() {
    var timeOutDateTime;
    timeOutDateTime = new Date();
    if (timeOutDateTime - loadDateTime < 5000) {
      window.location = download_link;
    }
  }, 1000);
  window.location = scheme_link;

接著是 Android 的 Chrome,我們透過 setTimeout 加上 window.webkitHidden(用以判斷是否跳到 App 畫面去了,網頁不在使用焦點)與上面宣告的已跳轉變數 redirected 來實作。

window.location = scheme_link;
setTimeout(function() {
  if (!document.webkitHidden && !redirected) {
    redirected = true;
    window.location = download_link;
  }
}, 1000);

再來處理非 Chrome 的瀏覽器,原先在 Stackoverflow 找到的解法是像面這樣,透過動態產生的 iframe 處理。

可是實測發現,由於 app url scheme 網址算是不同網域了,因此無法取得是否真的沒有錯誤地載入完畢,無論如何都會觸發 onload 事件,這樣就無法達成希望的效果了。

iframe = document.createElement("iframe");
iframe.style.border = "none";
iframe.style.width = "1px";
iframe.style.height = "1px";
t = setTimeout(function() {
  window.location = download_link;
}, 1000);
iframe.onload = function() {
  clearTimeout(t);
};
iframe.src = scheme_link;
document.body.appendChild(iframe);

而在「已安裝該 url scheme 的 App」的情況下,在「非 Chrome 瀏覽器」中呼叫似乎是可以正常開啟 App 的,但「未安裝」的時候就會無情地跳個錯誤畫面,而原先瀏覽的畫面也被置換了,更不用談 setTimeout 再去做什麼。

目前這邊沒有比較好的解法,於是無論情況都跳個訊息告知使用者,若看到錯誤畫面就先安裝 App 再來開吧。(體驗很差啊…)

window.location = scheme_link;
alert("若您看到錯誤畫面,請先安裝 某某 App 喔!");

好,再來是 iOS 的部份,一樣透過 setTimeoutwebkitHidden 處理:

setTimeout(function() {
  if (!document.webkitHidden) {
    window.location = download_link;
  }
}, 25);
window.location = scheme_link;

最後是其他瀏覽器的處理方式:

loadDateTime = new Date();
setTimeout(function() {
  var timeOutDateTime;
  timeOutDateTime = new Date();
  if (timeOutDateTime - loadDateTime < 5000) {
    window.location = download_link;
  }
}, 1000);
window.location = scheme_link;

大致上好了,為什麼說「大致上」呢?因為我們還有幾個特殊情況需要處理:

  1. 在 iOS Facebook inapp browser 中,無法正常透過 URL Scheme 開啟 App。(毫無反應)

    而除非 Facebook inapp browser 有攔截處理這些 URL Scheme,不然似乎無法解決這問題。因此最後採用的方式是當判斷使用者使用 Facebook inapp browser 開啟網頁時,就用文字提醒使用者,「請透過 Safari 開啟這個網頁來繼續正常的後續流程」。

  2. 若使用者未安裝 App,又試圖用 Chrome Android 40.0.2214.89 點開 App URL Scheme 的話,會被導向至 This web page is not available 的錯誤畫面(相關的 issue

    這會導致我無法透過 setTimeout 導向至 Google Play/App Store 連結,因為已經跳到另一個頁面了。

    而這個情況似乎沒有什麼完美的解決方案,如同「Chrome 之外的 Android 瀏覽器」的情況,只能判斷這個版本以下的時候,一律跳 alert 訊息,告知使用者若看到錯誤畫面,那麼就請自行去下載 App 吧。(真是不負責任…只能希望大家的 Chrome Android 都乖乖升級了 XD)

特殊情況條列好了,那我們先處理 iOS Facebook inapp browser:

if (navigator.userAgent.match(/FBIOS/i)) {
  document.writeln("請按上或下方的箭頭,選擇「在 Safari 開啟」以繼續進行活動。");
  return;
}

經過實測,在 iOS Facebook inapp browser 的 User Agent 有塞入客製字串 FBIOS,這可讓我們判斷這個特殊情況,此例中若條件成立,就在頁面中印出提醒文字。

另外有個小提醒 – 在 Facebook inapp browser 中似乎也沒有實作 JavaScriptalert 訊息框,所以用 alert 是沒有反應的。

最後是 Chrome 40.0.2214.89 這個版本之下的情況,加上版本偵測,40 以下的版本就另外處理。修改一下原本 Chrome 的判斷點程式碼,變成:

if (+navigator.userAgent.match(/(chrome(?=\/))\/?\s*(\d+)/i)[2] >= 41) {
  window.location = scheme_link;
  setTimeout(function() {
    if (!document.webkitHidden && !redirected) {
      redirected = true;
      window.location = download_link;
    }
  }, 1000);
} else {
  window.location = scheme_link;
  alert("若您看到錯誤畫面,請先安裝 某某 App 喔!");
}

好了,大功告成。

結論

2017/05/22 更新,更新一些連結作為參考:

已經兩年過去了,隨著瀏覽器更新,有些方式應該已經不適用了(未詳細測試),以下內容就當實作參考吧 😅


最終合併的程式碼如下:
(請直接 END 的朋友們注意,這段 workaround 可能因為瀏覽器版本更新而無法正常作用哦 : P)

$(".js-open-app").on("click", function() {
  var download_link, iframe, loadDateTime, redirected, scheme_link, t;
  scheme_link = $(this).data("app-scheme");
  download_link = $(this).data("download-url");
  redirected = false;

  if (navigator.userAgent.match(/FBIOS/i)) {
    document.writeln("請按上或下方的箭頭,選擇「在 Safari 開啟」以繼續進行活動。");
    return;
  }

  if (navigator.userAgent.match(/Windows Phone/i)) {
    loadDateTime = new Date();
    setTimeout(function() {
      var timeOutDateTime;
      timeOutDateTime = new Date();
      if (timeOutDateTime - loadDateTime < 5000) {
        window.location = download_link;
      }
    }, 1000);
    window.location = scheme_link;
  } else if (navigator.userAgent.match(/Android/i)) {
    if (navigator.userAgent.match(/Chrome/i)) {
      if (+navigator.userAgent.match(/(chrome(?=\/))\/?\s*(\d+)/i)[2] >= 41) {
        window.location = scheme_link;
        setTimeout(function() {
          if (!document.webkitHidden && !redirected) {
            redirected = true;
            window.location = download_link;
          }
        }, 1000);
      } else {
        window.location = scheme_link;
        alert("若您看到錯誤畫面,請先安裝 某某 App 喔!");
      }
    } else {
      iframe = document.createElement("iframe");
      iframe.style.border = "none";
      iframe.style.width = "1px";
      iframe.style.height = "1px";
      t = setTimeout(function() {
        window.location = download_link;
      }, 1000);
      iframe.onload = function() {
        clearTimeout(t);
      };
      iframe.src = scheme_link;
      document.body.appendChild(iframe);
    }
  } else if (navigator.userAgent.match(/(iPhone|iPad|iPod)/i)) {
    setTimeout(function() {
      if (!document.webkitHidden) {
        window.location = download_link;
      }
    }, 25);
    window.location = scheme_link;
  } else {
    loadDateTime = new Date();
    setTimeout(function() {
      var timeOutDateTime;
      timeOutDateTime = new Date();
      if (timeOutDateTime - loadDateTime < 5000) {
        window.location = download_link;
      }
    }, 1000);
    window.location = scheme_link;
  }
});

發表迴響

你的電子郵件位址並不會被公開。 必要欄位標記為 *