JSBridge的原理

舞動乾坤發表於2018-03-29

關於 JSBridge,絕大多數同學最早遇到的是微信的 WeiXinJSBridge(現在被封裝成 JSSDK),各種 Web 頁面可以通過 Bridge 呼叫微信提供的一些原生功能,為使用者提供相關的功能。其實,JSBridge 很早就出現在軟體開發中,在一些桌面軟體中很早就運用了這樣的形式,多用在通知、產品詳情、廣告等模組中,然後這些模組中,使用的是 Web UI,而相關按鈕點選後,呼叫的是 Native 功能。現在移動端盛行,不管是 Hybrid 應用,還是 React-Native 都離不開 JSBridge,當然也包括在國內舉足輕重的微信小程式。那麼,JSBridge 到底是什麼?它的出現是為了什麼?它究竟是怎麼實現的?在這篇文章中,會在移動混合開發的範疇內,將給大家帶來 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 間訊息通訊的通道,而且是 雙向通訊的通道

JSBridge的原理
所謂 雙向通訊的通道:

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 端實現 以及 引用方式,並給出了一些示例程式碼,希望對讀者有一定的幫助。

相關文章