WebViewJavascriptBridge 源碼中 Get 到的“橋梁美學”

原文地址

前言

Web 頁面中的 JS 與 iOS Native 如何交互是每個 iOS 猿必須掌握的技能。而 JS 和 iOS Native 就好比兩塊沒有交集的大陸,如果想要使它們相互通信就必須要建立一座“橋梁”。

思考一下,如果項目組讓你去造這座“橋”,如何才能做到既優雅又實用?

本文將結合 WebViewJavascriptBridge 源碼逐步帶大家找到答案。

WebViewJavascriptBridge?是盛名已久的 JSBridge 庫,早在 2011 年就被作者?Marcus Westin?發布到 GitHub,直到現在作者還在積極維護中,目前該項目已收獲近 1w star 咯,其源碼非常值得我們學習。

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

我對 WebViewJavascriptBridge 的評價是小而美,這類小而美的源碼非常利于我們對其實現思想的學習(本文分析 WebViewJavascriptBridge 源碼版本為 v6.0.3)。

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

所以這篇文章不會再把重點放在 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,這里簡單列一部分(笑):

Facebook Messenger

Facebook Paper

ELSEWHERE

... & many more!

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

在讀完 WebViewJavascriptBridge 的源碼之后我將其劃分為三個層級:


其中 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

+ (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

? ? #define WVJB_WEBVIEW_DELEGATE_INTERFACE NSObject

#elif defined __IPHONE_OS_VERSION_MAX_ALLOWED

? ? #import

? ? #define WVJB_PLATFORM_IOS

? ? #define WVJB_WEBVIEW_TYPE UIWebView

? ? #define WVJB_WEBVIEW_DELEGATE_TYPE NSObject

? ? #define WVJB_WEBVIEW_DELEGATE_INTERFACE NSObject

#endif

分別根據所在平臺不同定義了 WVJB_WEBVIEW_TYPE,WVJB_WEBVIEW_DELEGATE_TYPE 以及剛才提到的 WVJB_WEBVIEW_DELEGATE_INTERFACE 宏定義,并且分別定義了 WVJB_PLATFORM_OSX 和 WVJB_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

#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 _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

- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand;

@end

@interface WebViewJavascriptBridgeBase : NSObject

@property (weak, nonatomic) id 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 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[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 組件時踩到的一些坑以及經驗,因為自己水平有限...所以也可能不會寫(笑)。

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