WebViewJavascriptBridge 源碼中 Get 到的“橋梁美學(xué)”

原文地址

前言

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

思考一下眉反,如果項(xiàng)目組讓你去造這座“橋”琅轧,如何才能做到既優(yōu)雅又實(shí)用律歼?

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

WebViewJavascriptBridge?是盛名已久的 JSBridge 庫,早在 2011 年就被作者?Marcus Westin?發(fā)布到 GitHub适肠,直到現(xiàn)在作者還在積極維護(hù)中霍衫,目前該項(xiàng)目已收獲近 1w star 咯,其源碼非常值得我們學(xué)習(xí)侯养。

WebViewJavascriptBridge 的代碼邏輯清晰敦跌,風(fēng)格良好,加上自身代碼量比較小使得其源碼閱讀非常輕松(可能需要一些 JS 基礎(chǔ))逛揩。更加難能可貴的是它僅使用了少量代碼就實(shí)現(xiàn)了對(duì)于 Mac OS X 的 WebView 以及 iOS 平臺(tái)的 UIWebView 和 WKWebView 三種組件的完美支持柠傍。

我對(duì) WebViewJavascriptBridge 的評(píng)價(jià)是小而美,這類小而美的源碼非常利于我們對(duì)其實(shí)現(xiàn)思想的學(xué)習(xí)(本文分析 WebViewJavascriptBridge 源碼版本為 v6.0.3)辩稽。

關(guān)于 iOS 與 JS 的原生交互知識(shí)惧笛,之前我有寫過一篇文章《iOS 與 JS 交互開發(fā)知識(shí)總結(jié)》,文章除了介紹 JavaScriptCore 庫以及 UIWebView 和 WKWebView 與 JS 原生交互的方法之外還捎帶提到了?Hybrid?的發(fā)展簡史逞泄,文末還提供了一個(gè)?JS 通過 Native 調(diào)用 iOS 設(shè)備攝像頭的 Demo患整。

所以這篇文章不會(huì)再把重點(diǎn)放在 iOS 與 JS 的原生交互了,本文旨在介紹?WebViewJavascriptBridge?的設(shè)計(jì)思路和實(shí)現(xiàn)原理炭懊,對(duì) iOS 與 JS 原生交互知識(shí)感興趣的同學(xué)推薦去閱讀上面提到的文章并级,應(yīng)該會(huì)有點(diǎn)兒幫助(笑)。

索引

WebViewJavascriptBridge 簡介

WebViewJavascriptBridge && WKWebViewJavascriptBridge 探究

WebViewJavascriptBridgeBase - JS 調(diào)用 Native 實(shí)現(xiàn)原理剖析

WebViewJavascriptBridge_JS - Native 調(diào)用 JS 實(shí)現(xiàn)解讀

WebViewJavascriptBridge 的“橋梁美學(xué)”

文章總結(jié)

WebViewJavascriptBridge 簡介

WebViewJavascriptBridge 是用于在 WKWebView侮腹,UIWebView 和 WebView 中的 Obj-C 和 JavaScript 之間發(fā)送消息的 iOS / OSX 橋接器嘲碧。

有許多不錯(cuò)的項(xiàng)目都有使用 WebViewJavascriptBridge,這里簡單列一部分(笑):

Facebook Messenger

Facebook Paper

ELSEWHERE

... & many more!

關(guān)于 WebViewJavascriptBridge 的具體使用方法詳見其?GitHub 頁面父阻。

在讀完 WebViewJavascriptBridge 的源碼之后我將其劃分為三個(gè)層級(jí):


其中 WebViewJavascriptBridge && WKWebViewJavascriptBridge 作為接口層主要負(fù)責(zé)提供方便的接口愈涩,隱藏實(shí)現(xiàn)細(xì)節(jié),其實(shí)現(xiàn)細(xì)節(jié)都是通過實(shí)現(xiàn)層 WebViewJavascriptBridgeBase 去做的加矛,而 WebViewJavascriptBridge_JS 作為 JS 層其實(shí)存儲(chǔ)了一段 JS 代碼履婉,在需要的時(shí)候注入到當(dāng)前 WebView 組件中,最終實(shí)現(xiàn) Native 與 JS 的交互斟览。

WebViewJavascriptBridge && WKWebViewJavascriptBridge 探究

WebViewJavascriptBridge 和 WKWebViewJavascriptBridge 作為接口層分別對(duì)應(yīng)于 UIWebView 和 WKWebView 組件毁腿,我們來簡單看一下這兩個(gè)文件暴露出的信息:

WebViewJavascriptBridge 暴露信息:

@interface WebViewJavascriptBridge : WVJB_WEBVIEW_DELEGATE_INTERFACE

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

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

+ (void)enableLogging; // 開啟日志

+ (void)setLogMaxLength:(int)length; // 設(shè)置日志最大長度

- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler; // 注冊(cè) handler (Native)

- (void)removeHandler:(NSString*)handlerName; // 刪除 handler (Native)

- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback; // 調(diào)用 handler (JS)

- (void)setWebViewDelegate:(id)webViewDelegate; // 設(shè)置 webViewDelegate

- (void)disableJavscriptAlertBoxSafetyTimeout; // 禁用 JS AlertBox 的安全時(shí)長來加速消息傳遞,不推薦使用

@end

WKWebViewJavascriptBridge 暴露信息:

// Emmmmm...這里應(yīng)該不需要我注釋了吧

@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 的安全時(shí)長來加速網(wǎng)橋消息傳遞的苛茂。如果想使用那么需要和前端約定好已烤,如果禁用之后前端 JS 代碼仍有調(diào)用 AlertBox 相關(guān)代碼(alert, confirm, 或 prompt)則程序?qū)⒈粧炱穑赃@個(gè)方法是不安全的妓羊,如無特殊需求筆者不推薦使用胯究。

可以看得出來這兩個(gè)文件暴露出的接口幾乎一致,其中 WebViewJavascriptBridge 中使用了宏定義 WVJB_WEBVIEW_DELEGATE_INTERFACE 來分別適配 iOS 和 Mac OS X 平臺(tái)的 UIWebView 和 WebView 組件需要實(shí)現(xiàn)的代理方法躁绸。

