WebViewJavascriptBridge 源碼中 Get 到的「橋樑美學」

前言

Emmmmm...這篇文章發佈出來可能正逢聖誕節🎄,Merry Christmas!javascript

Web 頁面中的 JS 與 iOS Native 如何交互是每一個 iOS 猿必須掌握的技能。而 JS 和 iOS Native 就比如兩塊沒有交集的大陸,若是想要使它們相互通訊就必需要創建一座「橋樑」。前端

思考一下,若是項目組讓你去造這座「橋」,如何才能作到既優雅又實用?java

本文將結合 WebViewJavascriptBridge 源碼逐步帶你們找到答案。ios

WebViewJavascriptBridge 是盛名已久的 JSBridge 庫,早在 2011 年就被做者 Marcus Westin 發佈到 GitHub,直到如今做者還在積極維護中,目前該項目已收穫近 1w star 咯,其源碼很是值得咱們學習。git

WebViewJavascriptBridge 的代碼邏輯清晰,風格良好,加上自身代碼量比較小使得其源碼閱讀很是輕鬆(可能須要一些 JS 基礎)。更加難能難得的是它僅使用了少許代碼就實現了對於 Mac OS X 的 WebView 以及 iOS 平臺的 UIWebView 和 WKWebView 三種組件的完美支持。github

我對 WebViewJavascriptBridge 的評價是小而美,這類小而美的源碼很是利於咱們對其實現思想的學習(本文分析 WebViewJavascriptBridge 源碼版本爲 v6.0.3)。web

關於 iOS 與 JS 的原生交互知識,以前我有寫過一篇文章《iOS 與 JS 交互開發知識總結》,文章除了介紹 JavaScriptCore 庫以及 UIWebView 和 WKWebView 與 JS 原生交互的方法以外還捎帶提到了 Hybrid 的發展簡史,文末還提供了一個 JS 經過 Native 調用 iOS 設備攝像頭的 Demojson

因此這篇文章不會再把重點放在 iOS 與 JS 的原生交互了,本文旨在介紹 WebViewJavascriptBridge 的設計思路和實現原理,對 iOS 與 JS 原生交互知識感興趣的同窗推薦去閱讀上面提到的文章,應該會有點兒幫助(笑)。數組

索引

  • WebViewJavascriptBridge 簡介
  • WebViewJavascriptBridge && WKWebViewJavascriptBridge 探究
  • WebViewJavascriptBridgeBase - JS 調用 Native 實現原理剖析
  • WebViewJavascriptBridge_JS - Native 調用 JS 實現解讀
  • WebViewJavascriptBridge 的「橋樑美學」
  • 文章總結

WebViewJavascriptBridge 簡介

WebViewJavascriptBridge 是用於在 WKWebView,UIWebView 和 WebView 中的 Obj-C 和 JavaScript 之間發送消息的 iOS / OSX 橋接器。緩存

有許多不錯的項目都有使用 WebViewJavascriptBridge,這裏簡單列一部分(笑):

關於 WebViewJavascriptBridge 的具體使用方法詳見其 GitHub 頁面

在讀完 WebViewJavascriptBridge 的源碼以後我將其劃分爲三個層級:

層級 源文件
接口層 WebViewJavascriptBridge && WKWebViewJavascriptBridge
實現層 WebViewJavascriptBridgeBase
JS 層 WebViewJavascriptBridge_JS

其中 WebViewJavascriptBridge && WKWebViewJavascriptBridge 做爲接口層主要負責提供方便的接口,隱藏實現細節,其實現細節都是經過實現層 WebViewJavascriptBridgeBase 去作的,而 WebViewJavascriptBridge_JS 做爲 JS 層其實存儲了一段 JS 代碼,在須要的時候注入到當前 WebView 組件中,最終實現 Native 與 JS 的交互。

WebViewJavascriptBridge && WKWebViewJavascriptBridge 探究

WebViewJavascriptBridge 和 WKWebViewJavascriptBridge 做爲接口層分別對應於 UIWebView 和 WKWebView 組件,咱們來簡單看一下這兩個文件暴露出的信息:

WebViewJavascriptBridge 暴露信息:

@interface WebViewJavascriptBridge : WVJB_WEBVIEW_DELEGATE_INTERFACE

+ (instancetype)bridgeForWebView:(id)webView; // 初始化
+ (instancetype)bridge:(id)webView; // 初始化

+ (void)enableLogging; // 開啓日誌
+ (void)setLogMaxLength:(int)length; // 設置日誌最大長度

- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler; // 註冊 handler (Native)
- (void)removeHandler:(NSString*)handlerName; // 刪除 handler (Native)
- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback; // 調用 handler (JS)
- (void)setWebViewDelegate:(id)webViewDelegate; // 設置 webViewDelegate
- (void)disableJavscriptAlertBoxSafetyTimeout; // 禁用 JS AlertBox 的安全時長來加速消息傳遞,不推薦使用

@end
複製代碼

WKWebViewJavascriptBridge 暴露信息:

// Emmmmm...這裏應該不須要我註釋了吧
@interface WKWebViewJavascriptBridge : NSObject<WKNavigationDelegate, WebViewJavascriptBridgeBaseDelegate>

+ (instancetype)bridgeForWebView:(WKWebView*)webView;
+ (void)enableLogging;

- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;
- (void)removeHandler:(NSString*)handlerName;
- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback;
- (void)reset;
- (void)setWebViewDelegate:(id)webViewDelegate;
- (void)disableJavscriptAlertBoxSafetyTimeout;

