JSBridge總結

由于Webview內嵌H5的性能/功能各種受限,于是有了各種的混合開發解決方案,例如Hybrid、RN、WEEX、Flutter、小程序、快應用等等。

React Native 至今沒有推出1.0版本,由于各種可能的坑,一些hold不住的團隊可能會放棄。
Flutter 是否可替代RN,真正實現兩端統一,拭目以待,他從頭到尾重寫一套跨平臺的UI框架,包括UI控件、渲染邏輯甚至開發語言。我本人之后會關注學習一下。
小程序 不用說太多了,大家都很熟悉了;微信、支付寶、百度都在用。除了第一次需要花點時間下載,體驗上可以說是很不錯了,但是封閉性是他很大的一個缺點。
快應用 目標是很好的,統一API,但是還是要看各廠家的執行力度。

現在來總結一下我們團隊目前使用的Hybrid方案。算是回顧一下,鞏固基礎,好記性不如爛筆頭。

一、Hybrid簡介

Hybrid可以說是上面提到的幾種里最古老,最成熟的解決方案了。

缺點是明顯的:H5有的缺點他幾乎都有,比如性能差、JS執行效率低等等。

但是優點也很顯著:隨時發版,不受應用市場審核限制(當然這個前提是Hybrid對應Native的功能都已準備就緒);擁有幾乎和Native一樣的能力,eg:拍照、存儲、加日歷等等...

基本原理

Hybrid利用JSBridge進行通信的基本原理網上一搜一大把,簡單記錄一下。

Native => JS
兩端都有現成方法。誰讓都在別人的地盤下面玩呢,Native當然有辦法來執行JS方法。
iOS

// Swift
webview.stringByEvaluatingJavaScriptFromString("Math.random()")
// OC
[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"];

Android

mWebView.evaluateJavascript("javascript: 方法名('參數,需要轉為字符串')", new ValueCallback() {
        @Override
        public void onReceiveValue(String value) {
            //這里的value即為對應JS方法的返回值
        }
});

JS => Native
對于Webview中發起的網絡請求,Native都有能力去捕獲/截取/干預。所以JSBridge的核心就是設計一套url方案,讓Native可以識別,從而做出響應,執行對應的操作就完事。
例如,正常的網絡請求可能是: https://img.alicdn.com/tps/TB17ghmIFXXXXXAXFXXXXXXXXXX.png
我們可以自定義協議,改成jsbridge://methodName?param1=value1&param2=value2
Native攔截jsbridge開頭的網絡請求,做出對應的動作。
最常見的做法就是創建一個隱藏的iframe來實現通信。

二、現成的解決方案

iOS WebViewJavascriptBridge
Android JsBridge

基本原理都相同,項目的設計就決定了一個它的可擴展性&可維護性。良好的可擴展性&可維護性對于JSBridge尤為重要,他是后面一切業務的基石。

基礎庫簡析

(下面都以Android為例)

1、 初始化

類似寫普通H5頁面需要監聽DOMContentLoaded或者onLoad來決定開始執行腳本一樣,JSBridge需要一個契機去告訴JS,我準備好了,你可以來調用我的方法了。

[前端] 執行監聽 && 檢測

    if (window.WebViewJavascriptBridge) {
        //do your work here
    } else {
        document.addEventListener(
            'WebViewJavascriptBridgeReady'
            , function() {
                //do your work here
            },
            false
        );
    }

[Native (埋在端里的JS)] dispatchEvent觸發

    var WebViewJavascriptBridge = window.WebViewJavascriptBridge = {
        init: init,
        send: send,
        registerHandler: registerHandler,
        callHandler: callHandler,
        _fetchQueue: _fetchQueue,
        _handleMessageFromNative: _handleMessageFromNative
    };
    
    var readyEvent = doc.createEvent('Events');
    readyEvent.initEvent('WebViewJavascriptBridgeReady');
    readyEvent.bridge = WebViewJavascriptBridge;
    doc.dispatchEvent(readyEvent);
2、JS調Native方法

先上代碼,下面是埋在端內的,JSBridge.callHandler,用來實現JS調用Native。

    // 調用線程
    function callHandler(handlerName, data, responseCallback) {
        _doSend({
            handlerName: handlerName,
            data: data
        }, responseCallback);
    }

    //sendMessage add message, 觸發native處理 sendMessage
    function _doSend(message, responseCallback) {
        if (responseCallback) {
            var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
            responseCallbacks[callbackId] = responseCallback;
            message.callbackId = callbackId;
        }

        sendMessageQueue.push(message);
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }

jsbridge.callHandler是JS調Native方法的核心。
handlerName是前端與Native協商好的方法名稱
data 參數
responseCallback 回調
回調函數綁在了一個內部對象中var responseCallbacks = {},發送給Native的消息message中只包含了這個回調函數對應的id,端上處理完成之后觸發&銷毀。

這個方法并不直接把消息全部推送走,而是存在一個隊列中sendMessageQueue。同時通知Native,有新數據(message)需要處理。即上面代碼的最后一行,他利用iframe的src通知端上的信息如下:

    var CUSTOM_PROTOCOL_SCHEME = 'sn'
    var QUEUE_HAS_MESSAGE = '__sn__queue_message__'

上面提到的,JS只是通知了端上有新消息,Native調用獲取時機暫時不考慮,就假設他收到一條就處理一次,極端高頻情況下,兩三條處理一次。Native通過_fetchQueue統一處理存儲在sendMessageQueue中的數據:

    // 提供給native調用,該函數作用:獲取sendMessageQueue返回給native,由于android不能直接獲取返回的內容,所以使用url shouldOverrideUrlLoading 的方式返回內容
    function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        //android can't read directly the return data, so we can reload iframe src to communicate with java
        if (messageQueueString !== '[]') {
            bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
        }
    }