WebViewJavascriptBridge 中的宏定義

其實(shí) WebViewJavascriptBridge 中為了適配 iOS 和 Mac OS X 平臺(tái)的 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

分別根據(jù)所在平臺(tái)不同定義了 WVJB_WEBVIEW_TYPE臣嚣,WVJB_WEBVIEW_DELEGATE_TYPE 以及剛才提到的 WVJB_WEBVIEW_DELEGATE_INTERFACE 宏定義,并且分別定義了 WVJB_PLATFORM_OSX 和 WVJB_PLATFORM_IOS 便于之后的實(shí)現(xiàn)源碼區(qū)分當(dāng)前平臺(tái)時(shí)使用剥哑,下面的 supportsWKWebView 宏定義也是同樣的道理:

#if (__MAC_OS_X_VERSION_MAX_ALLOWED > __MAC_10_9 || __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_7_1)

#define supportsWKWebView

#endif

在引入頭文件的時(shí)候可以通過這個(gè) supportsWKWebView 宏靈活引入所需的頭文件:

// WebViewJavascriptBridge.h

#if defined supportsWKWebView

#import

#endif

// WebViewJavascriptBridge.m

#if defined(supportsWKWebView)

#import "WKWebViewJavascriptBridge.h"

#endif

WebViewJavascriptBridge 的實(shí)現(xiàn)分析

我們接著看一下 WebViewJavascriptBridge 的實(shí)現(xiàn)部分硅则,首先從內(nèi)部變量信息看起:

#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 對(duì)應(yīng)的 WebView 組件

? ? WVJB_WEAK id _webViewDelegate; // 給 WebView 組件設(shè)置的代理(需要的話)

? ? long _uniqueId; // 唯一標(biāo)識(shí),Emmmmm...但是我發(fā)現(xiàn)沒卵用株婴,只有 _base 中的 _uniqueId 才有用

? ? WebViewJavascriptBridgeBase *_base; // 上文說過抢埋,底層實(shí)現(xiàn)其實(shí)都是 WebViewJavascriptBridgeBase 在做

}

上文提到 WebViewJavascriptBridge 和 WKWebViewJavascriptBridge 的 .h 文件暴露接口信息非常相似,那么我們要不要看看 WKWebViewJavascriptBridge 的內(nèi)部變量信息呢督暂?

// 注釋參見 WebViewJavascriptBridge 就好

@implementation WKWebViewJavascriptBridge {

? ? __weak WKWebView* _webView;

? ? __weak id _webViewDelegate;

? ? long _uniqueId;

? ? WebViewJavascriptBridgeBase *_base;

}

嘛~ 這倆貨簡直是一個(gè)媽生的揪垄。其實(shí)這是作者故意為之,因?yàn)樽髡呦雽?duì)外提供一套接口逻翁,即 WebViewJavascriptBridge饥努,我們只需要使用 WebViewJavascriptBridge 就可以自動(dòng)根據(jù)綁定的 WebView 組件的不同生成與之對(duì)應(yīng)的 JSBridge 實(shí)例。

+ (instancetype)bridge:(id)webView {

// 如果支持 WKWebView

#if defined supportsWKWebView

? ? // 需要先判斷當(dāng)前入?yún)?webView 是否從屬于 WKWebView

? ? if ([webView isKindOfClass:[WKWebView class]]) {

? ? ? ? // 返回 WKWebViewJavascriptBridge 實(shí)例

? ? ? ? return (WebViewJavascriptBridge*) [WKWebViewJavascriptBridge bridgeForWebView:webView];

? ? }

#endif

? ? // 判斷當(dāng)前入?yún)?webView 是否從屬于 WebView(Mac OS X)或者 UIWebView(iOS)

? ? if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) {

? ? ? ? // 返回 WebViewJavascriptBridge 實(shí)例

? ? ? ? WebViewJavascriptBridge* bridge = [[self alloc] init];

? ? ? ? [bridge _platformSpecificSetup:webView];

? ? ? ? return bridge;

? ? }


? ? // 拋出 BadWebViewType 異常并返回 nil

? ? [NSException raise:@"BadWebViewType" format:@"Unknown web view type."];

? ? return nil;

}

我們可以看到上面的代碼八回,實(shí)現(xiàn)并不復(fù)雜酷愧。如果支持 WKWebView 的話(#if defined supportsWKWebView)則去判斷當(dāng)前綁定的 WebView 組件是否從屬于 WKWebView,這樣可以返回 WKWebViewJavascriptBridge 實(shí)例缠诅,否則返回 WebViewJavascriptBridge 實(shí)例溶浴,最后如果入?yún)?webView 的類型不滿足判斷條件則拋出 BadWebViewType 異常。

還有一個(gè)關(guān)于 _webViewDelegate 的小細(xì)節(jié)管引,本來不打算講的士败,但是還是提一下吧(囧)。其實(shí)在 WebViewJavascriptBridge 以及 WKWebViewJavascriptBridge 的初始化實(shí)現(xiàn)過程中褥伴,會(huì)把當(dāng)前 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 自己是因?yàn)?WebViewJavascriptBridge 的實(shí)現(xiàn)原理上是利用我之前的文章《iOS 與 JS 交互開發(fā)知識(shí)總結(jié)》中講過的假 Request 方法實(shí)現(xiàn)的谅将,所以需要監(jiān)聽 WebView 組件的代理方法獲取加載之前的 Request.URL 并做處理。這也是為什么 WebViewJavascriptBridge 提供了一個(gè)接口 setWebViewDelegate: 存儲(chǔ)了一個(gè)邏輯上的 _webViewDelegate重慢,這個(gè) _webViewDelegate 也需要遵循 WebView 組件的代理協(xié)議饥臂,這樣在 WebViewJavascriptBridge 內(nèi)部不同的代理方法中做完 bridge 要做的事情只有就會(huì)再去調(diào)用 _webViewDelegate 對(duì)應(yīng)的代理方法,其實(shí)可以理解為 WebViewJavascriptBridge 對(duì)當(dāng)前 WebView 組件的代理做了 hook似踱。

