JSBridge總結(jié)

由于Webview內(nèi)嵌H5的性能/功能各種受限,于是有了各種的混合開(kāi)發(fā)解決方案,例如Hybrid、RN、WEEX、Flutter、小程序、快應(yīng)用等等。

React Native 至今沒(méi)有推出1.0版本,由于各種可能的坑,一些hold不住的團(tuán)隊(duì)可能會(huì)放棄。
Flutter 是否可替代RN,真正實(shí)現(xiàn)兩端統(tǒng)一,拭目以待,他從頭到尾重寫(xiě)一套跨平臺(tái)的UI框架,包括UI控件、渲染邏輯甚至開(kāi)發(fā)語(yǔ)言。我本人之后會(huì)關(guān)注學(xué)習(xí)一下。
小程序 不用說(shuō)太多了,大家都很熟悉了;微信、支付寶、百度都在用。除了第一次需要花點(diǎn)時(shí)間下載,體驗(yàn)上可以說(shuō)是很不錯(cuò)了,但是封閉性是他很大的一個(gè)缺點(diǎn)。
快應(yīng)用 目標(biāo)是很好的,統(tǒng)一API,但是還是要看各廠家的執(zhí)行力度。

現(xiàn)在來(lái)總結(jié)一下我們團(tuán)隊(duì)目前使用的Hybrid方案。算是回顧一下,鞏固基礎(chǔ),好記性不如爛筆頭。

一、Hybrid簡(jiǎn)介

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

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

但是優(yōu)點(diǎn)也很顯著:隨時(shí)發(fā)版,不受應(yīng)用市場(chǎng)審核限制(當(dāng)然這個(gè)前提是Hybrid對(duì)應(yīng)Native的功能都已準(zhǔn)備就緒);擁有幾乎和Native一樣的能力,eg:拍照、存儲(chǔ)、加日歷等等...

基本原理

Hybrid利用JSBridge進(jìn)行通信的基本原理網(wǎng)上一搜一大把,簡(jiǎn)單記錄一下。

Native => JS
兩端都有現(xiàn)成方法。誰(shuí)讓都在別人的地盤(pán)下面玩呢,Native當(dāng)然有辦法來(lái)執(zhí)行JS方法。
iOS

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

Android

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

JS => Native
對(duì)于Webview中發(fā)起的網(wǎng)絡(luò)請(qǐng)求,Native都有能力去捕獲/截取/干預(yù)。所以JSBridge的核心就是設(shè)計(jì)一套u(yù)rl方案,讓Native可以識(shí)別,從而做出響應(yīng),執(zhí)行對(duì)應(yīng)的操作就完事。
例如,正常的網(wǎng)絡(luò)請(qǐng)求可能是: https://img.alicdn.com/tps/TB17ghmIFXXXXXAXFXXXXXXXXXX.png
我們可以自定義協(xié)議,改成jsbridge://methodName?param1=value1&param2=value2。
Native攔截jsbridge開(kāi)頭的網(wǎng)絡(luò)請(qǐng)求,做出對(duì)應(yīng)的動(dòng)作。
最常見(jiàn)的做法就是創(chuàng)建一個(gè)隱藏的iframe來(lái)實(shí)現(xiàn)通信。

二、現(xiàn)成的解決方案

iOS WebViewJavascriptBridge
Android JsBridge

基本原理都相同,項(xiàng)目的設(shè)計(jì)就決定了一個(gè)它的可擴(kuò)展性&可維護(hù)性。良好的可擴(kuò)展性&可維護(hù)性對(duì)于JSBridge尤為重要,他是后面一切業(yè)務(wù)的基石。

基礎(chǔ)庫(kù)簡(jiǎn)析

(下面都以Android為例)

1、 初始化

類似寫(xiě)普通H5頁(yè)面需要監(jiān)聽(tīng)DOMContentLoaded或者onLoad來(lái)決定開(kāi)始執(zhí)行腳本一樣,JSBridge需要一個(gè)契機(jī)去告訴JS,我準(zhǔn)備好了,你可以來(lái)調(diào)用我的方法了。

[前端] 執(zhí)行監(jiān)聽(tīng) && 檢測(cè)

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

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

    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調(diào)Native方法

先上代碼,下面是埋在端內(nèi)的,JSBridge.callHandler,用來(lái)實(shí)現(xiàn)JS調(diào)用Native。

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

    //sendMessage add message, 觸發(fā)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調(diào)Native方法的核心。
handlerName是前端與Native協(xié)商好的方法名稱
data 參數(shù)
responseCallback 回調(diào)
回調(diào)函數(shù)綁在了一個(gè)內(nèi)部對(duì)象中var responseCallbacks = {},發(fā)送給Native的消息message中只包含了這個(gè)回調(diào)函數(shù)對(duì)應(yīng)的id,端上處理完成之后觸發(fā)&銷毀。

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

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

