混合App交互相關,jsBridge的原理


關於 JSBridge,絕大多數同學最早遇到的是微信的 WeiXinJSBridge(現在被封裝成 JSSDK),各種 Web 頁面可以通過 Bridge 調用微信提供的一些原生功能,為用戶提供相關的功能。
JSBridge 很早就出現在軟件開發中,在一些桌面軟件中很早就運用了這樣的形式,多用在通知、產品詳情、廣告等模塊中。
這些模塊中,使用的是 Web UI,而相關按鈕點擊后,調用的是 Native 功能。
現在移動端盛行,不管是 Hybrid 應用,還是 React-Native 都離不開 JSBridge,當然也包括在國內舉足輕重的微信小程序。
 

 

 

 

 

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
    }
};
在上面的文章中,提到過 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 端實現 以及 引用方式,並給出了一些示例代碼,希望對讀者有一定的幫助。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM