關於 JSBridge,絕大多數同學最早遇到的是微信的 WeiXinJSBridge(現在被封裝成 JSSDK),各種 Web 頁面可以通過 Bridge 呼叫微信提供的一些原生功能,為使用者提供相關的功能。其實,JSBridge 很早就出現在軟體開發中,在一些桌面軟體中很早就運用了這樣的形式,多用在通知、產品詳情、廣告等模組中,然後這些模組中,使用的是 Web UI,而相關按鈕點選後,呼叫的是 Native 功能。現在移動端盛行,不管是 Hybrid 應用,還是 React-Native 都離不開 JSBridge,當然也包括在國內舉足輕重的微信小程式。那麼,JSBridge 到底是什麼?它的出現是為了什麼?它究竟是怎麼實現的?在這篇文章中,會在移動混合開發的範疇內,將給大家帶來 JSBridge 的深入剖析。
1 前言
有些童鞋聽到 JSBridge 這個名詞,就是覺得非常高上大,有了它 Web 和 Native 可以進行互動,就像『進化藥水』,讓 Web 搖身一變,成為移動戰場的『上將一名』。其實並非如此,JSBridge 其實真是一個很簡單的東西,更多的是一種形式、一種思想。
2 JSBridge 的起源
為什麼是 JSBridge ?而不是 PythonBridge 或是 RubyBridge ?
當然不是因為 JavaScript 語言高人一等(雖然史丹佛大學已經把演算法導論的語言從 Java 改成 JavaScript,小得意一下,嘻嘻),主要的原因還是因為 JavaScript 主要載體 Web 是當前世界上的 最易編寫 、 最易維護 、最易部署 的 UI 構建方式。工程師可以用很簡單的 HTML 標籤和 CSS 樣式快速的構建出一個頁面,並且在服務端部署後,使用者不需要主動更新,就能看到最新的 UI 展現。
因此,開發維護成本 和 更新成本 較低的 Web 技術成為混合開發中幾乎不二的選擇,而作為 Web 技術邏輯核心的 JavaScript 也理所應當肩負起與其他技術『橋接』的職責,並且作為移動不可缺少的一部分,任何一個移動作業系統中都包含可執行 JavaScript 的容器,例如 WebView 和 JSCore。所以,執行 JavaScript 不用像執行其他語言時,要額外新增執行環境。因此,基於上面種種原因,JSBridge 應運而生。
PhoneGap(Codova 的前身)作為 Hybrid 鼻祖框架,應該是最先被開發者廣泛認知的 JSBridge 的應用場景;而對於 JSBridge 的應用在國內真正興盛起來,則是因為殺手級應用微信的出現,主要用途是在網頁中通過 JSBridge 設定分享內容。
移動端混合開發中的 JSBridge,主要被應用在兩種形式的技術方案上:
基於 Web 的 Hybrid 解決方案:例如微信瀏覽器、各公司的 Hybrid 方案
非基於 Web UI 但業務邏輯基於 JavaScript 的解決方案:例如 React-Native
【注】:微信小程式基於 Web UI,但是為了追求執行效率,對 UI 展現邏輯和業務邏輯的 JavaScript 進行了隔離。因此小程式的技術方案介於上面描述的兩種方式之間。
3 JSBridge 的用途
JSBridge 簡單來講,主要是 給 JavaScript 提供呼叫 Native 功能的介面,讓混合開發中的『前端部分』可以方便地使用地址位置、攝像頭甚至支付等 Native 功能。
既然是『簡單來講』,那麼 JSBridge 的用途肯定不只『呼叫 Native 功能』這麼簡單寬泛。實際上,JSBridge 就像其名稱中的『Bridge』的意義一樣,是 Native 和非 Native 之間的橋樑,它的核心是 構建 Native 和非 Native 間訊息通訊的通道,而且是 雙向通訊的通道。
所謂 雙向通訊的通道:JS 向 Native 傳送訊息 : 呼叫相關功能、通知 Native 當前 JS 的相關狀態等。
Native 向 JS 傳送訊息 : 回溯呼叫結果、訊息推送、通知 JS 當前 Native 的狀態等。
這裡有些同學有疑問了:訊息都是單向的,那麼呼叫 Native 功能時 Callback 怎麼實現的? 對於這個問題,在下一節裡會給出解釋。
4 JSBridge 的實現原理
JavaScript 是執行在一個單獨的 JS Context 中(例如,WebView 的 Webkit 引擎、JSCore)。由於這些 Context 與原生執行環境的天然隔離,我們可以將這種情況與 RPC(Remote Procedure Call,遠端過程呼叫)通訊進行類比,將 Native 與 JavaScript 的每次互相呼叫看做一次 RPC 呼叫。
在 JSBridge 的設計中,可以把前端看做 RPC 的客戶端,把 Native 端看做 RPC 的伺服器端,從而 JSBridge 要實現的主要邏輯就出現了:通訊呼叫(Native 與 JS 通訊) 和 控制程式碼解析呼叫。(如果你是個前端,而且並不熟悉 RPC 的話,你也可以把這個流程類比成 JSONP 的流程)
通過以上的分析,可以清楚地知曉 JSBridge 主要的功能和職責,接下來就以 Hybrid 方案 為案例從這幾點來剖析 JSBridge 的實現原理。
4.1 JSBridge 的通訊原理
Hybrid 方案是基於 WebView 的,JavaScript 執行在 WebView 的 Webkit 引擎中。因此,Hybrid 方案中 JSBridge 的通訊原理會具有一些 Web 特性。
4.1.1 JavaScript 呼叫 Native
JavaScript 呼叫 Native 的方式,主要有兩種:注入 API 和 攔截 URL SCHEME。
4.1.1.1 注入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 邏輯
};
複製程式碼
前端呼叫方式:
window.postBridgeMessage(message);
複製程式碼
對於 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 邏輯
}
}
複製程式碼
前端呼叫方式:
window.webkit.messageHandlers.nativeBridge.postMessage(message);
複製程式碼
對於 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");
// TODO 顯示 WebView
}
publicclassJavaScriptInterface{
Context mContext;
JavaScriptInterface(Context c) {
mContext = c;
}
publicvoidpostMessage(String webMessage){
// Native 邏輯
}
}
}
複製程式碼
前端呼叫方式:
1
window.nativeBridge.postMessage(message);
複製程式碼
在 4.2 之前,Android 注入 JavaScript 物件的介面是 addJavascriptInterface,但是這個介面有漏洞,可以被不法分子利用,危害使用者的安全,因此在 4.2 中引入新的介面 @JavascriptInterface(上面程式碼中使用的)來替代這個介面,解決安全問題。所以 Android 注入對物件的方式是 有相容性問題的。(4.2 之前很多方案都採用攔截 prompt 的方式來實現,因為篇幅有限,這裡就不展開了。)
4.1.1.2 攔截 URL SCHEME
先解釋一下 URL SCHEME:URL SCHEME是一種類似於url的連結,是為了方便app直接互相呼叫設計的,形式和普通的 url 近似,主要區別是 protocol 和 host 一般是自定義的,例如: qunarhy://hy/url?url=ymfe.tech,protocol 是 qunarhy,host 則是 hy。
攔截 URL SCHEME 的主要流程是:Web 端通過某種方式(例如 iframe.src)傳送 URL Scheme 請求,之後 Native 攔截到請求並根據 URL SCHEME(包括所帶的引數)進行相關操作。
在時間過程中,這種方式有一定的 缺陷:
使用 iframe.src 傳送 URL SCHEME 會有 url 長度的隱患。
建立請求,需要一定的耗時,比注入 API 的方式呼叫同樣的功能,耗時會較長。
但是之前為什麼很多方案使用這種方式呢?因為它 支援 iOS6。而現在的大環境下,iOS6 佔比很小,基本上可以忽略,所以並不推薦為了 iOS6 使用這種 並不優雅 的方式。
【注】:有些方案為了規避 url 長度隱患的缺陷,在 iOS 上採用了使用 Ajax 傳送同域請求的方式,並將引數放到 head 或 body 裡。這樣,雖然規避了 url 長度的隱患,但是 WKWebView 並不支援這樣的方式。
【注2】:為什麼選擇 iframe.src 不選擇 locaiton.href ?因為如果通過 location.href 連續呼叫 Native,很容易丟失一些呼叫。
4.1.2 Native 呼叫 JavaScript
相比於 JavaScript 呼叫 Native, Native 呼叫 JavaScript 較為簡單,畢竟不管是 iOS 的 UIWebView 還是 WKWebView,還是 Android 的 WebView 元件,都以子元件的形式存在於 View/Activity 中,直接呼叫相應的 API 即可。
Native 呼叫 JavaScript,其實就是執行拼接 JavaScript 字串,從外部呼叫 JavaScript 中的方法,因此 JavaScript 的方法必須在全域性的 window 上。(閉包裡的方法,JavaScript 自己都呼叫不了,更不用想讓 Native 去呼叫了)
對於 iOS 的 UIWebView,示例如下:
result = [uiWebview stringByEvaluatingJavaScriptFromString:javaScriptString];
複製程式碼
對於 iOS 的 WKWebView,示例如下:
[wkWebView evaluateJavaScript:javaScriptString completionHandler:completionHandler];
複製程式碼
對於 Android,在 Kitkat(4.4)之前並沒有提供 iOS 類似的呼叫方式,只能用 loadUrl 一段 JavaScript 程式碼,來實現:
webView.loadUrl("javascript:" + javaScriptString);
複製程式碼
而 Kitkat 之後的版本,也可以用 evaluateJavascript 方法實現:
webView.evaluateJavascript(javaScriptString, new ValueCallback<String>() {
@Override
publicvoidonReceiveValue(String value){
}
});
複製程式碼
【注】:使用 loadUrl 的方式,並不能獲取 JavaScript 執行後的結果。
4.1.3 通訊原理小總結
通訊原理是 JSBridge 實現的核心,實現方式可以各種各樣,但是萬變不離其宗。這裡,筆者推薦的實現方式如下:
JavaScript 呼叫 Native 推薦使用 注入 API 的方式(iOS6 忽略,Android 4.2以下使用 WebViewClient 的 onJsPrompt 方式)。
Native 呼叫 JavaScript 則直接執行拼接好的 JavaScript 程式碼即可。
對於其他方式,諸如 React Native、微信小程式 的通訊方式都與上描述的近似,並根據實際情況進行優化。
以 React Native 的 iOS 端舉例:JavaScript 執行在 JSCore 中,實際上可以與上面的方式一樣,利用注入 API 來實現 JavaScript 呼叫 Native 功能。不過 React Native 並沒有設計成 JavaScript 直接呼叫 Object-C,而是 為了與 Native 開發裡事件響應機制一致,設計成 需要在 Object-C 去調 JavaScript 時才通過返回值觸發呼叫。原理基本一樣,只是實現方式不同。
當然不僅僅 iOS 和 Android,其他手機作業系統也用相應的 API,例如 WMP(Win 10)下可以用 window.external.notify 和 WebView.InvokeScript/InvokeScriptAsync 進行雙向通訊。其他系統也類似。
4.2 JSBridge 介面實現
從上面的剖析中,可以得知,JSBridge 的介面主要功能有兩個:呼叫 Native(給 Native 發訊息) 和 接被 Native 呼叫(接收 Native 訊息)。因此,JSBridge 可以設計如下:
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 () {
var id = 0,
callbacks = {};
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 原封不動傳回
// 具體邏輯
// bridgeName 和 callbackId 不會同時存在
if (callbackId) {
if (callbacks[callbackId]) { // 找到相應控制程式碼
callbacks[callbackId](msg.data); // 執行呼叫
}
} elseif (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); // 執行呼叫
}
} elseif (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 一起傳送給前端即可。
5 JSBridge 如何引用
對於 JSBridge 的引用,常用有兩種方式,各有利弊。
5.1 由 Native 端進行注入
注入方式和 Native 呼叫 JavaScript 類似,直接執行橋的全部程式碼。
它的優點在於:橋的版本很容易與 Native 保持一致,Native 端不用對不同版本的 JSBridge 進行相容;與此同時,它的缺點是:注入時機不確定,需要實現注入失敗後重試的機制,保證注入的成功率,同時 JavaScript 端在呼叫介面時,需要優先判斷 JSBridge 是否已經注入成功。
5.2 由 JavaScript 端引用
直接與 JavaScript 一起執行。
與由 Native 端注入正好相反,它的優點在於:JavaScript 端可以確定 JSBridge 的存在,直接呼叫即可;缺點是:如果橋的實現方式有更改,JSBridge 需要相容多版本的 Native Bridge 或者 Native Bridge 相容多版本的 JSBridge。
6 總結
這篇文章主要剖析的 JSBridge 的實現及應用,包括 JavaScript 與 Native 間的通訊原理,JSBridge 的 JavaScript 端實現 以及 引用方式,並給出了一些示例程式碼,希望對讀者有一定的幫助。