前言
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,這里簡單列一部分(笑):
... & 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ì)寫(笑)。