【quickhybrid】JSBridge的實現

来源:http://www.cnblogs.com/dailc/archive/2017/12/24/8098597.html
-Advertisement-
Play Games

前言 本文介紹 框架的核心 的實現 由於在最新版本中,已經沒有考慮 等低版本,因此在選用方案時沒有採用 方式,而是直接基於 實現 交互原理 具體H5和Native的交互原理可以參考前文的 交互原理圖如下: 預計的最終效果 如果一步一步來分析,最後再看效果,可能會很枯燥,甚至還有點化簡為繁的樣子。(感 ...


前言

本文介紹quick hybrid框架的核心JSBridge的實現

由於在最新版本中,已經沒有考慮iOS7等低版本,因此在選用方案時沒有採用url scheme方式,而是直接基於WKWebView實現

交互原理

具體H5和Native的交互原理可以參考前文的H5和Native交互原理

交互原理圖如下:

預計的最終效果

如果一步一步來分析,最後再看效果,可能會很枯燥,甚至還有點化簡為繁的樣子。(感覺直接看代碼應該是最簡單的,奈何每次寫成文章時都得加一大堆的描述)

因此,先來看看最終完成後應該是什麼樣的。

// 調用ui中alert的示例
callHandler({
    // 模塊名,本文中的API劃分了模塊
    module: 'ui',
    // 方法名
    name: 'alert',
    // 需要傳遞給native的請求參數
    data: {
        message: 'hello',
    },
    callback: function(res) {
        /**
         * 調用後的回調,接收原生傳遞的回調數據
         * alert如果成功,可以點擊後再回調
         {
            // 1成功/0失敗
            code: 1,
            message: '描述',
            // 數據
            data: {},
         }
         */
    }
});

架構

從頭開始實現一個JSBridge,很容易兩眼一抹黑,無從下手。

因此我們需要先從大方向上把功能交互確定好,然後再開始構建細節,編碼實現

功能分析與確認

根據核心架構,規劃需要實現的功能:

  • H5橋接對象的設計(JSBridge)

    • 短期回調池,需自動回收

    • 長期回調池,可多次使用

    • 調用Native方法的通道,橋接對象上原生註冊的接收方法

    • 接收Native調用的通道,橋接對象上H5註冊的接收方法

    • H5可以註冊主動給原生調用的方法

  • 原生橋接對象的設計

    • 長期方法池,每一個長期調用都會存儲在回調池中,可以多次使用

    • 短期立即執行,每一個短期調用都是立即執行

    • 調用H5方法的通道,橋接對象上H5註冊的接收方法

    • 接收H5調用的通道,橋接對象上原生註冊的接收方法,底層自動解析,然後執行對應API

    • 回調對象,底層基於調用H5的通道,每次執行完畢後都通過回調對象回調給H5

    • 主動調用H5,不同於回調對象只能被動響應,這個可以主動調用H5中註冊的方法

  • API的設計

    • H5中的API,供前端調用,底層通過調用Native方法的通道,然後將預處理後的參數發送給原生

    • Native中的API,真正的功能實現

接下來就是JSBridge的實現

全局通信對象的確認

最重要的,是先把H5和Native通信時的幾個全局橋接對象確定:

  • JSBridge,H5端的橋接對象,對象中綁定了接收原生調用的方法_handleMessageFromNative,以及內部有對回調函數等進行管理

  • webkit.messageHandlers.WKWebViewJavascriptBridge.postMessage,iOS端的橋接對象,這個方法接收H5的調用

  • prompt,Android端的橋接對象,為了方便,直接重寫了WebChromeClient中的onJsPrompt

// H5端的內部邏輯處理
window.JSBridge = {...}

// 接收原生的調用,有回調以及主動調用兩種
JSBridge._handleMessageFromNative = function() {...}
// H5主動調用原生
if (os.ios) {
    // ios採用
    window.webkit.messageHandlers.WKWebViewJavascriptBridge.postMessage(...);
} else {
    window.top.prompt(...);
}

JSBridge對象的實現

H5就依靠這個對象與Native通信,這裡僅介紹核心的邏輯

JSBridge = {
    // 本地註冊的方法集合,原生只能主動調用本地註冊的方法
    messageHandlers: {},
    // 短期回調函數集合,在原生調用完對應的方法後會自動刪除回收
    responseCallbacks: {},
    // 長期存在的回調集合,可以多次調用
    responseCallbacksLongTerm: {},
    
    _handleMessageFromNative: function(messageJSON) {
        // 內部的處理:**
          如果是回調函數:
          如果是短期回調responseCallbacks中查詢回調id,並執行,執行後自動銷毀
          如果是短期回調responseCallbacksLongTerm中查詢回調id,並執行
          */
          
          /**
          如果是Native的主動調用:
          去本地註冊的方法池messageHandlers中搜索,並執行
          *},
    
    callHandler: function(...) {
        // 底層分別調用Android或iOS的原生接收方法
        
        // 如果是短期回調,會將回調添加到responseCallbacks中
        // 如果是長期回調,會將回調添加到responseCallbacksLongTerm中
        
        // 省略若幹邏輯
        ...
        
        if (os.ios) {
            // ios採用
            window.webkit.messageHandlers.WKWebViewJavascriptBridge.postMessage(...);
        } else {
            window.top.prompt(...);
        }
    },
    
    registerHandler: function(handlerName, handler) {
        // H5在本地註冊可供原生調用的方法
    },
    
    ...
};

Android中橋接對象的實現

Android中的核心就是JSBridge,其餘都是圍繞這個來的,以下是偽代碼,列舉主要的邏輯

public class JSBridge {
    // 緩存所有的API模塊(註冊時添加進去)
    static exposedAPIModlues =  new HashMap<>();
    
    static register(String apiModelName, Class<? extends IBridgeImpl> clazz) {
        // 註冊時會自動尋找所有的框架API模塊,然後添加到緩存exposedAPIModlues,每一個模塊中可以有若幹API
        // 每一個模塊都需要實現IBridgeImpl介面
        ...
    }
    
    static callAPI(...) {
        // 首先會解析參數(H5中傳遞的),解析出調用了哪一個API,傳遞了些什麼,解析結果包括如下
        // port:H5傳遞的回調id,是responseCallbacks或responseCallbacksLongTerm中的key
        // moduleName:調用的API的模塊名,用來檢索exposedAPIModlues中註冊的模塊
        // name:調用的API的方法名,在對於找到的模塊中去查找API
        // 其他:包括傳遞的參數等等
        
        // 然後會根據H5的回調埠號,生成一個回調對象(用來回調通知H5)
        Callback callback = new Callback(port);
        
        // 之後,根據解析的參數尋找API方法
        // java.lang.reflect.Method;
        Method method = searchMethodBy(moduleName, name);
        
        // 沒有找到方法會回調對於錯誤信息
        // 否則執行對於的method,傳遞解析出的參數
        // 並且在method內部執行完畢後主動回調給H5對於信息
        method.invoke(..., callback);
    }
}

callback類偽代碼如下:

public class Callback {
    apply(...) {
        // 先解析拼裝參數,然後將參數組裝成javascript代碼,參數中包含Callback對於的port值(回調id)
        ...
        String js = javascript:JSBridge._handleMessageFromNative(對於的json參數);
        
        callJS(js);
    }
    callHandler(...) {
        // 主動調用H5,封裝的參數中不再是回調id,而是handleName
        ...
        callJS(js);
    }
    callJS(js) {
        // 底層通過loadUrl執行
        ...
        webviewContext.loadUrl(js);
    }
}

IBridgeImpl介面是空的,只是一個抽象定義,以下以某個實現這個介面的API為例

// 為了清晰,以ui.alert為例
public class xxxApi implements IBridgeImpl {
    // 定義一個註冊的模塊別名,方便查找,譬如ui
    static RegisterName = "ui";
    
    // 模塊中的某個API,譬如alert
    public static void alert(..., Callback callback) {
        // 接下來就是在這個API中實現對於的邏輯
        ...
        // 最後,通過觸發callback通知H5即可
        callback.apply(...);
    }
}

最後可以看到,在webview中,重新了WebChromeClientonJsPrompt來接收H5的調用

並且在webview載入時就會調用JSBridgeregister

public class XXXWebChromeClient extends WebChromeClient {
    @Override
    public boolean onJsPrompt(..., JsPromptResult result) {
        // 內部觸發JSBridge.callJava
        result.confirm(JSBridge.callJava(...));
        return true;
    }
}

以上幾個就是Andorid中JSBridge核心實現,其他的如長期回調,短期回調,細節實現等優化不是核心邏輯,就列舉,詳情可以參考最後的源碼

iOS中橋接對象的實現

這裡仍然是OC實現的,主要參考的marcuswestin/WebViewJavascriptBridge實現

核心仍然是WKWebViewJavascriptBridge,其餘一切都是通過它來分發代理

@implementation WKWebViewJavascriptBridge {
    // 內部基於一個WebViewJavascriptBridgeBase基類(基類中定義交互方法)
    WebViewJavascriptBridgeBase *_base;    
}
/**
 * API
 */
- (void)callHandler:(NSString *)handlerName data:(id)data {
    // 主動調用H5的方法
    // 底層調用_base的sendData,發送數據給H5
}

- (void)registerModuleFrameAPI {
    // 註冊模塊API,模塊用到了別名代理
    [self registerHandlersWithClassName:@"UIApi" moduleName:@"ui"];
    
    // 其中registerHandlersWithClassName就是將模塊示例化註冊到全局中的作用,不贅述
}

- (void)excuteMessage:(NSString *)message {
    // 內部執行API的實現,這裡會解析API解析出來的數據,如
    // module.name,port(callbackid)等
    ...
    // 然後底層調用_base的excuteMsg(它內部會根據註冊的API,找到相對應的,然後執行原生功能,最後通過回調通知H5)
}

#pragma mark - WKScriptMessageHandler其實就是一個遵循的協議,它能讓網頁通過JS把消息發送給OC
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    // 監聽到對於API調用時,底層會調用excuteMessage
    if ([message.name isEqualToString:@"WKWebViewJavascriptBridge"]) {
        [self excuteMessage:message.body];
    }
}

然後看看它基類WebViewJavascriptBridgeBase的實現

@implementation WebViewJavascriptBridgeBase

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
    // 底層將接收到的數據組裝成js代碼執行
    ...
    NSString* javascriptCommand = [NSString stringWithFormat:@"JSBridge._handleMessageFromNative('%@');", messageJSON];
    
    [_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
}

- (void)excuteMsg:(NSString *)messageQueueString moduleName:(NSString *)moduleName {
    // 底層根據對於的模塊,API名,找到註冊的handler
    ...
    
    // 然後創建一個回調對象
    WVJBResponseCallback responseCallback = (通過sendData通知H5回調數據);
    
    // 然後執行這個handler
    handler(message[@"data"], responseCallback);
}

接下來是API的定義

定義API模塊之前,需要先瞭解RegisterBaseClass,所有模塊必須實現的基類,定義瞭如何註冊

@implementation RegisterBaseClass
#pragma mark - 註冊api的統一方法
- (void)registerHandlers {
    // 子類重寫改方法實現自定義API註冊
}

#pragma mark - handler存取
- (void)registerHandlerName:(NSString *)handleName
                    handler:(WVJBHandler)handler {
    // 註冊某個模塊下的某個API
}

- (WVJBHandler)handler:(NSString *)handlerName {
    // 通過名稱獲取對應的API
}

要定義一個API模塊,則需繼承RegisterBaseClass然後重寫registerHandlers(為了清晰,以ui.alert為例)

@implementation UIApi
- (void)registerHandlers {
    [self registerHandlerName:@"alert" handler:^(id data, WVJBResponseCallback responseCallback) {
        // 同樣,在接收到數據,並處理後,通過responseCallback通知H5
        ...
        responseCallback(...);
    }
}

webview載入時就會調用WKWebViewJavascriptBridgeregisterModuleFrameAPI,對於模塊名ui與別名UIApi,可以在註冊時看到,它們之間是有一一對應關係的

然後在webview創建時,會進行監聽,userContentController

WKWebViewConfiguration * webConfig = [[WKWebViewConfiguration alloc] init];
WKUserContentController * userContentVC = [[WKUserContentController alloc] init];
webConfig.userContentController = userContentVC;
WKWebView * wk = [[WKWebView alloc] initWithFrame: CGRectZero configuration: webConfig];

self.wv = wk;
...

// 代理
self.bridge = [WKWebViewJavascriptBridge bridgeForWebView: self.wv];
[self.bridge setWebViewDelegate: self];

// 添加供js調用oc的橋梁。這裡的name對應WKScriptMessage中的name,多數情況下我們認為它就是方法名。
[self.wv.configuration.userContentController addScriptMessageHandler: self.bridge name: @"WKWebViewJavascriptBridge"];

同樣,iOS中的長期回調等其它一些非核心內容也暫時隱藏了

API的設計

按照上述的實現,可以構建出一個完整的JSBridge交互流程,H5和Native的交互已經通了

接下來就是設計API真正給外界調用

準確的來說,API的設計已經脫離了JSBridge交互內容,屬於混合框架框架應用層次,因此後續會有單獨的章節介紹quick hybrid中的API

API如何實現?可以參考上文中Android的繼承IBridgeImpl法以及iOS的繼承RegisterBaseClass然後重寫registerHandlers

至於該規劃些什麼API,這與實際的需求有關,不過一般情況下,像ui.alert等等一般都是必須的

更多詳情請待後續章節

結束語

最後再來一張圖鞏固下把

至此,整個JSBridge交互就已經完成了

其實在總結文章時,考慮過很多種形式,發現,
如果是全文字描述,十分枯燥,很難堅持讀下來,
如果是各種原理都用繪圖+描述,發現會化簡為繁,硬生生把難度提高了幾個level,
所以最終採用的是偽代碼(半偽半真)展示形式(剔除一些無效信息,提取關鍵,而且還不和最終的代碼衝突)

雖然說,這整套流程都沒有特別難的地方,涉及的知識點都不是特別深。但是卻包含了前端,Android,iOS三個領域。
因此如果要將整套工作做的比較好的化最好還是有分工的好,比較一個人的精力有限,真正專精多個領域的人還是比較少的,
而且後續各個優化的內容也不少(API,優化,等等...)

返回根目錄

源碼

github上這個框架的實現

quickhybrid/quickhybrid

附錄

參考資料


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 一切就緒,開始規劃API,這裡在規劃前對API進行了一次分類:__短期API、長期API__ 首先申明下,這個是在實際框架演變過程中自創的一個概念,其它混合框架可能也會有這個概念,但應該是會在原生底層來實現,而不是前端實現。。 而這裡由於是 ,所以相比其它混合框架,前端多了一個處理引擎(包括多 ...
  • 1、 添加自定義屬性 page 2、 為 ztree 每個樹形節點,添加點擊事件 ...
  • 在每個節點添加 id 和 pid, id 表示當前節點編號,pid 表示父節點編號 第一步:在頁面顯示菜單位置,添加 ul設置 class=”ztree” 第二步:開啟簡單數據格式支持 第三步:編寫樹形菜單數據 第四步:生成樹形菜單 ...
  • 匿名函數:創建函數時,不指定函數名的函數。此種函數只能運行一次,可以避免變數全局污染,保護變數。 匿名函數可以回調使用,比如 btn.addEventListener("click",function(){...}) 也可以用匿名函數聲明函數名,比如var abc=function (){conso ...
  • 1、 對選項卡面板區域 div 設置 class=”easyui-tabs” 2、 對選項卡面板區域添加多個 div,每個 div 就是一個選項卡(每個面板一定設置 title) 3、 設置面板 fit 為 true ,自適應父容器大小 4、 設置選項卡 closable 為 true ,添加可關閉 ...
  • 1、對摺疊面板區域 div 設置 class=”easyui-accordion” 2、在區域添加多個 div, 每個 div 就是一個面板 (每個面板一定要設置 title 屬性)。 3、設置面板屬性 fit 為 true,自適應父容器大小 ...
  • 一、前言 前段時間寫博客分享和介紹了阿裡雲的UI框架NG-ZORRO(博客請查看:http://www.cnblogs.com/donaldtdz/p/7892960.html),結合近段時間對.Net開源框架ABP的學習。完成將ABP前端框架替換成阿裡雲的NG-ZORRO。 二、替換說明 ABP版 ...
  • 作者: "zyl910" [TOC] 一、緣由 由於在ES6之前,JavaScript中沒有定義類(class)語法。導致大家用各種五花八門的辦法來定義類,代碼風格不統一。而且對於模擬面向對象的三大支柱“封裝”、“繼承”、“多態”,更是有許多專門的深度研究,實現辦法更加複雜,不利於JavaScrip ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...