@end
複製代碼

Note: disableJavscriptAlertBoxSafetyTimeout 方法是經過禁用 JS 端 AlertBox 的安全時長來加速網橋消息傳遞的。若是想使用那麼須要和前端約定好,若是禁用以後前端 JS 代碼仍有調用 AlertBox 相關代碼(alert, confirm, 或 prompt)則程序將被掛起,因此這個方法是不安全的,如無特殊需求筆者不推薦使用。

能夠看得出來這兩個文件暴露出的接口幾乎一致,其中 WebViewJavascriptBridge 中使用了宏定義 WVJB_WEBVIEW_DELEGATE_INTERFACE 來分別適配 iOS 和 Mac OS X 平臺的 UIWebView 和 WebView 組件須要實現的代理方法。

WebViewJavascriptBridge 中的宏定義

其實 WebViewJavascriptBridge 中爲了適配 iOS 和 Mac OS X 平臺的 UIWebView 和 WebView 組件使用了一系列的宏定義,其源碼比較簡單:

#if defined __MAC_OS_X_VERSION_MAX_ALLOWED
    #define WVJB_PLATFORM_OSX
    #define WVJB_WEBVIEW_TYPE WebView
    #define WVJB_WEBVIEW_DELEGATE_TYPE NSObject<WebViewJavascriptBridgeBaseDelegate>
    #define WVJB_WEBVIEW_DELEGATE_INTERFACE NSObject<WebViewJavascriptBridgeBaseDelegate, WebPolicyDelegate>
#elif defined __IPHONE_OS_VERSION_MAX_ALLOWED
    #import <UIKit/UIWebView.h>
    #define WVJB_PLATFORM_IOS
    #define WVJB_WEBVIEW_TYPE UIWebView
    #define WVJB_WEBVIEW_DELEGATE_TYPE NSObject<UIWebViewDelegate>
    #define WVJB_WEBVIEW_DELEGATE_INTERFACE NSObject<UIWebViewDelegate, WebViewJavascriptBridgeBaseDelegate>
#endif
複製代碼

分別根據所在平臺不一樣定義了 WVJB_WEBVIEW_TYPEWVJB_WEBVIEW_DELEGATE_TYPE 以及剛纔提到的 WVJB_WEBVIEW_DELEGATE_INTERFACE 宏定義,而且分別定義了 WVJB_PLATFORM_OSXWVJB_PLATFORM_IOS 便於以後的實現源碼區分當前平臺時使用,下面的 supportsWKWebView 宏定義也是一樣的道理:

#if (__MAC_OS_X_VERSION_MAX_ALLOWED > __MAC_10_9 || __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_7_1)
#define supportsWKWebView
#endif
複製代碼

在引入頭文件的時候能夠經過這個 supportsWKWebView 宏靈活引入所需的頭文件:

// WebViewJavascriptBridge.h
#if defined supportsWKWebView
#import <WebKit/WebKit.h>
#endif

// WebViewJavascriptBridge.m
#if defined(supportsWKWebView)
#import "WKWebViewJavascriptBridge.h"
#endif
複製代碼

WebViewJavascriptBridge 的實現分析

咱們接着看一下 WebViewJavascriptBridge 的實現部分,首先從內部變量信息看起:

#if __has_feature(objc_arc_weak)
    #define WVJB_WEAK __weak
#else
    #define WVJB_WEAK __unsafe_unretained
#endif

@implementation WebViewJavascriptBridge {
    WVJB_WEAK WVJB_WEBVIEW_TYPE* _webView; // bridge 對應的 WebView 組件
    WVJB_WEAK id _webViewDelegate; // 給 WebView 組件設置的代理(須要的話)
    long _uniqueId; // 惟一標識,Emmmmm...可是我發現沒卵用,只有 _base 中的 _uniqueId 纔有用
    WebViewJavascriptBridgeBase *_base; // 上文說過,底層實現其實都是 WebViewJavascriptBridgeBase 在作
}
複製代碼

上文提到 WebViewJavascriptBridge 和 WKWebViewJavascriptBridge 的 .h 文件暴露接口信息很是類似,那麼咱們要不要看看 WKWebViewJavascriptBridge 的內部變量信息呢?

// 註釋參見 WebViewJavascriptBridge 就好
@implementation WKWebViewJavascriptBridge {
    __weak WKWebView* _webView;
    __weak id<WKNavigationDelegate> _webViewDelegate;
    long _uniqueId;
    WebViewJavascriptBridgeBase *_base;
}
複製代碼

嘛~ 這倆貨簡直是一個媽生的。其實這是做者故意爲之,由於做者想對外提供一套接口,即 WebViewJavascriptBridge,咱們只須要使用 WebViewJavascriptBridge 就能夠自動根據綁定的 WebView 組件的不一樣生成與之對應的 JSBridge 實例。

+ (instancetype)bridge:(id)webView {
// 若是支持 WKWebView
#if defined supportsWKWebView
    // 須要先判斷當前入參 webView 是否從屬於 WKWebView
    if ([webView isKindOfClass:[WKWebView class]]) {
        // 返回 WKWebViewJavascriptBridge 實例
        return (WebViewJavascriptBridge*) [WKWebViewJavascriptBridge bridgeForWebView:webView];
    }
#endif
    // 判斷當前入參 webView 是否從屬於 WebView(Mac OS X)或者 UIWebView(iOS)
    if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) {
        // 返回 WebViewJavascriptBridge 實例
        WebViewJavascriptBridge* bridge = [[self alloc] init];
        [bridge _platformSpecificSetup:webView];
        return bridge;
    }
    
    // 拋出 BadWebViewType 異常並返回 nil
    [NSException raise:@"BadWebViewType" format:@"Unknown web view type."];
    return nil;
}
複製代碼

