
1 前言
有了它,Web 和 Native 可以進行交互,就像『進化葯水』,讓 Web 搖身一變,成為移動戰場的『上將一名』。
實際上,JSBridge 其實真是一個很簡單的東西,更多的是一種形式、一種思想。
2 JSBridge 的起源
任何一個移動操作系統中都包含可運行 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 就像其名稱中的『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 的目的。
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 去調用了)
4.1.3 通信原理小總結
通信原理是 JSBridge 實現的核心,實現方式可以各種各樣,但是萬變不離其宗。這里,筆者推薦的實現方式如下:
JavaScript 調用 Native 推薦使用 注入 API 的方式。
Native 調用 JavaScript 則直接執行拼接好的 JavaScript 代碼即可。
對於其他方式,諸如 React Native、微信小程序 的通信方式都與上描述的近似,並根據實際情況進行優化。
以 React Native 的 iOS 端舉例:
JavaScript 運行在 JSCore 中,實際上可以與上面的方式一樣,利用注入 API 來實現 JavaScript 調用 Native 功能。不過 React Native 並沒有設計成 JavaScript 直接調用 Object-C,而是 為了與 Native 開發里事件響應機制一致,設計成 需要在 Object-C 去調 JavaScript 時才通過返回值觸發調用。原理基本一樣,只是實現方式不同。
4.2 JSBridge 接口實現
從上面的剖析中,可以得知,JSBridge 的接口主要功能有兩個:調用 Native(給 Native 發消息) 和 接被 Native 調用(接收 Native 消息)。因此,JSBridge 可以設計如下:
window.JSBridge = { // 調用 Native invoke: function(msg) { // 判斷環境,獲取不同的 nativeBridge nativeBridge.postMessage(msg); }, receiveMessage: function(msg) { // 處理 msg } };
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 中,
也會以此參數值作為句柄,調用相應的回調函數。
(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) { } } }; })();
(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 端實現 以及 引用方式,並給出了一些示例代碼,希望對讀者有一定的幫助。