Hybrid開發中,web頁面往往會跟native進行互動,而JSBridge就是web頁面和native進行通訊的橋樑,通過JSBridge可以實現web呼叫native的方法,native可以通過
webview.loadUrl
之類的方法,將javascript:xxx
程式碼放在頁面執行,這有點類似在瀏覽器位址列直接輸入:javascript:xxx
本文較長,先把目錄列出來:
- JSBridge多種形式
- js Interface 直接注入到window物件
- 改寫瀏覽器原有物件:alert/console/prompt
- URL scheme
- 喚起APP技術
- intent
- localserver
- scheme:deeplink/applink/Universal link
- smart app banner
- JSBridge安全
- JSBridge的最佳實踐
- 協議規範
- 回撥函式
- 預留升級/統計能力
- 簡單JSBridge呼叫封裝
JSBridge多種形式
web和native進行通訊,方法有很多,接下來一一列舉一下JSBridge的多種形式,及其利弊。
JavaScriptInterface
JSInterface是安卓4.2-官方推薦的解決方案,原理是通過WebView提供的addJavascriptInterface
方法給瀏覽器window
注入一個名稱空間,然後給Web增加一些可以操作Java的反射。
// Android java程式碼
mWebView.addJavascriptInterface(new Class(), 'android');
public class Class(){
@JavascriptInterface
public void method(){
}
}
// js 程式碼
window.android.method();複製程式碼
JSInterface在4.2之前的版本都可以,但是存在嚴重的安全隱患,容易被利用提權,從而呼叫各種Java的類和許可權,甚至頁面可以掛馬。在我們實際產品(手機百度)開始階段,用過這個方法,不過現在已經不使用了。
改寫瀏覽器原有物件
這個方法主要是通過修改原來瀏覽器的window
某些方法,然後攔截固定規則的引數,然後分發給Java對應的方法去處理。這裡常用的是以下四個方法:
- alert,可以被webview的
onJsAlert
監聽 - confirm,可以被webview的
onJsConfirm
監聽 - console.log,可以被webview的
onConsoleMessage
監聽 - prompt,可以被webview的
onJsPrompt
監聽
prompt簡單舉例說明,Web頁面通過呼叫prompt()
方法,安卓客戶端通過監聽onJsPrompt
事件,攔截傳入的引數,如果引數符合一定協議規範,那麼就解析引數,扔給後續的Java去處理。這種協議規範,最好是跟iOS的協議規範一樣,這樣跨端調起協議是一致的,但具體實現不一樣而已。比如:hybrid://action?arg1=1
這樣的協議,而其他格式的prompt
引數,是不會監聽的,即除了hybrid://action?arg1=1
這樣的規範協議,prompt
還是原來的prompt
。
這四個方法也是各有利弊,比如:
alert
/console.log
是除錯最常用的,如果你要看看協議是不是寫錯了,但是傳入協議卻被攔截了。。confirm
和prompt
都帶返回值,prompt
是四個裡面唯一可以自定義返回值,可以做同步的互動,要比寫各種回撥更「順」,但是一旦序列呼叫了,就要罵爹了
prompt
是我們目前安卓用的比較多的JSBridge解決方案。
URL scheme
這個叫法不是特別貼切,scheme是URI的一種格式,上文提到的hybrid://action?arg1=1
就是一個scheme協議,這裡說的scheme(或者schema)泛指安卓和iOS的schema協議,因為它通用。
安卓和iOS都可以通過攔截跳頁URL請求,然後解析這個scheme協議,符合約定規則的就扔個Native的方法處理。安卓和iOS分別用到攔截URL請求的方法是:
- 安卓:shouldOverrideUrlLoading方法
- iOS:UIWebView的delegate函式
喚起APP技術
上文介紹到的JSBridge是在APP內的Web頁面跟APP進行交換,還有一種特別多的需求,就是在APP外(瀏覽器、微信等)調起APP自己,給APP進行導流。這時候就要用到APP的喚起技術。這裡有一下幾種方法:
- intent:安卓
- localserver:安卓
- Universal links:iOS 9+
- Deep link/Applink:安卓
- smart app banner:iOS
安卓intent
intent格式示例如下:
intent:
HOST/URI-path // Optional host
#Intent;
package=[string];
action=[string];
category=[string];
component=[string];
scheme=[string];
S.xxx=xxx
end;複製程式碼
- 第一部分:host和path是跟url無異
- 第二部分:#intent到end是完整的intent,包含了調起的app包名,action等是常用的配置項
因為Intent不僅僅是調起APP,而是安卓客戶端內部模組通訊也會用,所以許可權很大,一般瀏覽器都給封掉了,?
安卓localserver
這是一個黑科技?,早前安卓允許在本地啟動一個本地server,這個server是在後臺守候的,通過這個localserver都可以進行各種需求:app間通訊、app調起、收集資料、基礎服務。百度的moplus就是這樣的一個localserver。
舉例說明:啟動一個本地server,埠號是:8888
,那麼在手機上,網頁就可以通過:http://127.0.0.1:8888
訪問這個server,server接收到請求就可以進行一些native的操作,對於需要回撥資料的,就通過返回請求內容來執行,比如:
- 獲取個定位資訊,js執行
$.get('http://127.0.0.1:8888/getGeoLocation?callback=cbname')
- server收到請求之後,呼叫native方法,獲取GPS的定位資訊,然後將資料通過response:
window.cbname&&cbname({xxx})
給頁面返回定位資料
如果控制不好許可權,因為localserver是一直後臺守候的,容易被利用,比如提權獲取通訊錄、甚至給通訊錄發簡訊、容易造成蠕蟲攻擊,感興趣的可以搜下moplus的文章。另外安卓各種安全軟體,都會清理記憶體和後臺程式,很容易被幹掉程式。瀏覽器也會封殺本地server調起,碰見127.0.0.1的請求就直接攔截。
Universal links / Deep link / Applink
這三個是官方推薦的調起方法,調起協議格式也是可以統一的,比如前文提到的hybrid://action?arg1=xxx
這類scheme協議就是。這樣可以統一安卓和iOS調起和JSBridge通訊。
其實簡單來說,這三個出發點是想給應用做分發,但是如果使用者手機沒有安裝這個APP,那麼就調起失敗,這時候直接不管不問,肯定體驗不好,而且浪費了點選資源,那麼做成分發吧!將調起協議做成一個調起頁面,放到一個域名下,點選這個URL就可以開啟這個頁面,頁面執行程式碼調起APP,如果調起失敗就展現APP的介紹,做分發。
Universal links / Deep link / Applink,就是這樣的一個過程,通過這個域名授權,把URL分發給APP進行處理,唯一不同的是:如果使用者安裝了APP,那麼就不用開啟這個分發頁面了。
Universal Links
iOS 9新出的一個功能,需要在App內宣告一個https域名(ul.test.com),然後在該網站根目錄放置apple-app-site-association檔案,檔案指明瞭轉發規則,例如:
{
"applinks": {
"apps": [],
"details": [
{
"appID": “xxx.com.baidu.SomeApp”,
"paths": ["*"]
}
]
}
}複製程式碼
當APP安裝成功之後,會下載這個檔案,明確知道遇見ul.test.com
的域名的URL時候,會把這個URL扔給你的APP,讓你去解析,APP拿到這個URL就可以解析出來需要做什麼事情。
Universal Link是iOS 9+的底層實現,所以在任何地方都可以直接調起APP,不受微信這類封閉APP的限制。
Deep link / Applink
Deep link 是安卓一開始推出的,主要用於搜尋調起APP,後來推出 Applink,實際是Deep link的升級版。
這裡需要提到微信的APPlink,畢竟微信作為SuperApp,是很大的分發資源,微信有自己的分發方法,安卓內可以申請微信的APPlink,跟Universal link一樣,也是一個域名下面的URL,符合一定規則就由微信(ios是底層系統)扔個對應的域名APP進行解析。
smart app banner
在頁面的head中新增下面meta,在Safari瀏覽器中就會出現下面的banner
<meta name="apple-itunes-app" content="app-id=myAppStoreID, affiliate-data=myAffiliateData, app-argument=myURL">複製程式碼
JSBridge安全
在APP內JSBridge可以實現Web和Native的通訊,但是如果APP開啟一個惡意的頁面,頁面可以任意呼叫JSBridge方法,獲取各種隱私的資料,就會引起安全問題。
JSBridge的安全有兩個方法:通過Native進行白名單配置,通過Server雲端授權。Server的雲端授權這塊,放到後續JSSDK的設計部分進行詳細講解。本文主要說下通過Native的方式來控制JSBridge的安全。
假設JSBridge的協議格式如下:
hybrid://action/method?arg1=xxx&arg2=xxx複製程式碼
可以通過下面方式進行安全設定:
- 配置某些方法的使用範圍,比如固定的Webview,固定的domain
- 通過正則來設定細化的許可權,比如:baidu.com網頁可以使用
*
,hao123.com可以使用:hybrid://hao123/*
JSBridge的最佳實踐
介紹了這麼多,什麼是最佳實踐的JSBridge呢?結合文章內容,要求JSBridge做到以下幾點:
- 官方認可,不走「歪門邪道」
- 跨平臺通用
- APP內和APP外規範通用
- 安全可靠
- 約定大於配置的原則
綜合上文介紹的內容,JSBridge的最佳實踐是:
- 協議規範都使用:
hybrid://action/method?arg1=xxx&arg2=xxx
- iOS使用Universal Link和UIWebview的delegate
- 安卓使用shouldOverrideUrlLoading和Applink
規範和約定
先貼個URL scheme的圖片,理解下URL的組成部分:
約定我們的規範如下:
yourappscheme://module/action?arg1=x&arg2=x&ios_version=xxx&andr_version=xxx&upgrade=1/0&callback=xxx&sendlog=1/0複製程式碼
- 整體小寫
- yourappscheme:就是你的scheme,可辨識,別衝突,通過這個可以進行Universal Link和Applink的分發
- module和action:某個模組元件的某個方法
?
後面是querystring,這裡預定了幾個特殊的引數:ios_version/andr_version
:非必須,iOS和安卓的最小版本,即本協議從哪個版本開始支援的,低版本不支援則忽略,配合upgrade使用進行APP升級- upgrade:是否強制升級,即當版本低於設定的ios/andr_version是否彈出提示使用者升級的對話方塊(yourappscheme已經可以調起app,只不過功能可能因為版本低不支援,這時候可以引導使用者升級)
- callback:非同步回撥函式,下面詳細樹下callback的最佳實踐
- sendlog:調起後是否打點傳送日誌
舉例:
# 賬號相關
## 開啟使用者個人主頁
fb://account/userprofile?id=xxx
## 開啟登入介面
fb://account/login?callback=xxx
# 工具類
## 獲取定位
fb://utils/getgeolocation?callback=xx複製程式碼
callback的設計
當native操作成功之後,會將處理結束後的結果或者資料通過callback
回撥傳給Web,當然有成果就又失敗,callback
的引數設計有兩種方式:
錯誤優先
即下面的回撥方法格式:
function callback(error, data){
if(error){
throw error
}
console.log(data)
}複製程式碼
JSON API式
即回撥方法只接收一個JSON物件,JSON格式如下:
{
error_code: 0,
data: {}
}複製程式碼
預留升級/日誌能力
做APP開發經常會遇見下面的問題:
- 功能/端能力是從某個版本開始的,低版本用不了,但是scheme還是會調起APP(APP懵逼。。
- 對於低版本,PM希望提示使用者升級
- 統計調起成功率,分發次數之類的統計需求
scheme的querystring部分由 ios_version/andr_version
和upgrade這三個成對的引數,可以解決升級問題,sendlog解決日誌統計問題。
ios_version/andr_version
:是標示該協議的最低支援版本,如果低於這個版本可能因為功能並未實現而能識別。- upgrade:是是否強制低版本彈出升級對話方塊
- sendlog:當為1的時候,則傳送調起成功失敗之類的統計需求
簡單JSBridge呼叫封裝
簡單封裝下JSBridge呼叫的方法,引數如下:
- module:類名稱,如果account
- action:具體操作方法,如login
- args:非必須,協議引數,支援string和物件
- callback:非必須,回撥單獨提出來,方便全域性方法命名
具體程式碼如下
function invoke (module, action, args, callback) {
let scheme = `yourappscheme://${module}/${action}?`
if (isFunction(args)) {
callback = args
args = null
}
// 處理下引數
if (isString(args)) {
scheme += args
} else if (isObject(args)) {
each(args, (k, v) => {
if (isObject(v) || isArray(v)) {
v = JSON.stringify(v)
}
scheme += `${k}=${v}`
})
}
// callback獨立傳,方便全域性函式名命名
if (isFunction(callback)) {
var funcName = '_jsbridge_cb_' + getId()
window[funcName] = function () {
callback.apply(window, ([]).slice.call(arguments, 0))
}
scheme += (!~scheme.indexOf('?') ? '&' : '?') + `callback=${funcName}`
}
if (os.ios && versionCompare(os.version, '9.0') >= 0) {
window.location.href = scheme
} else {
var $node = document.createElement('iframe')
$node.style.display = 'none'
$node.src = scheme
var body = document.body || document.getElementsByTagName('body')[0]
body.appendChild($node)
setTimeout(function () {
body.removeChild($node)
$node = null
}, 10)
}
}複製程式碼
打完收工
今天寫的有點多,介紹了JSBridge常用的方法,然後介紹了APP外如何喚起APP,還介紹了scheme協議,最後比較了優缺點,做個最佳實踐。希望有用~
--eof--
@三水清
未經允許,請勿轉載
本文使用MPEditor(js8.in/mpeditor)編輯…
感覺有用,歡迎關注我的公眾號