對(duì)于 WebViewJavascriptBridge 中暴露的初始化以外的所有接口隅熙,其內(nèi)部實(shí)現(xiàn)都是通過 WebViewJavascriptBridgeBase 來實(shí)現(xiàn)的。這樣做的好處就是即使 WebViewJavascriptBridge 因?yàn)榻壎?WKWebView 返回了 WKWebViewJavascriptBridge 實(shí)例核芽,只要接口一致囚戚,對(duì) JSBridge 發(fā)送相同的消息,就會(huì)有相同的實(shí)現(xiàn)(都是由 WebViewJavascriptBridgeBase 類實(shí)現(xiàn)的)狞洋。

WebViewJavascriptBridgeBase - JS 調(diào)用 Native 實(shí)現(xiàn)原理剖析

作為 WebViewJavascriptBridge 的實(shí)現(xiàn)層弯淘,WebViewJavascriptBridgeBase 的命名也可以體現(xiàn)出其是作為整座“橋梁”橋墩一般的存在绿店,我們還是按照老規(guī)矩先看一下 WebViewJavascriptBridgeBase.h 暴露的信息吉懊,好對(duì)其有一個(gè)整體的印象:

typedef void (^WVJBResponseCallback)(id responseData); // 回調(diào) block

typedef void (^WVJBHandler)(id data, WVJBResponseCallback responseCallback); // 注冊(cè)的 Handler block

typedef NSDictionary WVJBMessage; // 消息類型 - 字典

@protocol WebViewJavascriptBridgeBaseDelegate

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

@end

@interface WebViewJavascriptBridgeBase : NSObject

@property (weak, nonatomic) id delegate; // 代理庐橙,指向接口層類,用以給對(duì)應(yīng)接口綁定的 WebView 組件發(fā)送執(zhí)行 JS 消息

@property (strong, nonatomic) NSMutableArray* startupMessageQueue; // 啟動(dòng)消息隊(duì)列借嗽,可以理解為存放 WVJBMessage

@property (strong, nonatomic) NSMutableDictionary* responseCallbacks; // 回調(diào) blocks 字典态鳖,存放 WVJBResponseCallback 類型的 block

@property (strong, nonatomic) NSMutableDictionary* messageHandlers; // 已注冊(cè)的 handlers 字典,存放 WVJBHandler 類型的 block

@property (strong, nonatomic) WVJBHandler messageHandler; // 沒卵用

+ (void)enableLogging; // 開啟日志

+ (void)setLogMaxLength:(int)length; // 設(shè)置日志最大長度

- (void)reset; // 對(duì)應(yīng) WKJSBridge 的 reset 接口

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName; // 發(fā)送消息恶导,入?yún)⒁来问菂?shù)浆竭,回調(diào) block,對(duì)應(yīng) JS 端注冊(cè)的 HandlerName

- (void)flushMessageQueue:(NSString *)messageQueueString; // 刷新消息隊(duì)列惨寿,核心代碼

- (void)injectJavascriptFile; // 注入 JS

- (BOOL)isWebViewJavascriptBridgeURL:(NSURL*)url; // 判定是否為 WebViewJavascriptBridgeURL

- (BOOL)isQueueMessageURL:(NSURL*)urll; // 判定是否為隊(duì)列消息 URL

- (BOOL)isBridgeLoadedURL:(NSURL*)urll; // 判定是否為 bridge 載入 URL

- (void)logUnkownMessage:(NSURL*)url; // 打印收到未知消息信息

- (NSString *)webViewJavascriptCheckCommand; // JS bridge 檢測命令

- (NSString *)webViewJavascriptFetchQueyCommand; // JS bridge 獲取查詢命令

- (void)disableJavscriptAlertBoxSafetyTimeout; // 禁用 JS AlertBox 安全時(shí)長以獲取發(fā)送消息速度提升邦泄,不建議使用,理由見上文

@end

嘛~ 從 .h 文件中我們可以看到整個(gè) WebViewJavascriptBridgeBase 所暴露出來的信息裂垦,屬性層面上需要對(duì)以下 4 個(gè)屬性加深印象顺囊,之后分析實(shí)現(xiàn)的過程中會(huì)帶入這些屬性:

id delegate 代理,可以通過代理讓當(dāng)前 bridge 綁定的 WebView 組件執(zhí)行 JS 代碼

NSMutableArray* startupMessageQueue; 啟動(dòng)消息隊(duì)列蕉拢,存放 Obj-C 發(fā)送給 JS 的消息(可以理解為存放 WVJBMessage 類型)

NSMutableDictionary* responseCallbacks; 回調(diào) blocks 字典特碳,存放 WVJBResponseCallback 類型的 block

NSMutableDictionary* messageHandlers; Obj-C 端已注冊(cè)的 handlers 字典,存放 WVJBHandler 類型的 block

Emmmmm...接口層面看一下注釋就好了晕换,后面分析實(shí)現(xiàn)的時(shí)候會(huì)捎帶講解一些接口午乓,剩下一些跟實(shí)現(xiàn)無關(guān)的接口內(nèi)容感興趣的同學(xué)推薦自己扒源碼哈。

我們?cè)趯?duì) WebViewJavascriptBridgeBase 整體有了一個(gè)初始印象之后就可以自己寫一個(gè)頁面闸准,簡單的嵌入一些 JS 跑一遍流程益愈,在中間下斷點(diǎn)扒源碼,這樣我們對(duì)于 Native 與 JS 的交互流程就可以一清二楚了夷家。

下面模擬一遍 JS 通過 WebViewJavascriptBridge 調(diào)用 Native 功能的流程分析 WebViewJavascriptBridgeBase 的相關(guān)實(shí)現(xiàn)(考慮現(xiàn)在的時(shí)間點(diǎn)決定以 WKWebView 為例講解腕唧,即針對(duì) WKWebViewJavascriptBridge 源碼講解):

1.監(jiān)聽假 Request 并注入 WebViewJavascriptBridge_JS 內(nèi)的 JS 代碼

上文說到 WebViewJavascriptBridge 的實(shí)現(xiàn)其實(shí)本質(zhì)上是利用了我之前的文章《iOS 與 JS 交互開發(fā)知識(shí)總結(jié)》中講過的假 Request 方法實(shí)現(xiàn)的,那么我們就從監(jiān)聽假 Request 開始講起吧瘾英。

// WKNavigationDelegate 協(xié)議方法枣接,用于監(jiān)聽 Request 并決定是否允許導(dǎo)航

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {

? ? // webView 校驗(yàn)

? ? 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

? ? ? ? ? ? // 刷新消息隊(duì)列

? ? ? ? ? ? [self WKFlushMessageQueue];

? ? ? ? } else {

? ? ? ? ? ? // 記錄未知 bridge msg 日志

? ? ? ? ? ? [_base logUnkownMessage:url];

? ? ? ? }

? ? ? ? decisionHandler(WKNavigationActionPolicyCancel);

? ? ? ? return;

? ? }


? ? // 調(diào)用 _webViewDelegate 對(duì)應(yīng)的代理方法

? ? if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {

? ? ? ? [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];

? ? } else {

? ? ? ? decisionHandler(WKNavigationActionPolicyAllow);

? ? }

}

Note: 之前說過 WebViewJavascriptBridge 會(huì) hook 綁定的 WebView 的代理方法,這一點(diǎn) WKWebViewJavascriptBridge 也一樣缺谴,在加入自己的代碼之后會(huì)判斷是否有 _webViewDelegate 響應(yīng)這個(gè)代理方法但惶,如果有則調(diào)用。

我們還是把注意力放到注釋中核心代碼的位置湿蛔,里面會(huì)先判斷當(dāng)前 url 是否為 bridge url:

// 相關(guān)宏定義

#define kOldProtocolScheme @"wvjbscheme"

#define kNewProtocolScheme @"https"

#define kQueueHasMessage ? @"__wvjb_queue_message__"

#define kBridgeLoaded ? ? ?@"__bridge_loaded__"

WebViewJavascriptBridge GitHub 頁面?的使用方法中第 4 步明確指出要復(fù)制粘貼 setupWebViewJavascriptBridge 方法到前段 JS 中膀曾,我們先來看一下這段 JS 方法源碼:

function setupWebViewJavascriptBridge(callback) {

if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }

if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }

window.WVJBCallbacks = [callback];

// 創(chuàng)建一個(gè) iframe

var WVJBIframe = document.createElement('iframe');

// 設(shè)置 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)

}

上面的代碼創(chuàng)建了一個(gè)不顯示的 iframe 并將其 src 置為 https://__bridge_loaded__,與上文中 kBridgeLoaded 宏定義一致阳啥,即用于 isBridgeLoadedURL: 方法中判定當(dāng)前 url 是否為 BridgeLoadedURL添谊。

Note: 假 Request 的發(fā)起有兩種方式,-1:location.href -2:iframe察迟。通過 location.href 有個(gè)問題斩狱,就是如果 JS 多次調(diào)用原生的方法也就是 location.href 的值多次變化耳高,Native 端只能接受到最后一次請(qǐng)求,前面的請(qǐng)求會(huì)被忽略掉所踊,所以這里 WebViewJavascriptBridge 選擇使用 iframe泌枪,后面不再解釋。

因?yàn)榧尤肓?src 為 https://__bridge_loaded__ 的 iframe 元素秕岛,我們上面截獲 url 的代理方法就會(huì)拿到一個(gè) https://__bridge_loaded__ 的 url碌燕,由于 https 滿足判定 WebViewJavascriptBridgeURL,將會(huì)進(jìn)入核心代碼區(qū)域接著會(huì)被判定為 BridgeLoadedURL 執(zhí)行注入 JS 代碼的方法继薛,即 [_base injectJavascriptFile];修壕。

- (void)injectJavascriptFile {

? ? // 獲取到 WebViewJavascriptBridge_JS 的代碼

? ? NSString *js = WebViewJavascriptBridge_js();

? ? // 將獲取到的 js 通過代理方法注入到當(dāng)前綁定的 WebView 組件

? ? [self _evaluateJavascript:js];

? ? // 如果當(dāng)前已有消息隊(duì)列則遍歷并分發(fā)消息,之后清空消息隊(duì)列

? ? if (self.startupMessageQueue) {

? ? ? ? NSArray* queue = self.startupMessageQueue;

? ? ? ? self.startupMessageQueue = nil;

? ? ? ? for (id queuedMessage in queue) {

? ? ? ? ? ? [self _dispatchMessage:queuedMessage];

? ? ? ? }

? ? }

}

至此遏考,第一步交互已完成叠殷。關(guān)于 WebViewJavascriptBridge_JS 內(nèi)部的 JS 代碼我們放到后面的章節(jié)解讀,現(xiàn)在可以簡單理解為 WebViewJavascriptBridge 在 JS 端的具體實(shí)現(xiàn)代碼诈皿。

2.JS 端調(diào)用 callHandler 方法之后 Native 端究竟是如何響應(yīng)的林束?

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 端調(diào)用 setupWebViewJavascriptBridge 方法會(huì)走我們剛才分析過的第一步,即監(jiān)聽假 Request 并注入 WebViewJavascriptBridge_JS 內(nèi)的 JS 代碼稽亏。那么當(dāng) JS 端調(diào)用 bridge.callHandler 時(shí)壶冒,Native 端究竟是如何做出響應(yīng)的呢?這里我們需要先稍微解讀一下之前注入的 WebViewJavascriptBridge_JS 中的 JS 代碼:

// 調(diào)用 iOS handler截歉,參數(shù)校驗(yàn)之后調(diào)用 _doSend 函數(shù)

function callHandler(handlerName, data, responseCallback) {

if (arguments.length == 2 && typeof data == 'function') {

responseCallback = data;

data = null;

}

_doSend({ handlerName:handlerName, data:data }, responseCallback);

}

// 如有回調(diào)胖腾,則設(shè)置 message['callbackId'] 與 responseCallbacks[callbackId]

