第一次接觸到混合開發應該是在一年前,當時在做 ionic 和 Cordova(PhoneGap)專案的時候,這些框架在 web 基礎上包了一層 Native,然後透過 Bridge 技術使得 js 可以呼叫影片、位置、音訊等功能。目前手上負責的專案則是一個與自家 APP 互動的混合開發專案,於是在課餘時間就查了查相關的實現方案和原理。本文就是介紹這層 Bridge 的互動原理,主要描述了 js 與 ios 及 android 底層的通訊原理及 JSBridge 的封裝技術及除錯方法。
主要用途
JSBridge 簡單來講,主要是 給 JavaScript 提供呼叫 Native 功能的介面,讓混合開發中的前端部分可以方便地使用定位、攝像頭、系統相簿等 Native 功能。
但是 JSBridge 的用途肯定不只呼叫 Native 功能這麼簡單。實際上,JSBridge 就像其名稱中的
Bridge
的意義一樣,是 Native 和非 Native 之間的橋樑,它的核心是 構建
Native
和
非 Native
間訊息通訊的通道,而且是 雙向通訊的通道。
所謂 雙向通訊的通道:
JS 向 Native 傳送訊息 : 呼叫相關功能、通知 Native 當前 JS 的相關狀態等。
Native 向 JS 傳送訊息 : 回溯呼叫結果、訊息推送、通知 JS 當前 Native 的狀態等。
實現原理
主要分為兩個部分,分別是
JavaScript 呼叫 Native
和
Native 呼叫 JavaScript
JavaScript 呼叫 Native
主要有兩種實現方案,一種是
攔截 URL SCHEME
,另一種是
注入API讓js直接呼叫
攔截 URL SCHEME
什麼是 URL SCHEME
由於蘋果的 app 都是在沙盒中,相互是不能訪問資料的。但是蘋果還是給出了一個可以在 app 之間跳轉的方法:URL Scheme。URL SCHEME:URL SCHEME 是一種類似於 url 的連結,是為了方便 iosapp 直接互相跳轉設計的,形式和普通的 url 近似,主要區別是 protocol 和 host 一般是自定義的,例如: lefit://leoao/xxxx?xx=123,protocol 是 lefit,host 則是 leoao
實現流程
在 UIWebView 內發起的所有網路請求,都可以透過 delegate 函式在 Native 層得到通知。這樣,我們就可以在 UIWebView 內發起一個自定義的網路請求,攔截 URL SCHEME 的主要流程是:Web 端透過某種方式(例如 iframe。src)傳送 URL Scheme 請求,之後 Native 攔截到請求並根據 URL SCHEME(包括所帶的引數)進行相關操作。
在實際過程中,這種方式有一定的 缺陷:
使用 iframe。src 傳送 URL SCHEME 會有 url 長度的隱患。
建立請求,需要一定的耗時,比注入 API 的方式呼叫同樣的功能,耗時會較長。
但是之前為什麼很多方案使用這種方式呢?因為它 支援 iOS6。而現在的大環境下,iOS6 佔比很小,基本上可以忽略,所以並不推薦為了 iOS6 使用這種 並不優雅 的方式。
注 1
:有些方案為了規避 url 長度隱患的缺陷,在 iOS 上採用了使用 Ajax 傳送同域請求的方式,並將引數放到 head 或 body 裡。這樣,雖然規避了 url 長度的隱患,但是 WKWebView 並不支援這樣的方式。
注 2
:為什麼選擇 iframe。src 不選擇 locaiton。href ?因為透過 location。href 有個問題,就是如果我們連續多次修改 window。location。href 的值,在 Native 層只能接收到最後一次請求,前面的請求都會被忽略掉。
注入 API 讓 js 直接呼叫
注入 API 方式的主要原理是,透過 WebView 提供的介面,向 JavaScript 的 Context(window)中注入物件或者方法,讓 JavaScript 呼叫時,直接執行相應的 Native 程式碼邏輯,達到 JavaScript 呼叫 Native 的目的。
iOS
對於 iOS 的 UIWebView,例項如下:
JSContext *context = [uiWebView valueForKeyPath:@“documentView。webView。mainFrame。javaScriptContext”];context[@“postBridgeMessage”] = ^(NSArray
前端呼叫方式:
window。postBridgeMessage(message);
對於 iOS 的 WKWebView 可以用以下方式:
@interface WKWebVIewVC ()
前端呼叫方式:
window。webkit。messageHandlers。nativeBridge。postMessage(message);
Android
對於 Android 可以採用下面的方式:
public class JavaScriptInterfaceDemoActivity extends Activity { private WebView Wv; @Override public void onCreate(Bundle savedInstanceState) { super。onCreate(savedInstanceState); Wv = (WebView)findViewById(R。id。webView); final JavaScriptInterface myJavaScriptInterface = new JavaScriptInterface(this); Wv。getSettings()。setJavaScriptEnabled(true); Wv。addJavascriptInterface(myJavaScriptInterface, “nativeBridge”); // TODO 顯示 WebView } public class JavaScriptInterface { Context mContext; JavaScriptInterface(Context c) { mContext = c; } public void postMessage(String webMessage){ // Native 邏輯 } }}
前端呼叫方式:
window。nativeBridge。postMessage(message);
在 4。2 之前,Android 注入 JavaScript 物件的介面是
addJavascriptInterface
,但是這個介面有漏洞,可以被不法分子利用,危害使用者的安全,因此在 4。2 中引入新的介面
@JavascriptInterface
(上面程式碼中使用的)來替代這個介面,解決安全問題。所以 Android 注入對物件的方式是 有相容性問題的。
javascript 執行以下四種行為會被 webview 監聽到,箭頭後面是對應觸發的 Java 方法。由於 prompt 相對來說使用的很少,所以 4。2 之前很多方案都採用攔截 prompt 的方式來實現。
1、window。alert => onJSAlert2、window。confirm => onJSConfirm3、window。prompt => onJsPrompt4、window。location => shouldOverrideUrlLoading
Native 呼叫 JavaScript
Native 呼叫 JavaScript,其實就是
執行拼接 JavaScript 字串
,從外部呼叫 JavaScript 中的方法,因此 JavaScript 的方法必須在全域性的 window 上。(閉包裡的方法,JavaScript 自己都調用不了,更不用想讓 Native 去呼叫了)
iOS
對於 iOS 的 UIWebView,示例如下:
result = [uiWebview stringByEvaluatingJavaScriptFromString:javaScriptString];
對於 iOS 的 WKWebView,示例如下:
[wkWebView evaluateJavaScript:javaScriptString completionHandler:completionHandler];
Android
對於 Android,在 Kitkat(4。4)之前並沒有提供 iOS 類似的呼叫方式,只能用 loadUrl 一段 JavaScript 程式碼,來實現:
webView。loadUrl(“javascript:” + javaScriptString);
而 Kitkat 之後的版本,也可以用 evaluateJavascript 方法實現:
webView。evaluateJavascript(javaScriptString, new ValueCallback
注
:使用 loadUrl 的方式,並不能獲取 JavaScript 執行後的結果。