咱們能夠看到上面的代碼,實現並不複雜。若是支持 WKWebView 的話(#if defined supportsWKWebView)則去判斷當前綁定的 WebView 組件是否從屬於 WKWebView,這樣能夠返回 WKWebViewJavascriptBridge 實例,不然返回 WebViewJavascriptBridge 實例,最後若是入參 webView 的類型不知足判斷條件則拋出 BadWebViewType 異常。

還有一個關於 _webViewDelegate 的小細節,原本不打算講的,可是仍是提一下吧(囧)。其實在 WebViewJavascriptBridge 以及 WKWebViewJavascriptBridge 的初始化實現過程當中,會把當前 WebView 組件的代理綁定爲本身:

// WebViewJavascriptBridge
- (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView {
    _webView = webView;
    _webView.delegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    _base.delegate = self;
}

// WKWebViewJavascriptBridge
- (void) _setupInstance:(WKWebView*)webView {
    _webView = webView;
    _webView.navigationDelegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    _base.delegate = self;
}
複製代碼

Note: 替換組件的代理將其代理綁定爲 bridge 本身是由於 WebViewJavascriptBridge 的實現原理上是利用我以前的文章《iOS 與 JS 交互開發知識總結》中講過的假 Request 方法實現的,因此須要監聽 WebView 組件的代理方法獲取加載以前的 Request.URL 並作處理。這也是爲何 WebViewJavascriptBridge 提供了一個接口 setWebViewDelegate: 存儲了一個邏輯上的 _webViewDelegate,這個 _webViewDelegate 也須要遵循 WebView 組件的代理協議,這樣在 WebViewJavascriptBridge 內部不一樣的代理方法中作完 bridge 要作的事情只有就會再去調用 _webViewDelegate 對應的代理方法,其實能夠理解爲 WebViewJavascriptBridge 對當前 WebView 組件的代理作了 hook。

對於 WebViewJavascriptBridge 中暴露的初始化之外的全部接口,其內部實現都是經過 WebViewJavascriptBridgeBase 來實現的。這樣作的好處就是即便 WebViewJavascriptBridge 由於綁定了 WKWebView 返回了 WKWebViewJavascriptBridge 實例,只要接口一致,對 JSBridge 發送相同的消息,就會有相同的實現(都是由 WebViewJavascriptBridgeBase 類實現的)

WebViewJavascriptBridgeBase - JS 調用 Native 實現原理剖析

做爲 WebViewJavascriptBridge 的實現層,WebViewJavascriptBridgeBase 的命名也能夠體現出其是做爲整座「橋樑」橋墩通常的存在,咱們仍是按照老規矩先看一下 WebViewJavascriptBridgeBase.h 暴露的信息,好對其有一個總體的印象:

typedef void (^WVJBResponseCallback)(id responseData); // 回調 block
typedef void (^WVJBHandler)(id data, WVJBResponseCallback responseCallback); // 註冊的 Handler block
typedef NSDictionary WVJBMessage; // 消息類型 - 字典

@protocol WebViewJavascriptBridgeBaseDelegate <NSObject>
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand;
@end

@interface WebViewJavascriptBridgeBase : NSObject

@property (weak, nonatomic) id <WebViewJavascriptBridgeBaseDelegate> delegate; // 代理,指向接口層類,用以給對應接口綁定的 WebView 組件發送執行 JS 消息
@property (strong, nonatomic) NSMutableArray* startupMessageQueue; // 啓動消息隊列,能夠理解爲存放 WVJBMessage
@property (strong, nonatomic) NSMutableDictionary* responseCallbacks; // 回調 blocks 字典,存放 WVJBResponseCallback 類型的 block
@property (strong, nonatomic) NSMutableDictionary* messageHandlers; // 已註冊的 handlers 字典,存放 WVJBHandler 類型的 block
@property (strong, nonatomic) WVJBHandler messageHandler; // 沒卵用

+ (void)enableLogging; // 開啓日誌
+ (void)setLogMaxLength:(int)length; // 設置日誌最大長度
- (void)reset; // 對應 WKJSBridge 的 reset 接口
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName; // 發送消息,入參依次是參數,回調 block,對應 JS 端註冊的 HandlerName
- (void)flushMessageQueue:(NSString *)messageQueueString; // 刷新消息隊列,核心代碼
- (void)injectJavascriptFile; // 注入 JS
- (BOOL)isWebViewJavascriptBridgeURL:(NSURL*)url; // 斷定是否爲 WebViewJavascriptBridgeURL
- (BOOL)isQueueMessageURL:(NSURL*)urll; // 斷定是否爲隊列消息 URL
- (BOOL)isBridgeLoadedURL:(NSURL*)urll; // 斷定是否爲 bridge 載入 URL
- (void)logUnkownMessage:(NSURL*)url; // 打印收到未知消息信息
- (NSString *)webViewJavascriptCheckCommand; // JS bridge 檢測命令
- (NSString *)webViewJavascriptFetchQueyCommand; // JS bridge 獲取查詢命令
- (void)disableJavscriptAlertBoxSafetyTimeout; // 禁用 JS AlertBox 安全時長以獲取發送消息速度提高,不建議使用,理由見上文

@end
複製代碼

嘛~ 從 .h 文件中咱們能夠看到整個 WebViewJavascriptBridgeBase 所暴露出來的信息,屬性層面上須要對如下 4 個屬性加深印象,以後分析實現的過程當中會帶入這些屬性:

  • id <WebViewJavascriptBridgeBaseDelegate> delegate 代理,能夠經過代理讓當前 bridge 綁定的 WebView 組件執行 JS 代碼
  • NSMutableArray* startupMessageQueue; 啓動消息隊列,存放 Obj-C 發送給 JS 的消息(能夠理解爲存放 WVJBMessage 類型)
  • NSMutableDictionary* responseCallbacks; 回調 blocks 字典,存放 WVJBResponseCallback 類型的 block
  • NSMutableDictionary* messageHandlers; Obj-C 端已註冊的 handlers 字典,存放 WVJBHandler 類型的 block

Emmmmm...接口層面看一下注釋就行了,後面分析實現的時候會捎帶講解一些接口,剩下一些跟實現無關的接口內容感興趣的同窗推薦本身扒源碼哈。

咱們在對 WebViewJavascriptBridgeBase 總體有了一個初始印象以後就能夠本身寫一個頁面,簡單的嵌入一些 JS 跑一遍流程,在中間下斷點扒源碼,這樣咱們對於 Native 與 JS 的交互流程就能夠一清二楚了。

下面模擬一遍 JS 經過 WebViewJavascriptBridge 調用 Native 功能的流程分析 WebViewJavascriptBridgeBase 的相關實現(考慮如今的時間點決定以 WKWebView 爲例講解,即針對 WKWebViewJavascriptBridge 源碼講解):

1.監聽假 Request 並注入 WebViewJavascriptBridge_JS 內的 JS 代碼

上文說到 WebViewJavascriptBridge 的實現其實本質上是利用了我以前的文章《iOS 與 JS 交互開發知識總結》中講過的假 Request 方法實現的,那麼咱們就從監聽假 Request 開始講起吧。

// WKNavigationDelegate 協議方法,用於監聽 Request 並決定是否容許導航
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    // webView 校驗
    if (webView != _webView) { return; }
    NSURL *url = navigationAction.request.URL;
    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;

    // 核心代碼
    if ([_base isWebViewJavascriptBridgeURL:url]) { // 斷定 WebViewJavascriptBridgeURL
        if ([_base isBridgeLoadedURL:url]) { // 斷定 BridgeLoadedURL
            // 注入 JS 代碼
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) { // 斷定 QueueMessageURL
            // 刷新消息隊列
            [self WKFlushMessageQueue];
        } else {
            // 記錄未知 bridge msg 日誌
            [_base logUnkownMessage:url];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    
    // 調用 _webViewDelegate 對應的代理方法
    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
        [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}
複製代碼

Note: 以前說過 WebViewJavascriptBridge 會 hook 綁定的 WebView 的代理方法,這一點 WKWebViewJavascriptBridge 也同樣,在加入本身的代碼以後會判斷是否有 _webViewDelegate 響應這個代理方法,若是有則調用。

咱們仍是把注意力放到註釋中核心代碼的位置,裏面會先判斷當前 url 是否爲 bridge url:

// 相關宏定義
#define kOldProtocolScheme @"wvjbscheme"
#define kNewProtocolScheme @"https"
#define kQueueHasMessage @"__wvjb_queue_message__"
#define kBridgeLoaded @"__bridge_loaded__"
複製代碼

WebViewJavascriptBridge GitHub 頁面 的使用方法中第 4 步明確指出要複製粘貼 setupWebViewJavascriptBridge 方法到前段 JS 中,咱們先來看一下這段 JS 方法源碼:

function setupWebViewJavascriptBridge(callback) {
	if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
	if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
	window.WVJBCallbacks = [callback];
	// 建立一個 iframe
	var WVJBIframe = document.createElement('iframe');
	// 設置 iframe 爲不顯示
	WVJBIframe.style.display = 'none';
	// 將 iframe 的 src 置爲 'https://__bridge_loaded__'
	WVJBIframe.src = 'https://__bridge_loaded__';
	// 將 iframe 加入到 document.documentElement
	document.documentElement.appendChild(WVJBIframe);
	setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
複製代碼

上面的代碼建立了一個不顯示的 iframe 並將其 src 置爲 https://__bridge_loaded__,與上文中 kBridgeLoaded 宏定義一致,即用於 isBridgeLoadedURL: 方法中斷定當前 url 是否爲 BridgeLoadedURL。

Note: 假 Request 的發起有兩種方式,-1:location.href -2:iframe。經過 location.href 有個問題,就是若是 JS 屢次調用原生的方法也就是 location.href 的值屢次變化,Native 端只能接受到最後一次請求,前面的請求會被忽略掉,因此這裏 WebViewJavascriptBridge 選擇使用 iframe,後面再也不解釋。

由於加入了 src 爲 https://__bridge_loaded__ 的 iframe 元素,咱們上面截獲 url 的代理方法就會拿到一個 https://__bridge_loaded__ 的 url,因爲 https 知足斷定 WebViewJavascriptBridgeURL,將會進入核心代碼區域接着會被斷定爲 BridgeLoadedURL 執行注入 JS 代碼的方法,即 [_base injectJavascriptFile];

- (void)injectJavascriptFile {
    // 獲取到 WebViewJavascriptBridge_JS 的代碼
    NSString *js = WebViewJavascriptBridge_js();
    // 將獲取到的 js 經過代理方法注入到當前綁定的 WebView 組件
    [self _evaluateJavascript:js];
    // 若是當前已有消息隊列則遍歷並分發消息,以後清空消息隊列
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}
複製代碼

至此,第一步交互已完成。關於 WebViewJavascriptBridge_JS 內部的 JS 代碼咱們放到後面的章節解讀,如今能夠簡單理解爲 WebViewJavascriptBridge 在 JS 端的具體實現代碼。

2.JS 端調用 callHandler 方法以後 Native 端到底是如何響應的?

WebViewJavascriptBridge GitHub 頁面 中指出 JS 端的操做方式:

setupWebViewJavascriptBridge(function(bridge) {
	
	/* Initialize your app here */

	bridge.registerHandler('JS Echo', function(data, responseCallback) {
		console.log("JS Echo called with:", data)
		responseCallback(data)
	})
	bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) {
		console.log("JS received response:", responseData)
	})
})
複製代碼

咱們知道 JS 端調用 setupWebViewJavascriptBridge 方法會走咱們剛纔分析過的第一步,即監聽假 Request 並注入 WebViewJavascriptBridge_JS 內的 JS 代碼。那麼當 JS 端調用 bridge.callHandler 時,Native 端到底是如何作出響應的呢?這裏咱們須要先稍微解讀一下以前注入的 WebViewJavascriptBridge_JS 中的 JS 代碼:

// 調用 iOS handler,參數校驗以後調用 _doSend 函數
function callHandler(handlerName, data, responseCallback) {
	if (arguments.length == 2 && typeof data == 'function') {
		responseCallback = data;
		data = null;
	}
	_doSend({ handlerName:handlerName, data:data }, responseCallback);
}

// 若有回調,則設置 message['callbackId'] 與 responseCallbacks[callbackId]
// 將 msg 加入 sendMessageQueue 數組,設置 messagingIframe.src
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;
}
	
// scheme 使用 https 以後經過 host 作匹配
var CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
複製代碼

能夠看到 JS 端的代碼中有 callHandler 函數的實現,其內部將入參 handlerName 以及 data 以字典形式做爲參數調用 _doSend 方法,咱們看一下 _doSend 方法的實現:

  • _doSend 方法內部會先判斷入參中是否有回調
  • 若是有回調則根據規則生成 callbackId 而且將回調 block 保存到 responseCallbacks 字典(囧~ JS 不叫字典的,我是爲了 iOS 讀者看着方便),以後給消息也加入一個鍵值對保存剛纔生成的 callbackId
  • 以後給 sendMessageQueue 隊列加入 message
  • messagingIframe.src 設置爲 https://__wvjb_queue_message__

好,點到爲止,對於 WebViewJavascriptBridge_JS 內的 JS 端其餘源碼咱們放着後面看。注意這裏加入了一個 src 爲 https://__wvjb_queue_message__messagingIframe,它也是一個不可見的 iframe。這樣 Native 端會收到一個 url 爲 https://__wvjb_queue_message__ 的 request,回到第 1 步中獲取到假的 request 以後會進行各項斷定,此次會知足 [_base isQueueMessageURL:url] 的斷定調用 Native 的 WKFlushMessageQueue 方法。

- (void)WKFlushMessageQueue {
    // 執行 WebViewJavascriptBridge._fetchQueue(); 方法
    [_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) {
        if (error != nil) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
        }
        // 刷新消息列表
        [_base flushMessageQueue:result];
    }];
}