// 將 msg 加入 sendMessageQueue 數(shù)組,設(shè)置 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 函數(shù)的實(shí)現(xiàn)瘪松,其內(nèi)部將入?yún)?handlerName 以及 data 以字典形式作為參數(shù)調(diào)用 _doSend 方法咸作,我們看一下 _doSend 方法的實(shí)現(xiàn):

_doSend 方法內(nèi)部會(huì)先判斷入?yún)⒅惺欠裼谢卣{(diào)

如果有回調(diào)則根據(jù)規(guī)則生成 callbackId 并且將回調(diào) block 保存到 responseCallbacks 字典(囧~ JS 不叫字典的,我是為了 iOS 讀者看著方便)宵睦,之后給消息也加入一個(gè)鍵值對(duì)保存剛才生成的 callbackId

之后給 sendMessageQueue 隊(duì)列加入 message

將 messagingIframe.src 設(shè)置為 https://__wvjb_queue_message__

好记罚,點(diǎn)到為止,對(duì)于 WebViewJavascriptBridge_JS 內(nèi)的 JS 端其他源碼我們放著后面看壳嚎。注意這里加入了一個(gè) src 為 https://__wvjb_queue_message__ 的 messagingIframe桐智,它也是一個(gè)不可見的 iframe。這樣 Native 端會(huì)收到一個(gè) url 為 https://__wvjb_queue_message__ 的 request烟馅,回到第 1 步中獲取到假的 request 之后會(huì)進(jìn)行各項(xiàng)判定说庭,這次會(huì)滿足 [_base isQueueMessageURL:url] 的判定調(diào)用 Native 的 WKFlushMessageQueue 方法。

- (void)WKFlushMessageQueue {

? ? // 執(zhí)行 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 端會(huì)在刷新隊(duì)列中調(diào)用 JS 端的 WebViewJavascriptBridge._fetchQueue(); 方法郑趁,我們來看一下 JS 端此方法的具體實(shí)現(xiàn):

// 獲取隊(duì)列刊驴,在 iOS 端刷新消息隊(duì)列時(shí)會(huì)調(diào)用此函數(shù)

function _fetchQueue() {

? ?// 將 sendMessageQueue 轉(zhuǎn)為 JSON 格式

var messageQueueString = JSON.stringify(sendMessageQueue);

// 重置 sendMessageQueue

sendMessageQueue = [];

// 返回 JSON 格式的

return messageQueueString;

}

這個(gè)方法會(huì)把當(dāng)前 JS 端 sendMessageQueue 消息隊(duì)列以 JSON 的形式返回,而 Native 端會(huì)調(diào)用 [_base flushMessageQueue:result]; 將拿到的 JSON 形式消息隊(duì)列作為參數(shù)調(diào)用 flushMessageQueue: 方法,這個(gè)方法是整個(gè)框架 Native 端的精華所在捆憎,就是稍微有點(diǎn)長(笑)舅柜。

- (void)flushMessageQueue:(NSString *)messageQueueString {

? ? // 校驗(yàn) 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) {

? ? ? ? // 類型校驗(yàn)

? ? ? ? if (![message isKindOfClass:[WVJBMessage class]]) {

? ? ? ? ? ? NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);

? ? ? ? ? ? continue;

? ? ? ? }

? ? ? ? [self _log:@"RCVD" json:message];


? ? ? ? // 嘗試取 responseId,如取到則表明是回調(diào)攻礼,從 _responseCallbacks 取匹配的回調(diào) block 執(zhí)行

? ? ? ? NSString* responseId = message[@"responseId"];

? ? ? ? if (responseId) { // 取到 responseId

? ? ? ? ? ? WVJBResponseCallback responseCallback = _responseCallbacks[responseId];

? ? ? ? ? ? responseCallback(message[@"responseData"]);

? ? ? ? ? ? [self.responseCallbacks removeObjectForKey:responseId];

? ? ? ? } else { // 未取到 responseId,則表明是正常的 JS callHandler 調(diào)用 iOS

? ? ? ? ? ? WVJBResponseCallback responseCallback = NULL;

? ? ? ? ? ? // 嘗試取 callbackId栗柒,示例 cb_1_1512035076293

? ? ? ? ? ? // 對(duì)應(yīng) JS 代碼 var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();

? ? ? ? ? ? NSString* callbackId = message[@"callbackId"];

? ? ? ? ? ? if (callbackId) { // 取到 callbackId礁扮,表示 js 端希望在調(diào)用 iOS native 代碼后有回調(diào)

? ? ? ? ? ? ? ? responseCallback = ^(id responseData) {

? ? ? ? ? ? ? ? ? ? if (responseData == nil) {

? ? ? ? ? ? ? ? ? ? ? ? responseData = [NSNull null];

? ? ? ? ? ? ? ? ? ? }


? ? ? ? ? ? ? ? ? ? // 將 callbackId 作為 msg 的 responseId 并設(shè)置 responseData,執(zhí)行 _queueMessage

? ? ? ? ? ? ? ? ? ? WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };

? ? ? ? ? ? ? ? ? ? // _queueMessage 函數(shù)主要是把 msg 轉(zhuǎn)為 JSON 格式瞬沦,內(nèi)含 responseId = callbackId

? ? ? ? ? ? ? ? ? ? // JS 端調(diào)用 WebViewJavascriptBridge._handleMessageFromObjC('msg_JSON'); 其中 'msg_JSON' 就是 JSON 格式的 msg

? ? ? ? ? ? ? ? ? ? [self _queueMessage:msg];

? ? ? ? ? ? ? ? };

? ? ? ? ? ? } else { // 未取到 callbackId

? ? ? ? ? ? ? ? responseCallback = ^(id ignoreResponseData) {

? ? ? ? ? ? ? ? ? ? // Do nothing

? ? ? ? ? ? ? ? };

? ? ? ? ? ? }


? ? ? ? ? ? // 嘗試以 handlerName 獲取 iOS 端之前注冊(cè)過的 handler

? ? ? ? ? ? WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];

? ? ? ? ? ? if (!handler) { // 沒注冊(cè)過太伊,則跳過此 msg

? ? ? ? ? ? ? ? NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);

? ? ? ? ? ? ? ? continue;

? ? ? ? ? ? }

