JSBridge原理與實作分析
前言:本文講述JSBridge相關原理以及實作方面的知識,主内容整理自網絡。
Hybrid App簡介
hybrid app是一種将native app和web app結合起來的一項app開發政策。
衆所周知,native app開發周期長,稽核嚴格,同時更新較為繁瑣,但使用者體驗非常好;而web app開發周期比較短,版本更新疊代都非常快速,但是使用者體驗較差。它們各自擁有不同的适用場景,那麼能否根據具體的業務場景,在app開發的過程中将二者結合起來,發揮各自的長處呢?有!那就是hybrid混合app。
一般hybrid app的體驗效果介于上述兩類app之間,其原理是将android和iOS app中都有的WebView(Android為WebView,iOS為UIWebView)嵌入到app頁面中并像浏覽器一樣加載網頁。如果将業務頁面寫成網頁形式,通過WebView加載到app中,就可以将web app和native app混合起來,成為hybrid app。
但是僅僅嵌入WebView并加載特定的頁面并不能完全滿足所有的業務場景,有些業務場景需要使用原生能力,比如擷取定位、調用原生彈窗等。有時app也需要通知業務頁面作出指定的行為,比如重定向、修改隔離配置等。這時候就需要js和native之間可以通信(互相調用),即業務頁面的js腳本可以調用原生代碼去執行相關操作,原生代碼可以調用js代碼去操作業務邏輯。
下面将分析js與native幾種通信政策的實作原理以及相容等問題。
Js與Native通信
JavaScript 調用 Native
JavaScript 調用 Native 的方式,主要有兩種:注入 API 和 攔截 URL SCHEME。
注入API
注入 API 方式的主要原理是,通過 WebView 提供的接口,向 JavaScript 的 Context(window)中注入對象或者方法,讓 JavaScript 調用時,直接執行相應的 Native 代碼邏輯,達到 JavaScript 調用 Native 的目的。
iOS UIWebView實作:
JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
context[@"postBridgeMessage"] = ^(NSArray<NSArray *> *calls) {
// Native 邏輯
};
前端調用方式:
iOS WKWebView實作:
@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];
// TODO 顯示 WebView
}
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"nativeBridge"]) {
NSLog(@"前端傳遞的資料 %@: ",message.body);
// Native 邏輯
}
}
前端調用方式:
Android addJavascriptInterface 實作:
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;
}
@JavascriptInterface
public void postMessage(String webMessage){
// Native 邏輯
}
}
}
前端調用方式:
Android 攔截Prompt 實作:
該實作并不屬于注入API類型,它的原理是通過js調用prompt(或alert\confirm)方法觸發WebView相關事件,并将調用的接口和參數資訊存入事件對象,android端通過重寫事件響應函數,擷取到相關的接口和參數資訊,并執行調用操作。
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
return super.onJsPrompt(view, url, message, defaultValue, result);
}
前端調用方式:
注意:在 4.2 之前,Android 注入 JavaScript 對象的接口是 addJavascriptInterface,但是這個接口有漏洞,可以被不法分子利用,危害使用者的安全,是以在 4.2 中引入新的接口 @JavascriptInterface(上面代碼中使用的)來替代這個接口,解決安全問題。是以 Android 注入js對象的方式是有相容性問題的,Android 4.2以前多采用的是攔截 Prompt 實作的。
攔截 URL SCHEME
URL SCHEME:URL SCHEME是一種類似于url的連結,是為了友善app直接互相調用設計的,形式和普通的 url 近似,主要差別是 protocol 和 host 一般是自定義的,例如: qunarhy://hy/url?url=http://ymfe.tech,protocol 是 qunarhy,host 則是 hy。
攔截 URL SCHEME 的主要流程是:Web 端通過某種方式(例如 iframe.src)發送 URL Scheme 請求,之後 Native 攔截到請求并根據 URL SCHEME(包括所帶的參數)進行相關操作。
前端發送 URL Scheme:
//建立隐藏iframe過程
var messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
document.documentElement.appendChild(messagingIframe);
//觸發scheme
messagingIframe.src = uri;
Android shouldOverrideUrlLoading 攔截 Scheme:
public boolean shouldOverrideUrlLoading(WebView view, String url){
//如果傳回false,則WebView處理連結url,如果傳回true,代表WebView根據程式來執行url
return true;
}
iOS中,UIWebView有個特性:在UIWebView内發起的所有網絡請求,都可以通過delegate函數在Native層得到通知。這樣,我們可以在webview中捕獲url scheme的觸發(原理是利用 shouldStartLoadWithRequest)。
iOS shouldStartLoadWithRequest 攔截 Scheme:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSURL *url = [request URL];
NSString *requestString = [[request URL] absoluteString];
//擷取利潤url scheme後自行進行處理
}
在實際使用過程中,這種方式有一定的 缺陷:
- 使用 iframe.src 發送 URL SCHEME 會有 url 長度的隐患。
- 建立請求,需要一定的耗時,比注入 API 的方式調用同樣的功能,耗時會較長。
但是之前為什麼很多方案使用這種方式呢?因為它 支援 iOS6。而現在的大環境下,iOS6 占比很小,基本上可以忽略,是以并不推薦為了 iOS6 使用這種并不優雅 的方式。
注意:
- 有些方案為了規避 url 長度隐患的缺陷,在 iOS 上采用了使用 Ajax 發送同域請求的方式,并将參數放到 head 或 body 裡。這樣,雖然規避了 url 長度的隐患,但是 WKWebView 并不支援這樣的方式。
- 為什麼選擇 iframe.src 不選擇 locaiton.href ?因為如果通過 location.href 連續調用 Native,很容易丢失一些調用。
Native 調用 JavaScript
相比于 JavaScript 調用 Native, Native 調用 JavaScript 較為簡單,畢竟不管是 iOS 的 UIWebView 還是 WKWebView,還是 Android 的 WebView 元件,都以子元件的形式存在于 View/Activity 中,直接調用相應的 API 即可。
Native 調用 JavaScript,其實就是執行拼接 JavaScript 字元串,從外部調用 JavaScript 中的方法,是以 JavaScript 的方法必須在全局的 window 上。(閉包裡的方法,JavaScript 自己都調用不了,更不用想讓 Native 去調用了)
iOS UIWebView 實作:
iOS WKWebView 實作:
Android Kitkat(4.4)之前loadUrl 實作:
Android Kitkat(4.4)之後 evaluateJavascript 實作:
webView.evaluateJavascript(javaScriptString, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
}
});
注意:使用 loadUrl 的方式,并不能擷取 JavaScript 執行後的結果。
JSBridge原理
JavaScript 是運作在一個單獨的 JS Context 中(例如,WebView 的 Webkit 引擎、JSCore)。由于這些Context 與原生運作環境的天然隔離,我們可以将這種情況與 RPC(Remote Procedure Call,遠端過程調用)通信進行類比,将 Native 與 JavaScript 的每次互相調用看做一次 RPC 調用。如此一來我們可以按照通常的 RPC 方式來進行設計和實作。
在 JSBridge 的設計中,可以把前端看做 RPC 的用戶端,把 Native 端看做 RPC 的伺服器端,進而 JSBridge 要實作的主要邏輯就出現了:通信調用(Native 與 JS 通信) 和 句柄解析調用。(如果你是個前端,而且并不熟悉 RPC 的話,你也可以把這個流程類比成 JSONP 的流程)
Native 和 Javascript 通信的原理是 JSBridge 實作的核心,實作方式可以各種各樣,但是萬變不離其宗。這裡,筆者推薦的實作方式如下:
- JavaScript 調用 Native 推薦使用 注入 API 的方式(iOS6 忽略,Android 4.2以下使用 WebViewClient 的 onJsPrompt 方式)。
- Native 調用 JavaScript 則直接執行拼接好的 JavaScript 代碼即可。
- 也可以将URL SCHEME的Restful形式的接口參數格式同注入API方式相結合,做到接口調用方式上的相容(隻改動底層實作)
JSBridge設計與實作
使用url scheme形式來設計和實作 JSBridge,現有的可參考架構和文章都非常多,若想詳細了解建議閱讀《JSBridge深度剖析》,此處不作贅述。
設計與實作
上文中已介紹了native與js互動的方式,但是都是較為底層的實作,實際使用時需要在 javascript 端和 native 端封裝出對應的子產品,來統一管理它們之間的互相調用。
javascript端的子產品需要實作兩個功能:
- 調用 Native(給 Native 發消息)
- 被 Native 調用(接收 Native 消息)
根據這兩個功能點,可以初步設計出JSBridge js子產品的初步結構:
window.JSBridge = {
// 調用 Native
invoke: function(msg) {
// 判斷環境,擷取不同的 nativeBridge
nativeBridge.postMessage(msg);
},
receiveMessage: function(msg) {
// 處理 msg
}
};
在上面的文章中,提到過 RPC 中有一個非常重要的環節是 句柄解析調用 ,這點在 JSBridge 中展現為 句柄與功能對應關系。同時,我們将句柄抽象為橋名(BridgeName),最終演化為 一個 BridgeName 對應一個 Native 功能或者一類 Native 消息。 基于此點,JSBridge 的實作可以優化為如下:
window.JSBridge = {
// 調用 Native
invoke: function(bridgeName, data) {
// 判斷環境,擷取不同的 nativeBridge
nativeBridge.postMessage({
bridgeName: bridgeName,
data: data || {}
});
},
receiveMessage: function(msg) {
var bridgeName = msg.bridgeName,
data = msg.data || {};
// 具體邏輯
}
};
JSBridge 大概的雛形出現了。現在終于可以着手解決這個問題了:消息都是單向的,那麼調用 Native 功能時 Callback 怎麼實作的?
對于 JSBridge 的 Callback ,其實就是 RPC 架構的回調機制。當然也可以用更簡單的 JSONP 機制解釋: 當發送 JSONP 請求時,url 參數裡會有 callback 參數,其值是目前頁面唯一 的,而同時以此參數值為 key 将回調函數存到 window 上,随後伺服器傳回 script 中,也會以此參數值作為句柄,調用相應的回調函數。
由此可見,callback 參數這個唯一辨別是這個回調邏輯的關鍵。這樣,我們可以參照這個邏輯來實作 JSBridge:用一個自增的唯一 id,來辨別并存儲回調函數,并把此 id 以參數形式傳遞給 Native,而 Native 也以此 id 作為回溯的辨別,來實作 Callback 回調邏輯。
(function () {
let id = 0,
callbacks = {};
window.JSBridge = {
// 調用 Native
invoke: function(bridgeName, callback, data) {
// 判斷環境,擷取不同的 nativeBridge
let thisId = id++; // 擷取唯一 id
callbacks[thisId] = callback; // 存儲 Callback
nativeBridge.postMessage({
bridgeName: bridgeName,
data: data || {},
callbackId: thisId // 傳到 Native 端
});
},
receiveMessage: function(msg) {
let bridgeName = msg.bridgeName,
data = msg.data || {},
callbackId = msg.callbackId; // Native 将 callbackId 原封不動傳回
// 具體邏輯
// bridgeName 和 callbackId 不會同時存在
if (callbackId) {
if (callbacks[callbackId]) { // 找到相應句柄
callbacks[callbackId](msg.data); // 執行調用
}
} else if (bridgeName) {
}
}
};
})();
最後用同樣的方式加上 Native 調用的回調邏輯,同時對代碼進行一些優化,就大概實作了一個功能比較完整的 JSBridge。其代碼如下:
(function () {
var id = 0,
callbacks = {},
registerFuncs = {};
window.JSBridge = {
// 調用 Native
invoke: function(bridgeName, callback, data) {
// 判斷環境,擷取不同的 nativeBridge
var thisId = id ++; // 擷取唯一 id
callbacks[thisId] = callback; // 存儲 Callback
nativeBridge.postMessage({
bridgeName: bridgeName,
data: data || {},
callbackId: thisId // 傳到 Native 端
});
},
receiveMessage: function(msg) {
var bridgeName = msg.bridgeName,
data = msg.data || {},
callbackId = msg.callbackId, // Native 将 callbackId 原封不動傳回
responstId = msg.responstId;
// 具體邏輯
// bridgeName 和 callbackId 不會同時存在
if (callbackId) {
if (callbacks[callbackId]) { // 找到相應句柄
callbacks[callbackId](msg.data); // 執行調用
}
} else if (bridgeName) {
if (registerFuncs[bridgeName]) { // 通過 bridgeName 找到句柄
var ret = {},
flag = false;
registerFuncs[bridgeName].forEach(function(callback) => {
callback(data, function(r) {
flag = true;
ret = Object.assign(ret, r);
});
});
if (flag) {
nativeBridge.postMessage({ // 回調 Native
responstId: responstId,
ret: ret
});
}
}
}
},
register: function(bridgeName, callback) {
if (!registerFuncs[bridgeName]) {
registerFuncs[bridgeName] = [];
}
registerFuncs[bridgeName].push(callback); // 存儲回調
}
};
})();
當然,這段代碼片段隻是一個示例,主要用于剖析 JSBridge 的原理和流程,裡面存在諸多省略和不完善的代碼邏輯,讀者們可以自行完善。
這一節主要講的是,JavaScript 端的 JSBridge 的實作,對于 Native 端涉及的并不多。在 Native 端配合實作 JSBridge 的 JavaScript 調用 Native 邏輯也很簡單,主要的代碼邏輯是:接收到 JavaScript 消息 => 解析參數,拿到 bridgeName、data 和 callbackId => 根據 bridgeName 找到功能方法,以 data 為參數執行 => 執行傳回值和 callbackId 一起回傳前端。 Native 調用 JavaScript 也同樣簡單,直接自動生成一個唯一的 ResponseId,并存儲句柄,然後和 data 一起發送給前端即可。
JSBridge 引用
對于 JSBridge 的引用,常用有兩種方式,各有利弊。
由 Native 端進行注入:
注入方式和 Native 調用 JavaScript 類似,直接執行橋的全部代碼。
- 優點:橋的版本很容易與 Native 保持一緻,Native 端不用對不同版本的 JSBridge 進行相容;
- 缺點:注入時機不确定,需要實作注入失敗後重試的機制,保證注入的成功率,同時 JavaScript 端在調用接口時,需要優先判斷 JSBridge 是否已經注入成功。
由 JavaScript 端引用:
直接與 JavaScript 一起執行。
- 優點:JavaScript 端可以确定 JSBridge 的存在,直接調用即可;
- 缺點:如果橋的實作方式有更改,JSBridge 需要相容多版本的 Native Bridge 或者 Native Bridge 相容多版本的 JSBridge。
靈犀LXJSBridge.js實作
實作原理基于上文,具體實作時,LXJSBridge.js在接口層面設計了兩類js調用native方式:一類是方法的直接調用,分為同步調用和異步調用;一類是注冊監聽事件的方式,但是這兩類調用方式其本質是一樣,js調用andriod native利用的是prompt方法,js調用ios native利用的postMessage方法。具體協定參見下圖,需要web端和native側約定好native能力的service以及action名稱。

針對異步調用的結果傳回以及native事件通知,LXJSBridge.js在接口層面也是設計了兩類native調用js接口:異步調用傳回結果使用callbackFromNative方法;事件觸發使用fireFromNative方法。
為了友善web端使用,我們仿照微信jssdk,在LXJSBridge之上又封裝了一層LXJSSDK的接口層,web隻需要使用LXJSSDK裡面的接口,就可以通路到native中的本地能力。具體參見代碼分析。
參考文章
- 《移動混合開發中的 JSBridge》
- 《JSBridge深度剖析》
- 《Hybrid應用開發入門》