前一篇文章中,咱們大體的講述了一下JavaScriptCore這個庫在iOS開發中的應用。在文中最後的階段,咱們提到了WebViewJavaScriptBridge這個庫。提到這個庫,可能有一些人就要說了,如今都什麼時代了,誰還會用這個庫啊?全是坑!不錯,早在三年前,這個庫有過一段輝煌的時光,在蘋果除了WKWebView以後,漸漸的使用這個庫的人愈來愈少,儘管這個庫也是支持了WKWebView的。 可是一個事物的存在就有他的價值,就算使用也不是那麼頻繁了,儘管他有不少的坑。可是對於一個開發者來講,咱們應該取其精華去其糟粕,現現在出的不少的交互的bridge依舊是有部分交互邏輯沿用了WebViewJavaScriptBridge的思想。 這裏就不得不提味精大神的一片文章,這篇文章裏面深刻淺出的談了談現現在Hybrid開發時經常使用的一些橋方法。有興趣的能夠去關注一下。廢話很少說,那麼咱們今天就從源碼開始解析這個庫的使用以及原理。javascript
簡單的來講,在最開始的UIWebView時,原生跟JS之間的交互通常是兩種方式:java
stringByEvaluatingJavaScriptFromString:
方法,傳入要執行的JS代碼就能夠實現;UIWebView
的代理方法webView:shouldStartLoadWithRequest:navigationType:
中攔截相應的URL作處理。固然這個其實也就是WebViewJavaScriptBridge的理論核心。可是上面這種實現方法爲何沒有人使用呢?緣由就是,經過在代理方法裏面攔截,咱們就必不可少的要寫不少的if else
的代碼。在項目中的混合插件愈來愈多的時候,就致使了這個代理方法裏面的邏輯愈來愈臃腫,愈來愈難以維護。 那麼WebViewJavaScriptBridge的做用就是以更加優雅的方式,去實現Native與JS之間的互調。讓Native能像調用OC的方法同樣調用JS,同時JS也能像調用JS方法同樣去調用OC。這就在OC和JS中間搭起了一座友誼的橋樑。git
這裏使用我就很少說了,直接pod 'WebViewJavascriptBridge'
就能夠引入到項目了。 附上源碼地址:WebViewJavaScriptBridgegithub
WebViewJavaScriptBridge參與交互的流程包括三個部分:初始化、JS調用Native、Native調用JS。接下來咱們就一一分析其中的過程。web
這裏必需要說一下,WebViewJavaScriptBridge的這個設計很巧妙,他在JS端和Native端,都各自初始化了一個WebViewJavaScriptBridge對象,就像是兩邊各自安排了一個」通信兵「,讓這兩個對象去完成消息的收發工做。同時兩邊還各自維護一個管理相應事件的messageHandlers容器、一個管理回調的callbackId容器。因此這裏的初始化,咱們得分爲兩個部分的初始化,一個部分是Native端的初始化,一個是JS端的初始化。這裏咱們都以UIWebView爲例子講解,WKWebView其實也是相相似的原理,能夠類比一下。json
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
類和相關的屬性bash
- (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
裏面保存一下app
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
複製代碼
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__
。函數
https://__bridge_loaded__
這個URLWebViewJavaScriptBridge_JS
中的代碼,初始化window.WebViewJavaScriptBridge
對象:首先在JS中建立一個WebViewJavaScriptBridge
對象,設置成window一個屬性,而後定義幾個用於管理消息的全局變量,接着給WebViewJavaScriptBridge
對象定義幾個處理消息的方法和函數,執行Native端startupMessageQueue
中保存的消息,也就是本地JS文件還未加載時就發送了的消息。window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
複製代碼
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
。
其Native調用JS和上面JS調用Native是有不少的類似之處的。固然,其實也是能夠直接經過web view執行JS腳本去實現的。可是WebViewJavaScriptBridge
使用了一套更加規範的調用方式。接下來來介紹一下這種方式。
[_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。其實思想都是差很少的。
data
、callbackId
和handlerName
。而後根據handlerName
去messageHandlers
裏面去對應的handler函數,而後去執行這個函數。第一個參數是傳過來的data
,第二個參數就是根據callbackId
建立的responseCallback
的function。這裏就能夠在handler
裏面處理接收到的回調了。message
裏面的東西不同了;跟上面JS調用Native不同的就是message
裏面如今不須要你傳一個callbackId
了,由於這裏原本就是JS回調給Native的,再傳這個,兩邊就一直在回調來回調去了。可是呢,多了一個responseId
,這是由於Native執行JS回調的時候,會根據這個responseId
從responseCallbacks
中去取對應的block
```
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
```
複製代碼
responseId
找到以前保存的responseCallback
的block,而後把message
中的responseData
(其實就是data)做爲參數回調給這個responseCallback。與JS調用Native不一樣的其實就是這裏的responseCallback
只有一個data
參數了,是沒有用於再次回調JS的block了。至此,WebViewJavaScriptBridge的總體核心流程就基本上講完了。這樣看看,其實其中的原理還算是簡單,可是很巧妙。兩邊都維護了一個WebViewJavaScriptBridge
的對象,消息都封裝成爲一個message
,而後全部的callback
,都巧妙的轉換成了id。經過直接傳遞id,而後根據id分別去對應的地方去尋找到對應的callback
。這種方式,其實也是值得咱們去學習和使用的。 接下來我會繼續的去研究如今比較火爆的JSCore的交互方式,對於Hybrid開發有想法的朋友,歡迎留言跟我交流。