? ? ? ? ? ? // 調(diào)用對(duì)應(yīng)的 handler,以 message[@"data"] 為入?yún)⒐渥辏?responseCallback 為回調(diào)

? ? ? ? ? ? handler(message[@"data"], responseCallback);

? ? ? ? }

? ? }

}

嘛~ flushMessageQueue: 方法作為整個(gè) Native 端的核心僚焦,有點(diǎn)長是可以理解的。我們簡單理一下它的實(shí)現(xiàn)思路:

入?yún)⑿r?yàn)

將 JSON 形式的入?yún)⑥D(zhuǎn)換為 Native 對(duì)象曙痘,即消息隊(duì)列芳悲,這里面消息類型是之前定義過的 WVJBMessage,即字典

如果消息中含有 “responseId” 則表明是之前 Native 調(diào)用的 JS 方法回調(diào)過來的消息(因?yàn)?JS 端和 Native 端實(shí)現(xiàn)邏輯是對(duì)等的边坤,所以這個(gè)地方不明白的可以參考下面的分析)

如果消息中不含 “responseId” 則表明是 JS 端通過 callHandler 函數(shù)正常調(diào)用 Native 端過來的消息

嘗試獲取消息中的 “callbackId”名扛,如果 JS 本次消息需要 Native 響應(yīng)之后回調(diào)才會(huì)有這個(gè)鍵值,具體參見上文中 JS 端 _doSend 部分源碼分析茧痒。如取到 “callbackId” 則需生成一個(gè)回調(diào) block肮韧,回調(diào) block 內(nèi)部將 “callbackId” 作為 msg 的 “responseId” 執(zhí)行 _queueMessage 將消息發(fā)送給 JS 端(JS 端處理消息邏輯與 Native 端一致,所以上面使用 “responseId” 判斷當(dāng)前消息是否為回調(diào)方法傳遞過來的消息是很容易理解的)

嘗試以消息中的 “handlerName” 從 messageHandlers(上文提到過旺订,是保存 Native 端注冊(cè)過的 handler 的字典)取到對(duì)應(yīng)的 handler block弄企,如果取到則執(zhí)行代碼塊,否則打印錯(cuò)誤日志

Note: 這個(gè)消息處理的方法雖然長区拳,但是邏輯清晰拘领,而且有效的解決了 JS 與 Native 相互調(diào)用的過程中參數(shù)傳遞的問題(包括回調(diào)),此外 JS 端的消息處理邏輯與 Native 端保持一致樱调,實(shí)現(xiàn)了邏輯對(duì)稱院究,非常值得我們學(xué)習(xí)。

WebViewJavascriptBridge_JS - Native 調(diào)用 JS 實(shí)現(xiàn)解讀

Emmmmm...這一章節(jié)主要講 JS 端注入的代碼本涕,即 WebViewJavascriptBridge_JS 中的 JS 源碼业汰。由于我沒做過前段,能力不足菩颖,水平有限样漆,可能有謬誤希望各位讀者發(fā)現(xiàn)的話及時(shí)指正,感激不盡晦闰。預(yù)警放祟,由于 JS 端和上文分析過的 Native 端邏輯對(duì)稱且上文已經(jīng)分析過部分 JS 端的函數(shù)鳍怨,所以下面的 JS 源碼沒有另做拆分,為避免被大段 JS 代碼糊臉不感興趣的同學(xué)可以直接看代碼后面的總結(jié)跪妥。

;(function() {

? ? // window.WebViewJavascriptBridge 校驗(yàn)鞋喇,避免重復(fù)

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 = []; // 發(fā)送消息隊(duì)列

var messageHandlers = {}; // JS 端注冊(cè)的消息處理 handlers 字典(囧眉撵,JS 其實(shí)叫對(duì)象)

// scheme 使用 https 之后通過 host 做匹配

var CUSTOM_PROTOCOL_SCHEME = 'https';

var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';

var responseCallbacks = {}; // JS 端存放回調(diào)的字典

var uniqueId = 1; // 唯一標(biāo)示侦香,用于回調(diào)時(shí)生成 callbackId

var dispatchMessagesWithTimeoutSafety = true; // 默認(rèn)啟用安全時(shí)長

? ? // 通過禁用 AlertBoxSafetyTimeout 來提速網(wǎng)橋消息傳遞

? ? function disableJavscriptAlertBoxSafetyTimeout() {

dispatchMessagesWithTimeoutSafety = false;

}

? ? // 同 iOS 邏輯,注冊(cè) handler 其實(shí)是往 messageHandlers 字典中插入對(duì)應(yīng) name 的 block

function registerHandler(handlerName, handler) {

messageHandlers[handlerName] = handler;

}

// 調(diào)用 iOS handler纽疟,參數(shù)校驗(yàn)之后調(diào)用 _doSend 函數(shù)

function callHandler(handlerName, data, responseCallback) {

?// 如果參數(shù)只有兩個(gè)且第二個(gè)參數(shù)類型為 function罐韩,則表示沒有參數(shù)傳遞,即 data 為空

if (arguments.length == 2 && typeof data == 'function') {

responseCallback = data;

data = null;

}

// 將 handlerName 和 data 作為 msg 對(duì)象參數(shù)調(diào)用 _doSend 函數(shù)

_doSend({ handlerName:handlerName, data:data }, responseCallback);

}

// _doSend 向 Native 端發(fā)送消息

function _doSend(message, responseCallback) {

?// 如有回調(diào)污朽,則設(shè)置 message['callbackId'] 與 responseCallbacks[callbackId]

if (responseCallback) {

var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();

responseCallbacks[callbackId] = responseCallback;

message['callbackId'] = callbackId;

}

// 將 msg 加入 sendMessageQueue 數(shù)組散吵,設(shè)置 messagingIframe.src

sendMessageQueue.push(message);

messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;

}

? ? // 獲取隊(duì)列,在 iOS 端刷新消息隊(duì)列時(shí)會(huì)調(diào)用此函數(shù)

function _fetchQueue() {

?// 內(nèi)部將發(fā)送消息隊(duì)列 sendMessageQueue 轉(zhuǎn)為 JSON 格式并返回

var messageQueueString = JSON.stringify(sendMessageQueue);

sendMessageQueue = [];

return messageQueueString;

}

// iOS 端 _dispatchMessage 函數(shù)會(huì)調(diào)用此函數(shù)

function _handleMessageFromObjC(messageJSON) {

?// 調(diào)度從 Native 端獲取到的消息

? ? ? ? _dispatchMessageFromObjC(messageJSON);

}


? ? // 核心代碼蟆肆,調(diào)度從 Native 端獲取到的消息矾睦,邏輯與 Native 端一致

function _dispatchMessageFromObjC(messageJSON) {

// 判斷有沒有禁用 AlertBoxSafetyTimeout,最終會(huì)調(diào)用 _doDispatchMessageFromObjC 函數(shù)

if (dispatchMessagesWithTimeoutSafety) {

setTimeout(_doDispatchMessageFromObjC);

} else {

?_doDispatchMessageFromObjC();

}

// 解析 msgJSON 得到 msg

function _doDispatchMessageFromObjC() {

var message = JSON.parse(messageJSON);

var messageHandler;

var responseCallback;

// 如果有 responseId,則說明是回調(diào),取對(duì)應(yīng)的 responseCallback 執(zhí)行价卤,之后釋放

if (message.responseId) {

responseCallback = responseCallbacks[message.responseId];

if (!responseCallback) {

return;

}

responseCallback(message.responseData);

delete responseCallbacks[message.responseId];

} else { // 沒有 responseId淘讥,則表示正常的 iOS call handler 調(diào)用 js

// 如 msg 包含 callbackId,說明 iOS 端需要回調(diào),初始化對(duì)應(yīng)的 responseCallback

if (message.callbackId) {

var callbackResponseId = message.callbackId;

responseCallback = function(responseData) {

_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });

};

}

// 從 messageHandlers 拿到對(duì)應(yīng)的 handler 執(zhí)行

var handler = messageHandlers[message.handlerName];

if (!handler) {

?// 如未取到對(duì)應(yīng)的 handler 則打印錯(cuò)誤日志

console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);

} else {

handler(message.data, responseCallback);

}

}

}

}

