前段時間遇到一個小需求:要求在分享出來的h5頁面中,有一個立即開啟的按鈕,如果本地安裝了我們的app,那麼點選就直接喚起本地app,如果沒有安裝,則跳轉到下載。
從來沒有做過這個需求,因此這注定是一個苦逼的調研過程。
我們最開始就面臨2個問題:一是如何喚起本地app,二是如何判斷瀏覽器是否安裝了對應app。
如何喚起本地app
首先,想要實現這個需求,肯定是必須要客戶端同學的配合才行,因此我們不用知道所有的實現細節,我們從前端角度思考看這個問題,需要知道的一點是,ios與Android都支援一種叫做schema協議的連結。比如網易新聞客戶端的協議為
1 |
newsapp://xxxxx |
當然,這個協議不需要我們前端去實現,我們只需要將協議放在a標籤的href屬性裡,或者使用location.href與iframe來實現啟用這個連結。而location.href與iframe是解決這個需求的關鍵。
在ios中,還支援通過smart app banner
來喚起app,即通過一個meta標籤,在標籤裡帶上app的資訊,和開啟後的行為,程式碼形如
1 |
<meta name="apple-itunes-app" content="app-id=1023600494, app-argument=tigerbrokersusstock://com.tigerbrokers.usstock/post?postId=7125" /> |
我們還需要知道的一點是,微信裡遮蔽了schema協議。除非你是微信的合作伙伴之類的,他們專門給你配置進白名單,否則我們就沒辦法通過這個協議在微信中直接喚起app。
因此我們會判斷頁面場景是否在微信中,如果在微信中,則會提示使用者在瀏覽器中開啟。
如何判斷本地是否安裝了app
首先我們可以確認的是,在瀏覽器中無法明確的判斷本地是否安裝了app。因此我們必須採取一些取巧的思路來解決這個問題。
我們能夠很容易想到,採用設定一個延遲定時器setTimeout的方式,第一時間嘗試喚起app,如果200ms沒有喚起成功,則預設本地沒有安裝app,200ms以後,將會觸發下載行為。
結合這個思路,我們來全域性考慮一下這個需求應該採用什麼樣的方案來實現它。
使用location.href的同學可能會面臨一個擔憂,在有的瀏覽器中,當我們嘗試啟用schema link的時候,若本地沒有安裝app,則會跳轉到一個瀏覽器預設的錯誤頁面去了。因此大多數人採用的解決方案都是使用iframe
測試了很多瀏覽器,沒有發現過這種情況
後來觀察了網易新聞,今日頭條,YY等的實現方案,發現大家都採用的是iframe來實現。好吧,面對這種情況,只能屈服。
整理一下目前的思路,得到下面的解決方案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var url = { open: 'app://xxxxx', down: 'xxxxxxxx' }; var iframe = document.createElement('iframe'); var body = document.body; iframe.style.cssText='display:none;width=0;height=0'; var timer = null; // 立即開啟的按鈕 var openapp = document.getElementById('openapp'); openapp.addEventListener('click', function() { if(/MicroMessenger/gi.test(navigator.userAgent) { // 引導使用者在瀏覽器中開啟 }) else{ body.appendChild(iframe); iframe.src = url.open; timer = setTimeout(function() { wondow.location.href = url.down; }, 500); } }, false) |
想法很美好,現實很殘酷。一測試,就發現簡單的這樣實現有許多的問題。
第一個問題在於,當頁面成功喚起app之後,我們再切換回來瀏覽器,發現跳轉到了下載頁面。
為了解決這個問題,發現各個公司都進行了不同方式的嘗試。
也是歷經的很多折磨,發現了幾個比較有用的事件。
pageshow: 頁面顯示時觸發,在load事件之後觸發。需要將該事件繫結到window上才會觸發
pagehide: 頁面隱藏時觸發
visibilitychange: 頁面隱藏沒有在當前顯示時觸發,比如切換tab,也會觸發該事件
document.hidden 當頁面隱藏時,該值為true,顯示時為false
由於各個瀏覽器的支援情況不同,我們需要將這些事件都給繫結上,即使這樣,也不一定能夠保證所有的瀏覽器都能夠解決掉這個小問題,實在沒辦法的事情就不管了。
因此需要擴充一下上面的方案,當本地app被喚起,則頁面會隱藏掉,就會觸發pagehide與visibilitychange事件
1 2 3 4 5 6 7 8 9 10 |
$(document).on('visibilitychange webkitvisibilitychange', function() { var tag = document.hidden || document.webkitHidden; if (tag) { clearTimeout(timer); } }) $(window).on('pagehide', function() { clearTimeout(timer); }) |
而另外一個問題就是IOS9+下面的問題了。ios9的Safari,根本不支援通過iframe跳轉到其他頁面去。也就是說,在safari下,我的整體方案被全盤否決!
於是我就只能嘗試使用location.href的方式,這個方式能夠喚起app,但是有一個坑爹的問題,使用schema協議喚起app會有彈窗而不會直接跳轉去app!甚至當本地沒有app時,會被判斷為連結無效,然後還有一個彈窗。
這個彈窗會造成什麼問題呢?如果使用者不點確認按鈕,根據上面的邏輯,這個時候就會發現頁面會自動跳轉到下載去了。而且無效的彈窗提示在使用者體驗上是不允許出現的。
好吧,繼續扒別人的程式碼,看看別人是如何實現的。然後我又去觀摩了其他公司的實現結果,發現網易新聞,今日頭條都可以在ios直接從微信中喚起app。真是神奇了,可是今日頭條在Android版微信上也沒辦法直接喚起的,他們在Android上都是直接到騰訊應用寶的下載裡去。所以按道理來說這不是新增了白名單。
為了找到這個問題的解決方案,我在網易新聞的頁面中扒出了他們的程式碼,並整理如下,新增了部分註釋
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
window.NRUM = window.NRUM || {}; window.NRUM.config = { key:'27e86c0843344caca7ba9ea652d7948d', clientStart: +new Date() }; (function() { var n = document.getElementsByTagName('script')[0], s = document.createElement('script'); s.type = 'text/javascript'; s.async = true; s.src = '//nos.netease.com/apmsdk/napm-web-min-1.1.3.js'; n.parentNode.insertBefore(s, n); })(); ; (function(window,doc){ // http://apm.netease.com/manual?api=web NRUM.mark && NRUM.mark('pageload', true) var list = [] var config = null // jsonp function jsonp(a, b, c) { var d; d = document.createElement('script'); d.src = a; c && (d.charset = c); d.onload = function() { this.onload = this.onerror = null; this.parentNode.removeChild(this); b && b(!0); }; d.onerror = function() { this.onload = this.onerror = null; this.parentNode.removeChild(this); b && b(!1); }; document.head.appendChild(d); }; function localParam(search,hash){ search = search || window.location.search; hash = hash || window.location.hash; var fn = function(str,reg){ if(str){ var data = {}; str.replace(reg,function( $0, $1, $2, $3 ){ data[ $1 ] = $3; }); return data; } } return {search: fn(search,new RegExp( "([^?=&]+)(=([^&]*))?", "g" ))||{},hash: fn(hash,new RegExp( "([^#=&]+)(=([^&]*))?", "g" ))||{}}; } jsonp('http://active.163.com/service/form/v1/5847/view/1047.jsonp') window.search = localParam().search window._callback = function(data) { window._callback = null list = data.list if(search.s && !!search.s.match(/^wap/i)) { config = list.filter(function(item){ return item.type === 'wap' })[0] return } config = list.filter(function(item){ return item.type === search.s })[0] } var isAndroid = !!navigator.userAgent.match(/android/ig), isIos = !!navigator.userAgent.match(/iphone|ipod/ig), isIpad = !!navigator.userAgent.match(/ipad/ig), isIos9 = !!navigator.userAgent.match(/OS 9/ig), isYx = !!navigator.userAgent.match(/MailMaster_Android/i), isNewsapp = !!navigator.userAgent.match(/newsapp/i), isWeixin = (/MicroMessenger/ig).test(navigator.userAgent), isYixin = (/yixin/ig).test(navigator.userAgent), isQQ = (/qq/ig).test(navigator.userAgent), params = localParam().search, url = 'newsapp://', iframe = document.getElementById('iframe'); var isIDevicePhone = (/iphone|ipod/gi).test(navigator.platform); var isIDeviceIpad = !isIDevicePhone && (/ipad/gi).test(navigator.platform); var isIDevice = isIDevicePhone || isIDeviceIpad; var isandroid2_x = !isIDevice && (/android\s?2\./gi).test(navigator.userAgent); var isIEMobile = !isIDevice && !isAndroid && (/MSIE/gi).test(navigator.userAgent); var android_url = (!isandroid2_x) ? "http://3g.163.com/links/4304" : "http://3g.163.com/links/6264"; var ios_url = "http://3g.163.com/links/3615"; var wphone_url = "http://3g.163.com/links/3614"; var channel = params.s || 'newsapp' // 判斷在不同環境下app的url if(params.docid){ if(params['boardid'] && params['title']){ url = url + 'comment/' + params.boardid + '/' + params.docid + '/' + params.title }else{ url = url + 'doc/' + params.docid } }else if(params.sid){ url = url + 'topic/' + params.sid }else if(params.pid){ var pid = params.pid.split('_') url = url + 'photo/' + pid[0] + '/' + pid[1] }else if(params.vid){ url = url + 'video/' + params.vid }else if(params.liveRoomid){ url = url + 'live/' + params.liveRoomid }else if(params.url){ url = url + 'web/' + decodeURIComponent(params.url) }else if(params.expertid){ url = url + 'expert/' + params.expertid }else if(params.subjectid){ url = url + 'subject/' + params.subjectid }else if(params.readerid){ url = url + 'reader/' + params.readerid }else{ url += 'startup' } if(url.indexOf('?') >= 0){ url += '&s=' + (params.s || 'sps') }else{ url += '?s=' + (params.s || 'sps') } // ios && 易信 用iframe 開啟 if((isIos||isIpad) && navigator.userAgent.match(/yixin/i)) { document.getElementById('iframe').src = url; } var height = document.documentElement.clientHeight; // 通常情況下先嚐試使用iframe開啟 document.getElementById('iframe').src = url; // 移動端瀏覽器中,將下載頁面顯示出來 if(!isWeixin && !isQQ && !isYixin && !isYx){ document.querySelector('.main-body').style.display = 'block' if(isIos9){ document.querySelector('.main-body').classList.add('showtip') } setTimeout(function(){ document.body.scrollTop = 0 },200) }else{ document.getElementById('guide').style.display = 'block' } // Forward To Redirect Url // Add by zhanzhixiang 12/28/2015 if (params.redirect) { var redirectUrl = decodeURIComponent(params.redirect); if ( typeof(URL) === 'function' && new URL(redirectUrl).hostname.search("163.com") !== -1) { window.location.href = redirectUrl; } else if (redirectUrl.search("163.com") !== -1){ window.location.href = redirectUrl; }; } // Forward To Redirect Url End if ((isWeixin || isQQ) && isAndroid) { window.location.href = 'http://a.app.qq.com/o/simple.jsp?pkgname=com.netease.newsreader.activity&ckey=CK1331205846719&android_schema=' + url.match(/(.*)\?/)[1] } if(isIos||isIpad){ document.getElementById("guide").classList.add('iosguideopen') }else if (isAndroid){ document.getElementById("guide").classList.add('androidguideopen') }else{ // window.location.href = 'http://www.163.com/newsapp' } document.getElementById('link').addEventListener('click', function(){ // 統計 neteaseTracker && neteaseTracker(false,'http://sps.163.com/func/?func=downloadapp&modelid='+modelid+'&spst='+spst+'&spsf&spss=' + channel,'', 'sps' ) if (config) { android_url = config.android } if (config && config.iOS) { ios_url = config.iOS } if(isWeixin || isQQ){ return } var msg = isIDeviceIpad ? "檢測到您正在使用iPad, 是否直接前往AppStore下載?" : "檢測到您正在使用iPhone, 是否直接前往AppStore下載?"; if (isIDevice){ window.location = ios_url; return; }else if(isAndroid){ // uc瀏覽器用iframe喚醒 if(navigator.userAgent.match(/ucbrowser|yixin|MailMaster/i)){ document.getElementById('iframe').src = url; } else { window.location.href = url; } setTimeout(function(){ if(document.webkitHidden) { return } if (confirm("檢測到您正在使用Android 手機,是否直接下載程式安裝包?")) { neteaseTracker && neteaseTracker(false,'http://sps.163.com/func/?func=downloadapp_pass&modelid='+modelid+'&spst='+spst+'&spsf&spss=' + channel,'', 'sps' ) window.location.href = android_url; } else { neteaseTracker && neteaseTracker(false,'http://sps.163.com/func/?func=downloadapp_cancel&modelid='+modelid+'&spst='+spst+'&spsf&spss=' + channel,'', 'sps' ) } },200) return; }else if(isIEMobile){ window.location = wphone_url; return; }else{ window.open('http://www.163.com/special/00774IQ6/newsapp_download.html'); return; } }, false) setTimeout(function(){ if(isIDevice && params.notdownload != 1 && !isNewsapp && !isIos9){ document.getElementById('link').click() } }, 1000) })(window,document); |
雖然有一些外部的引用,和一些搞不懂是幹什麼用的方法和變數,但是基本邏輯還是能夠看明白。好像也沒有什麼特別的地方。研究了許久,看到了一個jsonp請求很奇特。這是來幹嘛用的?
於是費盡千辛萬苦,搜尋了很多文章,最終鎖定了一個關鍵的名詞 **Universal links**。
如果我早知道這個名詞,那麼問題就不會變的那麼束手無策。所以這個東西是什麼呢?
Apple為iOS 9釋出了一個所謂的通用連結的深層連結特性,即Universal links。雖然它並不完美,但是這一發布,讓數以千計的應用開發人員突然意識到自己的應用體驗被打破。
Universal links,一種能夠方便的通過傳統的HTTP/HTTPS 連結來啟動App,使用相同的網址開啟網站和App。
關於這個問題的提問與universal links的介紹
http://stackoverflow.com/questions/31891777/ios-9-safari-iframe-src-with-custom-url-scheme-not-working
ios9推行的一個新的協議!!!!!
關於本文的這個問題,國內的論壇有許許多多的文章來解決,但是提到universal links的文章少之又少,而我想吐槽的是,我們的ios開發也尼瑪不知道這個名詞,搞什麼鬼。他改變了使用者體驗的關鍵在於,微信沒有遮蔽這個協議。因此如果我們的app註冊了這個協議,那麼我們就能夠從微信中直接喚起app。
這個時候我就發現,上面貼的網易新聞程式碼中的jsonp請求的內容,就是這個協議必須的一個叫做apple-app-site-association
的JSON檔案
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "applinks": { "apps": [ ], "details": { "TEAM-IDENTIFIER.YOUR.BUNDLE.IDENTIFIER": { "paths": [ "*" ] } } } } |
大家可以直接訪問這個連結,檢視裡面的內容
http://active.163.com/service/form/v1/5847/view/1047.jsonp
至於universal links具體如何實現,讓ios的同學去搞定吧,這裡提供兩個參考文章
http://www.cocoachina.com/bbs/read.php?tid-1486368.html
https://blog.branch.io/how-to-setup-universal-links-to-deep-link-on-apple-ios-9
支援了這個協議之後,我們又可以通過iframe來喚起app了,因此基本邏輯就是這樣了。最終的調研結果是
沒有完美的解決方案
就算是網易新聞,這個按鈕在使用過程中也會有一些小bug,無法做到完美的狀態。
因為我們面臨許多沒辦法解決的問題,比如無法真正意義上的判斷本地是否安裝了app,pageshow,pagehide並不是所有的瀏覽器都支援等。很多其他部落格裡面,什麼計算時間差等方案,根!本!沒!有!用!我還花了很久的時間去研究這個方案。
老實說,從微信中跳轉到外部瀏覽器,並不是一個好的解決方案,這樣會導致很多使用者流失,因此大家都在ios上實現了universal links,而我更加傾向的方案是知乎的解決,他們從設計上避免了在一個按鈕上來判斷這個邏輯,而採用了兩個按鈕的方式。
網易新聞的邏輯是,點選開啟會調整到一個下載頁面,這個下載頁面一載入完成就嘗試開啟app,如果開啟了就直接跑到app裡面去了,如果沒有就在頁面上有一個立即下載的按鈕,按鈕行只有下載處理。
這個問題就總結到這裡,如果大家有更好的方案,歡迎與我溝通。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式