前言
製作案件上遇到的需求,若使用者有安裝 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_link
與 download_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 的部份,一樣透過 setTimeout
與 webkitHidden
處理:
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;
大致上好了,為什麼說「大致上」呢?因為我們還有幾個特殊情況需要處理:
- 在 iOS Facebook inapp browser 中,無法正常透過 URL Scheme 開啟 App。(毫無反應)
而除非 Facebook inapp browser 有攔截處理這些 URL Scheme,不然似乎無法解決這問題。因此最後採用的方式是當判斷使用者使用 Facebook inapp browser 開啟網頁時,就用文字提醒使用者,「請透過 Safari 開啟這個網頁來繼續正常的後續流程」。
-
若使用者未安裝 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 中似乎也沒有實作 JavaScript
的 alert
訊息框,所以用 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 更新,更新一些連結作為參考:
- URI SchemeでTwitter投稿をしてみる
- 原 stackoverflow 的 new accepted answer
- Detect InApp – 前幾天發現前端社群有人發起了這個專案,應該可以幫助解決相關的痛點,因此也貢獻了一些 PR,期望整理過後可以變成易用的判斷方式囉。
已經兩年過去了,隨著瀏覽器更新,有些方式應該已經不適用了(未詳細測試),以下內容就當實作參考吧 😅
最終合併的程式碼如下:
(請直接 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;
}
});