原文作者:CoderSpr1ngHall
原文地址:https://juejin.im/post/5cecd746e51d45778f076cac
前言
前一篇文章中,我們大致的講述了一下JavaScriptCore這個庫在iOS開發中的應用。在文中最後的階段,我們提到了WebViewJavaScriptBridge這個庫。提到這個庫,可能有一些人就要說了,現在都什麼時代了,誰還會用這個庫啊?全是坑!不錯,早在三年前,這個庫有過一段輝煌的時光,在蘋果除了WKWebView之後,漸漸的使用這個庫的人越來越少,儘管這個庫也是支持了WKWebView的。 但是一個事物的存在就有他的價值,就算使用也不是那麼頻繁了,儘管他有很多的坑。但是對於一個開發者來說,我們應該取其精華去其糟粕,現如今出的很多的交互的bridge依舊是有部分交互邏輯沿用了WebViewJavaScriptBridge的思想。 這裏就不得不提味精大神的一片文章,這篇文章裏面深入淺出的談了談現如今Hybrid開發時常用的一些橋方法。有興趣的可以去關注一下。廢話不多說,那麼我們今天就從源碼開始解析這個庫的使用以及原理。
簡介
簡單的來說,在最開始的UIWebView時,原生跟JS之間的交互一般是兩種方式:
-
Native -> JS:這種方式很簡單,只是是原生調用
stringByEvaluatingJavaScriptFromString:
方法,傳入要執行的JS代碼就可以實現; -
JS -> Native:這種方式是在網頁上面加載一串Custom URL Scheme的URL,然後通過原生去
UIWebView
的代理方法webView:shouldStartLoadWithRequest:navigationType:
中攔截相應的URL做處理。
當然這個其實也就是WebViewJavaScriptBridge的理論核心。但是上面這種實現方法爲什麼沒有人使用呢?原因就是,通過在代理方法裏面攔截,我們就必不可少的要寫很多的if else
的代碼。在項目中的混合插件越來越多的時候,就導致了這個代理方法裏面的邏輯越來越臃腫,越來越難以維護。 那麼WebViewJavaScriptBridge的作用就是以更加優雅的方式,去實現Native與JS之間的互調。讓Native能像調用OC的方法一樣調用JS,同時JS也能像調用JS方法一樣去調用OC。這就在OC和JS中間搭起了一座友誼的橋樑。
使用
這裏使用我就不多說了,直接pod 'WebViewJavascriptBridge'
就可以引入到項目了。 附上源碼地址:WebViewJavaScriptBridge
目錄結構
- WebViewJavaScriptBridgeBase:bridge的核心類,用來初始化以及消息的處理;
- WebViewJavaScriptBridge:判斷WebView的類型,並通過不同的類型進行分發。針對UIWebView和WebView做的一層封裝,主要從來執行JS代碼,以及實現UIWebView和WebView的代理方法,並通過攔截URL來通知WebViewJavaScriptBridgeBase做的相應操作;
- WKWebViewJavaScriptBridge:主要是針對WKWebView做的一些封裝,主要也是執行JS代碼和實現WKWebView的代理方法的。同上面這個類類似;
- WebViewJavaScriptBridge_JS:裏面主要寫了一些JS的方法,JS端與Native”互動“的JS端的方法基 本上都在這個裏面;
主要流程
WebViewJavaScriptBridge參與交互的流程包括三個部分:初始化、JS調用Native、Native調用JS。接下來我們就一一分析其中的過程。
1、初始化
這裏必須要說一下,WebViewJavaScriptBridge的這個設計很巧妙,他在JS端和Native端,都各自初始化了一個WebViewJavaScriptBridge對象,就像是兩邊各自安排了一個”通訊兵“,讓這兩個對象去完成消息的收發工作。同時兩邊還各自維護一個管理相應事件的messageHandlers容器、一個管理回調的callbackId容器。所以這裏的初始化,我們得分爲兩個部分的初始化,一個部分是Native端的初始化,一個是JS端的初始化。這裏我們都以UIWebView爲例子講解,WKWebView其實也是相類似的原理,可以類比一下。
(1)、Native端的初始化
- 首先初始化
WebViewJavaScriptBridge
並且設置好代理
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
[_bridge setWebViewDelegate:self];
- (void) _setupInstance:(WKWebView*)webView {
_webView = webView;
_webView.navigationDelegate = self;
_base = [[WebViewJavascriptBridgeBase alloc] init];
_base.delegate = self;
}
然後其內部初始化了WebViewJavaScriptBridgeBase
類和相關的屬性
- (id)init {
if (self = [super init]) {
self.messageHandlers = [NSMutableDictionary dictionary];
self.startupMessageQueue = [NSMutableArray array];
self.responseCallbacks = [NSMutableDictionary dictionary];
_uniqueId = 0;
}
return self;
}
- 註冊
handler
,這個handler
是提供給JS調用的
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];
註冊其實就是在messageHandlers
這個NSMutableDictionary
裏面保存一下
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
(2)、web view端的初始化
- 當我們通過
loadRequest
加載URL之後,網頁一加載就會執行網頁JS中的bridge的初始化方法setupWebViewJavascriptBridge
函數
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
這裏主要做了兩件事情,一個是保存要執行的一直自定義初始化函數,比如註冊JS中的handler
,第二個就是通過添加一個iframe
加載初始化鏈接https://__bridge_loaded__
。
- Native端會攔截
https://__bridge_loaded__
這個URL
- 在webview中執行本地
WebViewJavaScriptBridge_JS
中的代碼,初始化window.WebViewJavaScriptBridge
對象:首先在JS中創建一個WebViewJavaScriptBridge
對象,設置成window一個屬性,然後定義幾個用於管理消息的全局變量,接着給WebViewJavaScriptBridge
對象定義幾個處理消息的方法和函數,執行Native端startupMessageQueue
中保存的消息,也就是本地JS文件還未加載時就發送了的消息。
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
2、JS調用Native
- JS中調用
callHandler()
方法,發消息給原生
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
複製代碼
然後我們看看callHandler
是怎麼定義的
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
那麼這個_doSend
是幹嘛的?我們順着往下看
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;
}
這下我們清楚了,原來我們在傳入handlerName
和data
被包裝成了一個message
傳入到_doSend
函數,然後生成一個callbackId
,也一道包裝到message
中去。這樣三個數據都被打包成了一個message
傳到Native。 當然爲什麼要傳入一個callbackId
進去呢?這是因爲用於處理原生回調的responseCallback
是一個函數,是不能直接傳給原生的,所以這裏就把這個responseCallback
存到了一個全局的responseCallbacks
對象的屬性裏面去,屬性名就是responseCallback
對應的id。這個地方就是爲了後面Native回調JS時,根據id找到對應的responseCallback
。
在上圖中的最後一步指的是JS會在
iframe
中加載發送消息的URL,此時原生就可以在相應的代理中攔截到這個URL,然後就知道JS端給我傳遞消息了,然後Native端會去調用JS,把sendMessageQueue
中的message
取出來,轉成JSON string的格式。接着原生把JSON string解析成字典,取出相應的data
、callbackId
和handlerName
。最後根據handlerName
去先前的messageHanlers
裏面取出相對應的block(handler)
,然後調用這個block
,data
作爲第一個參數,第二個參數是根據callbackId
創建的responseCallback(block)
,然後原生就可以在block(handler
)中處理接收到的data
以及回調JS了。如果說需要原生給JS回調的話,當這個
responseCallback
被回調的時候,會執行下面的代碼
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
NSMutableDictionary* message = [NSMutableDictionary dictionary];
if (data) {
message[@"data"] = data;
}
if (responseCallback) {
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
self.responseCallbacks[callbackId] = [responseCallback copy];
message[@"callbackId"] = callbackId;
}
if (handlerName) {
message[@"handlerName"] = handlerName;
}
[self _queueMessage:message];
}
這裏就是直接創建了一個message
(NSMutableDictionary)對象,把data
、callbackId
和handlerName
封裝之後轉換成爲JSON string,最後調用WebViewJavascriptBridge._handleMessageFromObjC('%@')
這個方法,把message
傳給JS。
- (void)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
[self _log:@"SEND" json:messageJSON];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}
在JS接收到了這個message
之後,會根據裏面的callbackId
找到之前的responseCallback
,把data
作爲參數,回調這個responseCallback
。
2、Native調用JS
其Native調用JS和上面JS調用Native是有很多的相似之處的。當然,其實也是可以直接通過web view執行JS腳本去實現的。但是WebViewJavaScriptBridge
使用了一套更加規範的調用方式。接下來來介紹一下這種方式。
- Native調用callHandler()方法,把消息發送給JS
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];
複製代碼
這個方法跟JS裏面的這個方法名是一樣的,當然實際的作用其實也是相似的。 在這裏都是將handlerName
、data
和responseCallback
對應的id包裝成一個message
。然後把這個message
對象轉成JSON string。最後在調用WebViewJavascriptBridge._handleMessageFromObjC(messageJSON)
方法把數據給到JS。這裏至於爲什麼也是傳id,其實原理跟上面是一樣的,block也是不能直接傳給JS的,所以這裏把responseCallback
的這個block存到了全局的responseCallbacks
字典裏面去了,key就是responseCallback
對應的id。JS回調Native的時候,就會來這個字典裏面去取對應的block。其實思想都是差不多的。
- JS端拿到了這個message之後,會將它解析成爲JS對象,然後去使用
data
、callbackId
和handlerName
。然後根據handlerName
去messageHandlers
裏面去對應的handler函數,然後去執行這個函數。第一個參數是傳過來的data
,第二個參數就是根據callbackId
創建的responseCallback
的function。這裏就可以在handler
裏面處理接收到的回調了。 - 這裏與前面JS調Native時Native回調JS的處理不太一樣,因爲JS調Native是不能直接調的。但是怎麼去通知Native呢?其實他這裏就是直接走了JS調用Native的流程,就是上面提到的這個流程。不過還是有不同的:
- 一是
message
裏面的東西不一樣了; - 二是Native對message的處理:
- 跟上面JS調用Native不一樣的就是
message
裏面現在不需要你傳一個callbackId
了,因爲這裏本來就是JS回調給Native的,再傳這個,兩邊就一直在回調來回調去了。但是呢,多了一個responseId
,這是因爲Native執行JS回調的時候,會根據這個responseId
從responseCallbacks
中去取對應的block
- 跟上面JS調用Native不一樣的就是
- 一是
```
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
```
* Native在收到JS回調之後,會根據`responseId`找到之前保存的`responseCallback`的block,然後把`message`中的`responseData`(其實就是data)作爲參數回調給這個responseCallback。與JS調用Native不同的其實就是這裏的`responseCallback`只有一個`data`參數了,是沒有用於再次回調JS的block了。
總結
至此,WebViewJavaScriptBridge的整體核心流程就基本上講完了。這樣看看,其實其中的原理還算是簡單,但是很巧妙。兩邊都維護了一個WebViewJavaScriptBridge
的對象,消息都封裝成爲一個message
,然後所有的callback
,都巧妙的轉換成了id。通過直接傳遞id,然後根據id分別去對應的地方去尋找到對應的callback
。這種方式,其實也是值得我們去學習和使用的。 接下來我會繼續的去研究現在比較火爆的JSCore的交互方式,對於Hybrid開發有想法的朋友,歡迎留言跟我交流。