上面提到的,JS只是通知了端上有新消息,Native調(diào)用獲取時(shí)機(jī)暫時(shí)不考慮,就假設(shè)他收到一條就處理一次,極端高頻情況下,兩三條處理一次。Native通過(guò)_fetchQueue統(tǒng)一處理存儲(chǔ)在sendMessageQueue中的數(shù)據(jù):

    // 提供給native調(diào)用,該函數(shù)作用:獲取sendMessageQueue返回給native,由于android不能直接獲取返回的內(nèi)容,所以使用url shouldOverrideUrlLoading 的方式返回內(nèi)容
    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主動(dòng)調(diào)用Native的流程,關(guān)于回調(diào)方法,下面統(tǒng)一說(shuō)。

3、Native調(diào)JS方法

雖說(shuō)Native可以隨意執(zhí)行JS,但是總是需要知道哪些JS方法是可執(zhí)行的吧。registerHandler就是用來(lái)執(zhí)行注冊(cè)。
registerHandler在Native端定義(是JSBridge對(duì)象的一個(gè)方法),由前端來(lái)注冊(cè)。

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

Native主動(dòng)調(diào)用。
Native主動(dòng)調(diào)用分兩種情況,1是Native主動(dòng)觸發(fā)前端事件,例如通知前端頁(yè)面可視狀態(tài)變化。2是前端調(diào)用Native的回調(diào)。JSBridge是天生異步的,所以回調(diào)和主動(dòng)調(diào)用歸結(jié)到一類里面了。
如果是前端主動(dòng)調(diào)用的方法,有responseId,即有回調(diào),直接調(diào)用執(zhí)行即可。
否則就去注冊(cè)的messageHandlers中尋找方法,調(diào)用。

    //提供給native使用,
    function _dispatchMessageFromNative(messageJSON) {
        setTimeout(function() {
            var message = JSON.parse(messageJSON);
            var responseCallback;
            //java call finished, now need to call js callback function
            // 前端主動(dòng)調(diào)用的Callback
            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {
                // Native主動(dòng)調(diào)用
                //直接發(fā)送
                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);
                    }
                }
            }
        });
    }

代碼分析基本就到這里,盜一張圖(地址放在最后了),把流程都畫(huà)了出來(lái),個(gè)人感覺(jué)沒(méi)啥問(wèn)題

三、業(yè)務(wù)封裝

直接使用前面的庫(kù)可以完成功能,但是不夠優(yōu)雅,代碼不經(jīng)過(guò)良好的設(shè)計(jì)可能會(huì)變得牽一發(fā)動(dòng)全身,可維護(hù)性差。下面說(shuō)說(shuō)我們的設(shè)計(jì),可能不是最好的,但是是很符合我們業(yè)務(wù)場(chǎng)景的。

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

  /**
   * 獲取jsbridge實(shí)例,注入到sncClass上的bridge屬性 `this.bridge`
   */
  connect() {
     // 事件廣播,通知開(kāi)始建立連接,統(tǒng)計(jì)使用
     // 建立JSBridge
     // 建立JSBridge.then  1.注冊(cè)Native主動(dòng)調(diào)用的事件,對(duì)應(yīng)上面的bridge.registerHandler;2.廣播 建立完成,統(tǒng)計(jì)使用
  }

  // ... 其他的一些方法
 // eg: 分平臺(tái)初始化JSBridge,處理差異性
 // eg: bridge.registerHandler 回調(diào)的封裝一層的統(tǒng)一處理函數(shù)

}

關(guān)于注冊(cè)Native主動(dòng)調(diào)用的事件(和下面會(huì)提到的JS主動(dòng)調(diào)用事件),實(shí)現(xiàn)插件化,并同一封裝。好處是可以明確代碼執(zhí)行步驟、方便業(yè)務(wù)同學(xué)調(diào)試(這不是我的鍋,我已經(jīng)執(zhí)行調(diào)用了...)、方便性能統(tǒng)計(jì)。

  1. 業(yè)務(wù)類
class SncClass extends ConnectClass {
  constructor(option){
    // 監(jiān)聽(tīng)connect,監(jiān)聽(tīng)首屏數(shù)據(jù)
    // 建立連接 this.connect
    // 掛載必備API
  }

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

  /**
   * 掛載 api
   * @param  {Object} apis api 對(duì)象集合
   */
  mountApi(apis) {
    // 1.  錯(cuò)誤處理
    // 2. 檢測(cè)是否已經(jīng)jsb建立連接 已連接則 直接執(zhí)行真正掛載函數(shù) return
    // 3. bridge 未初始化時(shí),定義方法預(yù)聲明。執(zhí)行的方法將會(huì)被儲(chǔ)存在緩存隊(duì)列里在 bridge 初始化后調(diào)用
    // 4. 監(jiān)聽(tīng)連接事件,執(zhí)行真正掛載 loadMethods
  }
}

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

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

export default appSNC.init(apis);

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

參考鏈接

JSBridge深度剖析

WebViewJavascriptBridge

JsBridge

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容