- (NSString *)webViewJavascriptFetchQueyCommand {
    return @"WebViewJavascriptBridge._fetchQueue();";
}
複製代碼

可見 Native 端會在刷新隊列中調用 JS 端的 WebViewJavascriptBridge._fetchQueue(); 方法,咱們來看一下 JS 端此方法的具體實現:

// 獲取隊列,在 iOS 端刷新消息隊列時會調用此函數
function _fetchQueue() {
   // 將 sendMessageQueue 轉爲 JSON 格式
	var messageQueueString = JSON.stringify(sendMessageQueue);
	// 重置 sendMessageQueue
	sendMessageQueue = [];
	// 返回 JSON 格式的 
	return messageQueueString;
}
複製代碼

這個方法會把當前 JS 端 sendMessageQueue 消息隊列以 JSON 的形式返回,而 Native 端會調用 [_base flushMessageQueue:result]; 將拿到的 JSON 形式消息隊列做爲參數調用 flushMessageQueue: 方法,這個方法是整個框架 Native 端的精華所在,就是稍微有點長(笑)。

- (void)flushMessageQueue:(NSString *)messageQueueString {
    // 校驗 messageQueueString
    if (messageQueueString == nil || messageQueueString.length == 0) {
        NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
        return;
    }

    // 將 messageQueueString 經過 NSJSONSerialization 解爲 messages 並遍歷
    id messages = [self _deserializeMessageJSON:messageQueueString];
    for (WVJBMessage* message in messages) {
        // 類型校驗
        if (![message isKindOfClass:[WVJBMessage class]]) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
            continue;
        }
        [self _log:@"RCVD" json:message];
        
        // 嘗試取 responseId,如取到則代表是回調,從 _responseCallbacks 取匹配的回調 block 執行
        NSString* responseId = message[@"responseId"];
        if (responseId) { // 取到 responseId
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
        } else { // 未取到 responseId,則代表是正常的 JS callHandler 調用 iOS
            WVJBResponseCallback responseCallback = NULL;
            // 嘗試取 callbackId,示例 cb_1_1512035076293
            // 對應 JS 代碼 var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
            NSString* callbackId = message[@"callbackId"];
            if (callbackId) { // 取到 callbackId,表示 js 端但願在調用 iOS native 代碼後有回調
                responseCallback = ^(id responseData) {
                    if (responseData == nil) {
                        responseData = [NSNull null];
                    }
                    
                    // 將 callbackId 做爲 msg 的 responseId 並設置 responseData,執行 _queueMessage
                    WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                    // _queueMessage 函數主要是把 msg 轉爲 JSON 格式,內含 responseId = callbackId
                    // JS 端調用 WebViewJavascriptBridge._handleMessageFromObjC('msg_JSON'); 其中 'msg_JSON' 就是 JSON 格式的 msg
                    [self _queueMessage:msg];
                };
            } else { // 未取到 callbackId
                responseCallback = ^(id ignoreResponseData) {
                    // Do nothing
                };
            }
            
            // 嘗試以 handlerName 獲取 iOS 端以前註冊過的 handler
            WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
            if (!handler) { // 沒註冊過,則跳過此 msg
                NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                continue;
            }
            // 調用對應的 handler,以 message[@"data"] 爲入參,以 responseCallback 爲回調
            handler(message[@"data"], responseCallback);
        }
    }
}
複製代碼

