介紹#
如今移動端盛行的年代,技術選型基本都是混合開發(Hybrid),即結合原生 Native 與 Web H5 兩者的優點同時使用這兩類技術:
- 原生技術指 iOS(Objective C、Swift)、Android(Java):開發效率較低,發布依賴用戶更新,但性能更高且功能覆蓋率更高
- Web 技術指 JavaScript:開發效率更高,發布更新靈活,但性能較低且功能特性也受限
這種模式下,H5 經常需要使用 Native 的功能,比如打開攝像頭、查看本地相冊、圖片上傳、頁面分享等等,Native 也需要向 Web 端推送更新狀態等。
而 JavaScript 是運行在單獨的 JS Context(Webview 容器、JSCore 等),與原生運行環境有隔離,需要有一種機制實現 Native 和 Web 端的雙向通信,這種機制就是 JSBridge:
以
JavaScript
引擎或Webview
容器作為媒介,通過協定協議進行通信,實現 Native 和 Web 雙向通信的一種機制。
通過 JSBridge,Web 端可以調用 Native 端的 Java 接口,Native 端也可以調用 Web 端的 JS 接口,實現雙向調用。
通信原理#
JS 調用 Native#
JavaScript 調用 Native 主要有三種方式:注入 API、攔截 URL SCHEME 和 重寫 prompt 等 JS 全局方法
注入 API#
主要原理是:通過 Webview 提供的接口,向 JS 的運行環境(window)中注入對象或方法,讓 JS 調用時直接執行相應的 Native 代碼邏輯
iOS UIWebView 實現(主要是 JavaScriptCore
)
JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
context[@"postBridgeMessage"] = ^(NSArray<NSArray *> *calls) {
// Native 邏輯
};
前端調用
window.postBridgeMessage(message)
iOS WKWebView 實現(主要是 WKScriptMessageHandler
)
@interface WKWebVIewVC ()<WKScriptMessageHandler>
@implementation WKWebVIewVC
- (void)viewDidLoad {
[super viewDidLoad];
WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
configuration.userContentController = [[WKUserContentController alloc] init];
WKUserContentController *userCC = configuration.userContentController;
// 注入對象,前端調用其方法時,Native 可以捕獲到
[userCC addScriptMessageHandler:self name:@"nativeBridge"];
WKWebView wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];
}
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"nativeBridge"]) {
NSLog(@"前端傳遞的數據 %@: ",message.body);
// Native 邏輯
}
}
前端調用
window.webkit.messageHandlers.nativeBridge.postMessage(message);
Android 實現
/**
在 4.2 之前,Android 注入 JavaScript 對象的接口是 addJavascriptInterface,
但是這個接口有漏洞,可以被不法分子利用,危害用戶的安全,
因此在 4.2 中引入新的接口 @JavascriptInterface 來替代這個接口,解決安全問題。
所以 Android 注入對對象的方式是 有兼容性問題的
*/
publicclassJavaScriptInterfaceDemoActivityextendsActivity{
private WebView Wv;
@Override
publicvoidonCreate(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");
}
publicclassJavaScriptInterface{
Context mContext;
JavaScriptInterface(Context c) {
mContext = c;
}
publicvoidpostMessage(String webMessage){
// Native 邏輯
}
}
}
前端調用
window.nativeBridge.postMessage(message);
攔截 URL Scheme#
URL Scheme
是一種類似 url 的鏈接,為了方便 App 直接相互調用設計,格式如下:
<protocol>://<host>/<path>?<qeury>#fragment
自定義的一個 JSBridge 通信的 URL Scheme:jsbridge://showToast?text=hello
主要原理是:Native 加載 Webview 後,Web 發送的所有請求都會經過 Webview 組件,所以 Native 可以重寫對應方法來攔截請求判斷符合自定義的 URL Scheme 格式則進行解析處理進而調用原生 Native 方法
Web 發送 URL 請求有以下幾種方式
a
標籤(缺點:需要用戶操作)location.href
(缺點:可能引起頁面跳轉丟失調用)ajax
請求(Android 沒有相應攔截方法)- 使用
iframe src
以上方法中 iframe src
從早期開始就是經常使用的方案,兼容性很好:
- 安卓提供了
shouldOverrideUrlLoading
方法攔截 UIWebView
使用shouldStartLoadWithRequest
,WKWebView
則使用decidePolicyForNavigationAction
不過也有一定的缺陷:
- 基於 URL,長度有所限制
- 創建請求有一定耗時,比起注入 API 的方式調用同樣功能耗時更長
重寫 prompt 等 JS 全局方法#
Webview
有一個方法 setWebChromeClient
可以設置 WebChromeClient
對象,其中有三個方法 onJsAlert
、onJsConfirm
、onJsPrompt
,當 JS 調用 window.alert
、window.confirm
、window.prompt
時上面對應的方法也會被觸發,利用這個機制也能做一些處理
由於攔截上述方法會對性能造成一定影響,因此需要選擇使用頻率較低的方法,而在 Android 中,相比其它幾個方法,幾乎不會使用到 prompt 方法,因此佔用 prompt 是最佳方案。
Native 調用 JS#
Native 端調用 JS 端,這個比較簡單,JavaScript 作為解釋性語言,最大的特性就是可以隨時隨地地通過解釋器執行一段 JS 代碼,所以可以將拼接的 JavaScript 代碼字符串,傳入 JS 解析器執行就可以,JS 解析器在這裡就是 Webview。
- iOS UIWebview 使用
stringByEvaluatingJavaScriptFromString
NSString *jsStr = @"執行的JS代碼";
[webView stringByEvaluatingJavaScriptFromString:jsStr];
- iOS WKWebView 使用
evaluateJavaScript
[webView evaluateJavaScript:@"執行的JS代碼" completionHandler:^(id _Nullable response, NSError * _Nullable error) {
}];
- Android 4.4 之前只能使用
loadUrl
實現並且無法執行回調且調用的時候會刷新 WebView
String jsCode = String.format("window.showWebDialog('%s')", text);
webView.loadUrl("javascript: " + jsCode);
- Android 4.4 之後提供了
evaluateJavascript
來執行 JS 代碼可以獲取返回值執行回調
String jsCode = String.format("window.showWebDialog('%s')", text);
webView.evaluateJavascript(jsCode, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
}
});
帶回調的調用#
上面提到的 Native、Web 雙向通信的幾種方法站在一端看還是一個單向通信的過程:Web 端調用 Native 方法,Native 直接進行相關操作但無法將結果返回 Web。
那麼如何實現對端操作後返回結果呢?
基於之前的單向通信,一端調用時在參數中加一個 callbackId 標記對應的回調,對端收到調用請求後進行實際操作後再進行一次調用將結果、callbackId 回傳回來,原端根據 callbackId 匹配相應的回調將結果傳入執行即可,實際上兩端都走了一次調用。
具體例子:在 Web 端點擊按鈕,獲取 Native 端輸入框的值,並將值以 Web 端彈窗展示。
// web 端代碼
<body>
<div>
<button id="showBtn">獲取 Native 輸入,以 Web 彈窗展現</button>
</div>
</body>
<script>
let id = 1;
// 根據 id 保存callback
const callbackMap = {};
// 使用 JSSDK 封裝調用與 Native 通信的事件,避免過多的污染全局環境
window.JSSDK = {
// 獲取 Native 端輸入框 value,帶有回調
getNativeEditTextValue(callback) {
const callbackId = id++;
callbackMap[callbackId] = callback;
// 調用 JSB 方法,並將 callbackId 傳入
window.NativeBridge.getNativeEditTextValue(callbackId);
},
// 接收 Native 端傳來的 callbackId
receiveMessage(callbackId, value) {
if (callbackMap[callbackId]) {
// 根據 ID 匹配 callback,並執行
callbackMap[callbackId](value);
}
}
};
const showBtn = document.querySelector('#showBtn');
// 綁定按鈕事件
showBtn.addEventListener('click', e => {
// 通過 JSSDK 調用,將回調函數傳入
window.JSSDK.getNativeEditTextValue(value => window.alert('Natvie輸入值:' + value));
});
</script>
// Android 端代碼
webView.addJavascriptInterface(new NativeBridge(this), "NativeBridge");
class NativeBridge {
private Context ctx;
NativeBridge(Context ctx) {
this.ctx = ctx;
}
// 獲取 Native 端輸入值
@JavascriptInterface
public void getNativeEditTextValue(int callbackId) {
MainActivity mainActivity = (MainActivity)ctx;
// 獲取 Native 端輸入框的 value
String value = mainActivity.editText.getText().toString();
// 需要注入在 Web 執行的 JS 代碼
String jsCode = String.format("window.JSSDK.receiveMessage(%s, '%s')", callbackId, value);
// 在 UI 線程中執行
mainActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
mainActivity.webView.evaluateJavascript(jsCode, null);
}
});
}
}
這樣就實現了 Web -> Native 帶有回調的調用,同理 Native -> Web 也是一樣的邏輯(相對 callbackId 有 bridgeName 的概念)
這裡也貼一張完整的調用時序圖,可以看到整個過程跟 JSONP 的機制非常相像,需要兩端遵循一套調用機制:
引用方式#
實際的 JSBridge 引用方式有以下兩種,各有利弊:
方式 | 優點 | 缺點 |
---|---|---|
Native 端注入 | JSBridge 版本容易與 Native 對齊,不用考慮兼容 | 注入時機不確定,需要有失敗重試機制 同時 JS 端調用時也要判斷是否已注入 |
JS 端引用 | 時機可控,JS 直接調用方便 | 需要考慮 JSBridge 與 Native 之間多版本的兼容 |