JSBridge的原理

關於 JSBridge,絕大多數同窗最先遇到的是微信的 WeiXinJSBridge(如今被封裝成 JSSDK),各類 Web 頁面能夠經過 Bridge 調用微信提供的一些原生功能,爲用戶提供相關的功能。其實,JSBridge 很早就出如今軟件開發中,在一些桌面軟件中很早就運用了這樣的形式,多用在通知、產品詳情、廣告等模塊中,而後這些模塊中,使用的是 Web UI,而相關按鈕點擊後,調用的是 Native 功能。如今移動端盛行,無論是 Hybrid 應用,仍是 React-Native 都離不開 JSBridge,固然也包括在國內舉足輕重的微信小程序。那麼,JSBridge 究竟是什麼?它的出現是爲了什麼?它到底是怎麼實現的?在這篇文章中,會在移動混合開發的範疇內,將給你們帶來 JSBridge 的深刻剖析。 javascript

1 前言

有些童鞋聽到 JSBridge 這個名詞,就是以爲很是高上大,有了它 Web 和 Native 能夠進行交互,就像『進化藥水』,讓 Web 搖身一變,成爲移動戰場的『上將一名』。其實並不是如此,JSBridge 其實真是一個很簡單的東西,更多的是一種形式、一種思想。前端

2 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 進行了隔離。所以小程序的技術方案介於上面描述的兩種方式之間。服務器

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

相關文章
相關標籤/搜索