? ? // messagingIframe 的聲明,類型 iframe,樣式不可見束世,src 設(shè)置

messagingIframe = document.createElement('iframe');

messagingIframe.style.display = 'none';

messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;

// messagingIframe 加入 document.documentElement 中

document.documentElement.appendChild(messagingIframe);

? ? // 注冊(cè) disableJavscriptAlertBoxSafetyTimeout handler,Native 可以通過禁用 AlertBox 的安全時(shí)長來加速橋接消息

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 端邏輯一致床玻,上面的代碼已經(jīng)加入了詳細(xì)的中文注釋毁涉,上文在對(duì)于“WebViewJavascriptBridgeBase - JS 調(diào)用 Native 實(shí)現(xiàn)原理剖析”章節(jié)的分析過程中為了走通整個(gè)調(diào)用的邏輯已經(jīng)對(duì)部分 JS 端代碼進(jìn)行了分析,這里我們簡單的梳理一下 JS 端核心代碼 _doDispatchMessageFromObjC 函數(shù)的邏輯:

將 messageJSON 使用 JSON 解析出來

嘗試取解析到的消息中的 responseId锈死,如果有取到則說明是 Native 端響應(yīng) JS 端之后通過回調(diào)向 JS 端發(fā)出的消息贫堰,用 responseId 取 responseCallbacks 中對(duì)應(yīng)的回調(diào)響應(yīng) block,找到后執(zhí)行該 block 之后刪除

如果沒取到 responseId 則表示這條消息是 Native 端通過 callHandler:data:responseCallback: 正常調(diào)用 JS 注冊(cè)的 handler 發(fā)送過來的消息(這里的正常是針對(duì)回調(diào)而言)

如果當(dāng)前的消息有 callbackId 則表明 Native 端需要 JS 端響應(yīng)本次消息之后回調(diào)反饋待牵,生成一個(gè) responseCallback 作為回調(diào) block (JS 端是 function) 其屏,其內(nèi)部使用 _doSend 方法傳遞一個(gè)帶有 responseId 的消息給 Native 端,表明此條消息是之前的回調(diào)消息

最后按照解析到的消息中 handlerName 從 messageHandlers缨该,即 JS 端注冊(cè)過的 handlers 中找到與名稱對(duì)應(yīng)的處理函數(shù)執(zhí)行偎行,如果沒找到則打印附帶相關(guān)信息的錯(cuò)誤日志

嘛~ 對(duì)比一下 Native 端的核心代碼 flushMessageQueue: 看一下,很容易發(fā)現(xiàn)兩端的處理實(shí)現(xiàn)是邏輯對(duì)稱的。

WebViewJavascriptBridge 的“橋梁美學(xué)”

在總結(jié) WebViewJavascriptBridge 的“橋梁美學(xué)”之前請(qǐng)?jiān)倩仡櫼幌?WebViewJavascriptBridge 的工作流:

JS 端加入 src 為 https://__bridge_loaded__ 的 iframe

Native 端檢測到 Request蛤袒,檢測如果是 __bridge_loaded__ 則通過當(dāng)前的 WebView 組件注入 WebViewJavascriptBridge_JS 代碼

注入代碼成功之后會(huì)加入一個(gè) messagingIframe熄云,其 src 為 https://__wvjb_queue_message__

之后不論是 Native 端還是 JS 端都可以通過 registerHandler 方法注冊(cè)一個(gè)兩端約定好的 HandlerName 的處理,也都可以通過 callHandler 方法通過約定好的 HandlerName 調(diào)用另一端的處理(兩端處理消息的實(shí)現(xiàn)邏輯對(duì)稱)

嘛~ 所以我們很容易列舉出 WebViewJavascriptBridge 所具有的“美學(xué)”:

隱性適配

接口對(duì)等

邏輯對(duì)稱

我們結(jié)合本文展開來說一下上面的“美學(xué)”的具體實(shí)現(xiàn)妙真。

隱性適配