嘛~ flushMessageQueue: 方法做爲整個 Native 端的核心,有點長是能夠理解的。咱們簡單理一下它的實現思路:

  • 入參校驗
  • 將 JSON 形式的入參轉換爲 Native 對象,即消息隊列,這裏面消息類型是以前定義過的 WVJBMessage,即字典
  • 若是消息中含有 「responseId」 則代表是以前 Native 調用的 JS 方法回調過來的消息(由於 JS 端和 Native 端實現邏輯是對等的,因此這個地方不明白的能夠參考下面的分析)
  • 若是消息中不含 「responseId」 則代表是 JS 端經過 callHandler 函數正常調用 Native 端過來的消息
  • 嘗試獲取消息中的 「callbackId」,若是 JS 本次消息須要 Native 響應以後回調纔會有這個鍵值,具體參見上文中 JS 端 _doSend 部分源碼分析。如取到 「callbackId」 則需生成一個回調 block,回調 block 內部將 「callbackId」 做爲 msg 的 「responseId」 執行 _queueMessage 將消息發送給 JS 端(JS 端處理消息邏輯與 Native 端一致,因此上面使用 「responseId」 判斷當前消息是否爲回調方法傳遞過來的消息是很容易理解的)
  • 嘗試以消息中的 「handlerName」 從 messageHandlers(上文提到過,是保存 Native 端註冊過的 handler 的字典)取到對應的 handler block,若是取到則執行代碼塊,不然打印錯誤日誌

Note: 這個消息處理的方法雖然長,可是邏輯清晰,並且有效的解決了 JS 與 Native 相互調用的過程當中參數傳遞的問題(包括回調),此外 JS 端的消息處理邏輯與 Native 端保持一致,實現了邏輯對稱,很是值得咱們學習。

WebViewJavascriptBridge_JS - Native 調用 JS 實現解讀

Emmmmm...這一章節主要講 JS 端注入的代碼,即 WebViewJavascriptBridge_JS 中的 JS 源碼。因爲我沒作過前段,能力不足,水平有限,可能有謬誤但願各位讀者發現的話及時指正,感激涕零。預警,因爲 JS 端和上文分析過的 Native 端邏輯對稱且上文已經分析過部分 JS 端的函數,因此下面的 JS 源碼沒有另作拆分,爲避免被大段 JS 代碼糊臉不感興趣的同窗能夠直接看代碼後面的總結。

;(function() {
    // window.WebViewJavascriptBridge 校驗,避免重複
	if (window.WebViewJavascriptBridge) {
		return;
	}

    // 懶加載 window.onerror,用於打印 error 日誌
	if (!window.onerror) {
		window.onerror = function(msg, url, line) {
			console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
		}
	}
	
	// window.WebViewJavascriptBridge 聲明
	window.WebViewJavascriptBridge = {
		registerHandler: registerHandler,
		callHandler: callHandler,
		disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
		_fetchQueue: _fetchQueue,
		_handleMessageFromObjC: _handleMessageFromObjC
	};

    // 變量聲明
	var messagingIframe; // 消息 iframe
	var sendMessageQueue = []; // 發送消息隊列
	var messageHandlers = {}; // JS 端註冊的消息處理 handlers 字典(囧,JS 其實叫對象)
	
	// scheme 使用 https 以後經過 host 作匹配
	var CUSTOM_PROTOCOL_SCHEME = 'https';
	var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
	
	var responseCallbacks = {}; // JS 端存放回調的字典
	var uniqueId = 1; // 惟一標示,用於回調時生成 callbackId
	var dispatchMessagesWithTimeoutSafety = true; // 默認啓用安全時長

    // 經過禁用 AlertBoxSafetyTimeout 來提速網橋消息傳遞
    function disableJavscriptAlertBoxSafetyTimeout() {
		dispatchMessagesWithTimeoutSafety = false;
	}

    // 同 iOS 邏輯,註冊 handler 實際上是往 messageHandlers 字典中插入對應 name 的 block
	function registerHandler(handlerName, handler) {
		messageHandlers[handlerName] = handler;
	}
	
	// 調用 iOS handler,參數校驗以後調用 _doSend 函數
	function callHandler(handlerName, data, responseCallback) {
	    // 若是參數只有兩個且第二個參數類型爲 function,則表示沒有參數傳遞,即 data 爲空
		if (arguments.length == 2 && typeof data == 'function') {
			responseCallback = data;
			data = null;
		}
		// 將 handlerName 和 data 做爲 msg 對象參數調用 _doSend 函數
		_doSend({ handlerName:handlerName, data:data }, responseCallback);
	}
	
	// _doSend 向 Native 端發送消息
	function _doSend(message, responseCallback) {
	    // 若有回調,則設置 message['callbackId'] 與 responseCallbacks[callbackId]
		if (responseCallback) {
			var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
			responseCallbacks[callbackId] = responseCallback;
			message['callbackId'] = callbackId;
		}
		// 將 msg 加入 sendMessageQueue 數組,設置 messagingIframe.src
		sendMessageQueue.push(message);
		messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
	}

    // 獲取隊列,在 iOS 端刷新消息隊列時會調用此函數
	function _fetchQueue() {
	    // 內部將發送消息隊列 sendMessageQueue 轉爲 JSON 格式並返回
		var messageQueueString = JSON.stringify(sendMessageQueue);
		sendMessageQueue = [];
		return messageQueueString;
	}

	// iOS 端 _dispatchMessage 函數會調用此函數
	function _handleMessageFromObjC(messageJSON) {
	    // 調度從 Native 端獲取到的消息
        _dispatchMessageFromObjC(messageJSON);
	}
    
    // 核心代碼,調度從 Native 端獲取到的消息,邏輯與 Native 端一致
	function _dispatchMessageFromObjC(messageJSON) {
		// 判斷有沒有禁用 AlertBoxSafetyTimeout,最終會調用 _doDispatchMessageFromObjC 函數
		if (dispatchMessagesWithTimeoutSafety) {
			setTimeout(_doDispatchMessageFromObjC);
		} else {
			 _doDispatchMessageFromObjC();
		}
		
		// 解析 msgJSON 獲得 msg
		function _doDispatchMessageFromObjC() {
			var message = JSON.parse(messageJSON);
			var messageHandler;
			var responseCallback;

			// 若是有 responseId,則說明是回調,取對應的 responseCallback 執行,以後釋放
			if (message.responseId) {
				responseCallback = responseCallbacks[message.responseId];
				if (!responseCallback) {
					return;
				}
				responseCallback(message.responseData);
				delete responseCallbacks[message.responseId];
			} else { // 沒有 responseId,則表示正常的 iOS call handler 調用 js
				// 如 msg 包含 callbackId,說明 iOS 端須要回調,初始化對應的 responseCallback
				if (message.callbackId) {
					var callbackResponseId = message.callbackId;
					responseCallback = function(responseData) {
						_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
					};
				}
				
				// 從 messageHandlers 拿到對應的 handler 執行
				var handler = messageHandlers[message.handlerName];
				if (!handler) {
				    // 如未取到對應的 handler 則打印錯誤日誌
					console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
				} else {
					handler(message.data, responseCallback);
				}
			}
		}
	}

    // messagingIframe 的聲明,類型 iframe,樣式不可見,src 設置
	messagingIframe = document.createElement('iframe');
	messagingIframe.style.display = 'none';
	messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
	// messagingIframe 加入 document.documentElement 中
	document.documentElement.appendChild(messagingIframe);

    // 註冊 disableJavscriptAlertBoxSafetyTimeout handler,Native 能夠經過禁用 AlertBox 的安全時長來加速橋接消息
	registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
	
	setTimeout(_callWVJBCallbacks, 0);
	function _callWVJBCallbacks() {
		var callbacks = window.WVJBCallbacks;
		delete window.WVJBCallbacks;
		for (var i=0; i<callbacks.length; i++) {
			callbacks[i](WebViewJavascriptBridge);
		}
	}
}
複製代碼

JS 端和 Native 端邏輯一致,上面的代碼已經加入了詳細的中文註釋,上文在對於「WebViewJavascriptBridgeBase - JS 調用 Native 實現原理剖析」章節的分析過程當中爲了走通整個調用的邏輯已經對部分 JS 端代碼進行了分析,這裏咱們簡單的梳理一下 JS 端核心代碼 _doDispatchMessageFromObjC 函數的邏輯:

  • 將 messageJSON 使用 JSON 解析出來
  • 嘗試取解析到的消息中的 responseId,若是有取到則說明是 Native 端響應 JS 端以後經過回調向 JS 端發出的消息,用 responseId 取 responseCallbacks 中對應的回調響應 block,找到後執行該 block 以後刪除
  • 若是沒取到 responseId 則表示這條消息是 Native 端經過 callHandler:data:responseCallback: 正常調用 JS 註冊的 handler 發送過來的消息(這裏的正常是針對回調而言)
  • 若是當前的消息有 callbackId 則代表 Native 端須要 JS 端響應本次消息以後回調反饋,生成一個 responseCallback 做爲回調 block (JS 端是 function) ,其內部使用 _doSend 方法傳遞一個帶有 responseId 的消息給 Native 端,代表此條消息是以前的回調消息
  • 最後按照解析到的消息中 handlerName 從 messageHandlers,即 JS 端註冊過的 handlers 中找到與名稱對應的處理函數執行,若是沒找到則打印附帶相關信息的錯誤日誌

嘛~ 對比一下 Native 端的核心代碼 flushMessageQueue: 看一下,很容易發現兩端的處理實現是邏輯對稱的。

WebViewJavascriptBridge 的「橋樑美學」

在總結 WebViewJavascriptBridge 的「橋樑美學」以前請再回顧一下 WebViewJavascriptBridge 的工做流:

  • JS 端加入 src 爲 https://__bridge_loaded__ 的 iframe
  • Native 端檢測到 Request,檢測若是是 __bridge_loaded__ 則經過當前的 WebView 組件注入 WebViewJavascriptBridge_JS 代碼
  • 注入代碼成功以後會加入一個 messagingIframe,其 src 爲 https://__wvjb_queue_message__
  • 以後不管是 Native 端仍是 JS 端均可以經過 registerHandler 方法註冊一個兩端約定好的 HandlerName 的處理,也均可以經過 callHandler 方法經過約定好的 HandlerName 調用另外一端的處理(兩端處理消息的實現邏輯對稱)

嘛~ 因此咱們很容易列舉出 WebViewJavascriptBridge 所具備的「美學」:

  • 隱性適配
  • 接口對等
  • 邏輯對稱

咱們結合本文展開來講一下上面的「美學」的具體實現。

隱性適配

WebViewJavascriptBridge 主要是做爲 Mac OS X 和 iOS 端(Native 端)與 JS 端相互通訊,互相調用的橋樑。對於 Mac OS X 和 iOS 兩種平臺包含的三種 WebView 功能組件而言,WebViewJavascriptBridge 作了隱性適配,即僅用一套代碼便可綁定不一樣平臺的 WebView 組件實現一樣功能的 JS 通訊功能,這一點很是方便。

接口對等

WebViewJavascriptBridge 對於 JS 端和 Native 端設計了對等的接口,不管是 JS 端仍是 Native 端,註冊本端的響應處理都是用 registerHandler 接口,調用另外一端(給另外一端發消息)都是用 callHandler 接口。

這樣作是很是合理的,由於不管是 JS 端仍是 Native 端,做爲通訊的雙方就通訊自己而言是處於對等的地位的。這就比如一座大橋鏈接兩塊陸地,兩地用大橋相互運輸貨物並接收資源,兩塊陸地在大橋的運輸使用過程當中邏輯上也是地位對等的。

邏輯對稱

WebViewJavascriptBridge 在 JS 端和 Native 端對發送過來的消息有着相同邏輯的處理實現,若是考慮到收發雙方的身份則能夠把邏輯相同看作邏輯對稱。

這種實現方式依舊很是合理,被橋鏈接的兩塊大陸在裝貨上橋和下橋卸貨這兩處邏輯上就應該是對稱的。

嘛~ 說到這裏就不得不祭出一個詞來形容 WebViewJavascriptBridge 了,這個詞就是優雅(笑)。當你們結合 WebViewJavascriptBridge 源碼閱讀本文以後不難發現其整個架構和設計思想跟現實橋樑設計中不少設計思想不謀而合,好比橋通常會分爲左右橋幅,而左右幅橋通常只有一條線路中心線,即一個前進方向,用於橋上單一方向的資源傳輸,左右橋幅在功能上對等。

文章總結

  • 文章系統分析了 WebViewJavascriptBridge 源碼,但願各位讀者可以在閱讀本文以後對 WebViewJavascriptBridge 的架構有一個總體認識。
  • 文章對 WebViewJavascriptBridge 在 JS 端和 Native 端的消息處理實現作了深刻剖析,但願能夠對各位讀者這部分源碼的理解提供一些微薄的幫助。
  • 總結了 WebViewJavascriptBridge 做爲一個 JSBridge 框架所具備的優點,即文中所指的「橋樑美學」,指望能夠對你們之後本身封裝一個 JSBridge 提供思路,拋磚引玉。

Emmmmm...不過須要注意的是 WebViewJavascriptBridge 僅僅是做爲 JSBridge 層用於提供 JS 和 Native 之間相互傳遞消息的基礎支持的。若是想要封裝本身項目中的 WebView 組件還須要另外實現 HTTP cookie 注入,自定義 User-Agent,白名單或者權限校驗等功能,更進一步還須要對 WebView 組件進行初始化速度,頁面渲染速度以及頁面緩存策略的優化。我以後也許可能大概應該會寫一篇文章分享一下本身封裝 WebView 組件時踩到的一些坑以及經驗,由於本身水平有限...因此也可能不會寫(笑)。

文章寫得比較用心(是我我的的原創文章,轉載請註明 lision.me/),若是發現錯誤會優先在個人 我的博客 中更新。若是有任何問題歡迎在個人微博 @Lision 聯繫我~


補充~ 我建了一個技術交流微信羣,想在裏面認識更多的朋友!若是各位同窗對文章有什麼疑問或者工做之中遇到一些小問題均可以在羣裏找到我或者其餘羣友交流討論,期待你的加入喲~

Emmmmm..因爲微信羣人數過百致使不能夠掃碼入羣,因此請掃描上面的二維碼關注公衆號進羣。

相關文章
相關標籤/搜索