這些基本就是JS主動調用Native的流程,關于回調方法,下面統一說。

3、Native調JS方法

雖說Native可以隨意執行JS,但是總是需要知道哪些JS方法是可執行的吧。registerHandler就是用來執行注冊。
registerHandler在Native端定義(是JSBridge對象的一個方法),由前端來注冊。

    // 注冊線程 往數組里面添加值
    function registerHandler(handlerName, handler) {
        messageHandlers[handlerName] = handler;
    }

Native主動調用。
Native主動調用分兩種情況,1是Native主動觸發前端事件,例如通知前端頁面可視狀態變化。2是前端調用Native的回調。JSBridge是天生異步的,所以回調和主動調用歸結到一類里面了。
如果是前端主動調用的方法,有responseId,即有回調,直接調用執行即可。
否則就去注冊的messageHandlers中尋找方法,調用。

    //提供給native使用,
    function _dispatchMessageFromNative(messageJSON) {
        setTimeout(function() {
            var message = JSON.parse(messageJSON);
            var responseCallback;
            //java call finished, now need to call js callback function
            // 前端主動調用的Callback
            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {
                // Native主動調用
                //直接發送
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        _doSend({
                            responseId: callbackResponseId,
                            responseData: responseData
                        });
                    };
                }

                var handler = WebViewJavascriptBridge._messageHandler;
                if (message.handlerName) {
                    handler = messageHandlers[message.handlerName];
                }
                //查找指定handler
                try {
                    handler(message.data, responseCallback);
                } catch (exception) {
                    if (typeof console != 'undefined') {
                        console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
                    }
                }
            }
        });
    }

代碼分析基本就到這里,盜一張圖(地址放在最后了),把流程都畫了出來,個人感覺沒啥問題

三、業務封裝

直接使用前面的庫可以完成功能,但是不夠優雅,代碼不經過良好的設計可能會變得牽一發動全身,可維護性差。下面說說我們的設計,可能不是最好的,但是是很符合我們業務場景的。

  1. 事件基礎類 EventClass
    處理事件廣播、訂閱。
  2. 連接基礎類 ConnectClass
/**
 * 創建和獲取 jsbridge 基礎類
 * @class ConnectClass
 * @extends EventsClass
 */
class ConnectClass extends EventsClass {

  /**
   * 獲取jsbridge實例,注入到sncClass上的bridge屬性 `this.bridge`
   */
  connect() {
     // 事件廣播,通知開始建立連接,統計使用
     // 建立JSBridge
     // 建立JSBridge.then  1.注冊Native主動調用的事件,對應上面的bridge.registerHandler;2.廣播 建立完成,統計使用
  }

  // ... 其他的一些方法
 // eg: 分平臺初始化JSBridge,處理差異性
 // eg: bridge.registerHandler 回調的封裝一層的統一處理函數

}

關于注冊Native主動調用的事件(和下面會提到的JS主動調用事件),實現插件化,并同一封裝。好處是可以明確代碼執行步驟、方便業務同學調試(這不是我的鍋,我已經執行調用了...)、方便性能統計。

  1. 業務類
class SncClass extends ConnectClass {
  constructor(option){
    // 監聽connect,監聽首屏數據
    // 建立連接 this.connect
    // 掛載必備API
  }

  // 初始化,根據參數決定掛載哪些api
  init(apis){
    this.mountApi(apis);
  }

  /**
   * 掛載 api
   * @param  {Object} apis api 對象集合
   */
  mountApi(apis) {
    // 1.  錯誤處理
    // 2. 檢測是否已經jsb建立連接 已連接則 直接執行真正掛載函數 return
    // 3. bridge 未初始化時,定義方法預聲明。執行的方法將會被儲存在緩存隊列里在 bridge 初始化后調用
    // 4. 監聽連接事件,執行真正掛載 loadMethods
  }
}

 /**
   * 加載 API 到實例屬性,標志著 api 的真正掛載
   */
  loadMethods(apis) {
      // 1.  防止重復掛載 api,
      // 2. 給插件初始化方法注入ctx,讓插件得以調用庫內真正的初始化函數,即封裝一層的上面提到的 callHandler
  }

// ... 其他實例方法,比如 extend,得以在業務中和Native互相約定新的非通用JSB,方便擴展
  1. 初始化
    導出單例appSNC,擁有的方法都在appApis中定義,如果有新的業務需求直接擴展此文件夾中內容即可。
import * as apis from '../appApis'; // 方法集合
import SNC from './sdk';  // 上面的 SncClass
const option = {} ; // 一些配置
const appSNC = new SNC(option);

export default appSNC.init(apis);

以上就是我們正在使用的方案,總結一下,不斷積累。

參考鏈接

JSBridge深度剖析

WebViewJavascriptBridge

JsBridge

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容