WebViewJavascriptBridge 主要是作為 Mac OS X 和 iOS 端(Native 端)與 JS 端相互通信缴允,互相調(diào)用的橋梁。對(duì)于 Mac OS X 和 iOS 兩種平臺(tái)包含的三種 WebView 功能組件而言珍德,WebViewJavascriptBridge 做了隱性適配练般,即僅用一套代碼即可綁定不同平臺(tái)的 WebView 組件實(shí)現(xiàn)同樣功能的 JS 通信功能,這一點(diǎn)非常方便菱阵。

接口對(duì)等

WebViewJavascriptBridge 對(duì)于 JS 端和 Native 端設(shè)計(jì)了對(duì)等的接口踢俄,不論是 JS 端還是 Native 端缩功,注冊(cè)本端的響應(yīng)處理都是用 registerHandler 接口晴及,調(diào)用另一端(給另一端發(fā)消息)都是用 callHandler 接口。

這樣做是非常合理的嫡锌,因?yàn)椴徽撌?JS 端還是 Native 端虑稼,作為通信的雙方就通信本身而言是處于對(duì)等的地位的。這就好比一座大橋連接兩塊陸地势木,兩地用大橋相互運(yùn)輸貨物并接收資源蛛倦,兩塊陸地在大橋的運(yùn)輸使用過程中邏輯上也是地位對(duì)等的。

邏輯對(duì)稱

WebViewJavascriptBridge 在 JS 端和 Native 端對(duì)發(fā)送過來的消息有著相同邏輯的處理實(shí)現(xiàn)啦桌,如果考慮到收發(fā)雙方的身份則可以把邏輯相同看做邏輯對(duì)稱溯壶。

這種實(shí)現(xiàn)方式依舊非常合理,被橋連接的兩塊大陸在裝貨上橋和下橋卸貨這兩處邏輯上就應(yīng)該是對(duì)稱的甫男。

嘛~ 說到這里就不得不祭出一個(gè)詞來形容 WebViewJavascriptBridge 了且改,這個(gè)詞就是優(yōu)雅(笑)。當(dāng)大家結(jié)合 WebViewJavascriptBridge 源碼閱讀本文之后不難發(fā)現(xiàn)其整個(gè)架構(gòu)和設(shè)計(jì)思想跟現(xiàn)實(shí)橋梁設(shè)計(jì)中很多設(shè)計(jì)思想不謀而合板驳,比如橋一般會(huì)分為左右橋幅又跛,而左右幅橋一般只有一條線路中心線,即一個(gè)前進(jìn)方向若治,用于橋上單一方向的資源傳輸慨蓝,左右橋幅在功能上對(duì)等。

文章總結(jié)

文章系統(tǒng)分析了 WebViewJavascriptBridge 源碼端幼,希望各位讀者能夠在閱讀本文之后對(duì) WebViewJavascriptBridge 的架構(gòu)有一個(gè)整體認(rèn)識(shí)礼烈。

文章對(duì) WebViewJavascriptBridge 在 JS 端和 Native 端的消息處理實(shí)現(xiàn)做了深入剖析,希望可以對(duì)各位讀者這部分源碼的理解提供一些微薄的幫助婆跑。

總結(jié)了 WebViewJavascriptBridge 作為一個(gè) JSBridge 框架所具有的優(yōu)勢济丘,即文中所指的“橋梁美學(xué)”,期望可以對(duì)大家以后自己封裝一個(gè) JSBridge 提供思路,拋磚引玉摹迷。

Emmmmm...不過需要注意的是 WebViewJavascriptBridge 僅僅是作為 JSBridge 層用于提供 JS 和 Native 之間相互傳遞消息的基礎(chǔ)支持的疟赊。如果想要封裝自己項(xiàng)目中的 WebView 組件還需要另外實(shí)現(xiàn) HTTP cookie 注入,自定義 User-Agent峡碉,白名單或者權(quán)限校驗(yàn)等功能近哟,更進(jìn)一步還需要對(duì) WebView 組件進(jìn)行初始化速度,頁面渲染速度以及頁面緩存策略的優(yōu)化鲫寄。我之后也許可能大概應(yīng)該會(huì)寫一篇文章分享一下自己封裝 WebView 組件時(shí)踩到的一些坑以及經(jīng)驗(yàn)吉执,因?yàn)樽约核接邢?..所以也可能不會(huì)寫(笑)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末地来,一起剝皮案震驚了整個(gè)濱河市戳玫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌未斑,老刑警劉巖咕宿,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蜡秽,居然都是意外死亡府阀,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門芽突,熙熙樓的掌柜王于貴愁眉苦臉地迎上來试浙,“玉大人,你說我怎么就攤上這事寞蚌√锇停” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵挟秤,是天一觀的道長壹哺。 經(jīng)常有香客問我,道長煞聪,這世上最難降的妖魔是什么斗躏? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮昔脯,結(jié)果婚禮上啄糙,老公的妹妹穿的比我還像新娘。我一直安慰自己云稚,他們只是感情好隧饼,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著静陈,像睡著了一般燕雁。 火紅的嫁衣襯著肌膚如雪诞丽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天拐格,我揣著相機(jī)與錄音僧免,去河邊找鬼。 笑死捏浊,一個(gè)胖子當(dāng)著我的面吹牛懂衩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播金踪,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼浊洞,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了胡岔?” 一聲冷哼從身側(cè)響起法希,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎靶瘸,沒想到半個(gè)月后苫亦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡奕锌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年著觉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了村生。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惊暴。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖趁桃,靈堂內(nèi)的尸體忽然破棺而出辽话,到底是詐尸還是另有隱情,我是刑警寧澤卫病,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布油啤,位于F島的核電站,受9級(jí)特大地震影響蟀苛,放射性物質(zhì)發(fā)生泄漏益咬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一帜平、第九天 我趴在偏房一處隱蔽的房頂上張望幽告。 院中可真熱鬧,春花似錦裆甩、人聲如沸冗锁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽冻河。三九已至箍邮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間叨叙,已是汗流浹背锭弊。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留擂错,地道東北人廷蓉。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像马昙,于是被迫代替她去往敵國和親桃犬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345