JSBridge總結

由于Webview內嵌H5的性能/功能各種受限拖叙,于是有了各種的混合開發(fā)解決方案,例如Hybrid赂乐、RN薯鳍、WEEX、Flutter挨措、小程序挖滤、快應用等等崩溪。

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

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

一盆顾、Hybrid簡介

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

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

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

基本原理

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

Native => JS
兩端都有現成方法贿肩。誰讓都在別人的地盤下面玩呢,Native當然有辦法來執(zhí)行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中發(fā)起的網絡請求汰规,Native都有能力去捕獲/截取/干預。所以JSBridge的核心就是設計一套url方案物邑,讓Native可以識別溜哮,從而做出響應,執(zhí)行對應的操作就完事色解。
例如茂嗓,正常的網絡請求可能是: https://img.alicdn.com/tps/TB17ghmIFXXXXXAXFXXXXXXXXXX.png
我們可以自定義協議,改成jsbridge://methodName?param1=value1&param2=value2科阎。
Native攔截jsbridge開頭的網絡請求述吸,做出對應的動作。
最常見的做法就是創(chuàng)建一個隱藏的iframe來實現通信锣笨。

二蝌矛、現成的解決方案

iOS WebViewJavascriptBridge
Android JsBridge

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

基礎庫簡析

(下面都以Android為例)

1椭岩、 初始化

類似寫普通H5頁面需要監(jiān)聽DOMContentLoaded或者onLoad來決定開始執(zhí)行腳本一樣茅逮,JSBridge需要一個契機去告訴JS,我準備好了簿煌,你可以來調用我的方法了氮唯。

[前端] 執(zhí)行監(jiān)聽 && 檢測

    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調Native方法

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

    // 調用線程
    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調Native方法的核心瞒渠。
handlerName是前端與Native協商好的方法名稱
data 參數
responseCallback 回調
回調函數綁在了一個內部對象中var responseCallbacks = {},發(fā)送給Native的消息message中只包含了這個回調函數對應的id技扼,端上處理完成之后觸發(fā)&銷毀伍玖。

這個方法并不直接把消息全部推送走,而是存在一個隊列中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可以隨意執(zhí)行JS糊啡,但是總是需要知道哪些JS方法是可執(zhí)行的吧。registerHandler就是用來執(zhí)行注冊吁津。
registerHandler在Native端定義(是JSBridge對象的一個方法)悔橄,由前端來注冊。

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

Native主動調用腺毫。
Native主動調用分兩種情況,1是Native主動觸發(fā)前端事件挣柬,例如通知前端頁面可視狀態(tài)變化潮酒。2是前端調用Native的回調。JSBridge是天生異步的邪蛔,所以回調和主動調用歸結到一類里面了急黎。
如果是前端主動調用的方法,有responseId侧到,即有回調勃教,直接調用執(zhí)行即可。
否則就去注冊的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主動調用
                //直接發(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);
                    }
                }
            }
        });
    }

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

三、業(yè)務封裝

直接使用前面的庫可以完成功能矢腻,但是不夠優(yōu)雅门驾,代碼不經過良好的設計可能會變得牽一發(fā)動全身,可維護性差多柑。下面說說我們的設計奶是,可能不是最好的,但是是很符合我們業(yè)務場景的。

  1. 事件基礎類 EventClass
    處理事件廣播聂沙、訂閱秆麸。
  2. 連接基礎類 ConnectClass
/**
 * 創(chuàng)建和獲取 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主動調用事件),實現插件化秧秉,并同一封裝呛踊。好處是可以明確代碼執(zhí)行步驟、方便業(yè)務同學調試(這不是我的鍋聘鳞,我已經執(zhí)行調用了...)脱惰、方便性能統計采盒。

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

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

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

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

// ... 其他實例方法玉转,比如 extend,得以在業(yè)務中和Native互相約定新的非通用JSB殴蹄,方便擴展
  1. 初始化
    導出單例appSNC究抓,擁有的方法都在appApis中定義,如果有新的業(yè)務需求直接擴展此文件夾中內容即可袭灯。
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

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末橘茉,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌畅卓,老刑警劉巖擅腰,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異翁潘,居然都是意外死亡趁冈,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門拜马,熙熙樓的掌柜王于貴愁眉苦臉地迎上來渗勘,“玉大人,你說我怎么就攤上這事一膨⊙叫希” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵豹绪,是天一觀的道長价淌。 經常有香客問我,道長瞒津,這世上最難降的妖魔是什么蝉衣? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮巷蚪,結果婚禮上病毡,老公的妹妹穿的比我還像新娘。我一直安慰自己屁柏,他們只是感情好啦膜,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著淌喻,像睡著了一般僧家。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上裸删,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天八拱,我揣著相機與錄音,去河邊找鬼涯塔。 笑死肌稻,一個胖子當著我的面吹牛,可吹牛的內容都是我干的匕荸。 我是一名探鬼主播爹谭,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼榛搔!你這毒婦竟也來了旦棉?” 一聲冷哼從身側響起齿风,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎绑洛,沒想到半個月后救斑,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡真屯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年脸候,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绑蔫。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡运沦,死狀恐怖,靈堂內的尸體忽然破棺而出配深,到底是詐尸還是另有隱情携添,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布篓叶,位于F島的核電站烈掠,受9級特大地震影響,放射性物質發(fā)生泄漏缸托。R本人自食惡果不足惜左敌,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望俐镐。 院中可真熱鬧矫限,春花似錦、人聲如沸佩抹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽棍苹。三九已至无宿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間廊勃,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工经窖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留坡垫,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓画侣,卻偏偏與公主長得像冰悠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子配乱,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內容