關於 JSBridge,絕大多數同窗最先遇到的是微信的 WeiXinJSBridge(如今被封裝成 JSSDK),各類 Web 頁面能夠經過 Bridge 調用微信提供的一些原生功能,爲用戶提供相關的功能。其實,JSBridge 很早就出如今軟件開發中,在一些桌面軟件中很早就運用了這樣的形式,多用在通知、產品詳情、廣告等模塊中,而後這些模塊中,使用的是 Web UI,而相關按鈕點擊後,調用的是 Native 功能。如今移動端盛行,無論是 Hybrid 應用,仍是 React-Native 都離不開 JSBridge,固然也包括在國內舉足輕重的微信小程序。那麼,JSBridge 究竟是什麼?它的出現是爲了什麼?它到底是怎麼實現的?在這篇文章中,會在移動混合開發的範疇內,將給你們帶來 JSBridge 的深刻剖析。 javascript
有些童鞋聽到 JSBridge 這個名詞,就是以爲很是高上大,有了它 Web 和 Native 能夠進行交互,就像『進化藥水』,讓 Web 搖身一變,成爲移動戰場的『上將一名』。其實並不是如此,JSBridge 其實真是一個很簡單的東西,更多的是一種形式、一種思想。前端
爲何是 JSBridge ?而不是 PythonBridge 或是 RubyBridge ?java
固然不是由於 JavaScript 語言高人一等(雖然斯坦福大學已經把算法導論的語言從 Java 改爲 JavaScript,小得意一下,嘻嘻),主要的緣由仍是由於 JavaScript 主要載體 Web 是當前世界上的 最易編寫 、 最易維護 、最易部署 的 UI 構建方式。工程師能夠用很簡單的 HTML 標籤和 CSS 樣式快速的構建出一個頁面,而且在服務端部署後,用戶不須要主動更新,就能看到最新的 UI 展示。web
所以,開發維護成本 和 更新成本 較低的 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 進行了隔離。所以小程序的技術方案介於上面描述的兩種方式之間。服務器
JSBridge 簡單來說,主要是 給 JavaScript 提供調用 Native 功能的接口,讓混合開發中的『前端部分』能夠方便地使用地址位置、攝像頭甚至支付等 Native 功能。
既然是『簡單來說』,那麼 JSBridge 的用途確定不僅『調用 Native 功能』這麼簡單寬泛。實際上,JSBridge 就像其名稱中的『Bridge』的意義同樣,是 Native 和非 Native 之間的橋樑,它的核心是 構建 Native 和非 Native 間消息通訊的通道,並且是 雙向通訊的通道。
所謂 雙向通訊的通道:JS 向 Native 發送消息 : 調用相關功能、通知 Native 當前 JS 的相關狀態等。
Native 向 JS 發送消息 : 回溯調用結果、消息推送、通知 JS 當前 Native 的狀態等。
這裏有些同窗有疑問了:消息都是單向的,那麼調用 Native 功能時 Callback 怎麼實現的? 對於這個問題,在下一節裏會給出解釋。
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 的實現原理。
Hybrid 方案是基於 WebView 的,JavaScript 執行在 WebView 的 Webkit 引擎中。所以,Hybrid 方案中 JSBridge 的通訊原理會具備一些 Web 特性。
JavaScript 調用 Native 的方式,主要有兩種:注入 API 和 攔截 URL SCHEME。
注入 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 的方式來實現,由於篇幅有限,這裏就不展開了。)
先解釋一下 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,很容易丟失一些調用。
相比於 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 執行後的結果。
通訊原理是 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 進行雙向通訊。其餘系統也相似。
從上面的剖析中,能夠得知,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 一塊兒發送給前端便可。
對於 JSBridge 的引用,經常使用有兩種方式,各有利弊。
注入方式和 Native 調用 JavaScript 相似,直接執行橋的所有代碼。
它的優勢在於:橋的版本很容易與 Native 保持一致,Native 端不用對不一樣版本的 JSBridge 進行兼容;與此同時,它的缺點是:注入時機不肯定,須要實現注入失敗後重試的機制,保證注入的成功率,同時 JavaScript 端在調用接口時,須要優先判斷 JSBridge 是否已經注入成功。
直接與 JavaScript 一塊兒執行。
與由 Native 端注入正好相反,它的優勢在於:JavaScript 端能夠肯定 JSBridge 的存在,直接調用便可;缺點是:若是橋的實現方式有更改,JSBridge 須要兼容多版本的 Native Bridge 或者 Native Bridge 兼容多版本的 JSBridge。
這篇文章主要剖析的 JSBridge 的實現及應用,包括 JavaScript 與 Native 間的通訊原理,JSBridge 的 JavaScript 端實現 以及 引用方式,並給出了一些示例代碼,但願對讀者有必定的幫助。