原文作者:CoderSpr1ngHall
原文地址:https://juejin.im/post/5cecd746e51d45778f076cac
前言
前一篇文章中,我們大致的講述了一下JavaScriptCore這個(gè)庫(kù)在iOS開(kāi)發(fā)中的應(yīng)用夯接。在文中最后的階段焕济,我們提到了WebViewJavaScriptBridge這個(gè)庫(kù)。提到這個(gè)庫(kù)盔几,可能有一些人就要說(shuō)了晴弃,現(xiàn)在都什么時(shí)代了,誰(shuí)還會(huì)用這個(gè)庫(kù)把放摹上鞠?全是坑!不錯(cuò)芯丧,早在三年前芍阎,這個(gè)庫(kù)有過(guò)一段輝煌的時(shí)光,在蘋(píng)果除了WKWebView之后缨恒,漸漸的使用這個(gè)庫(kù)的人越來(lái)越少谴咸,盡管這個(gè)庫(kù)也是支持了WKWebView的轮听。 但是一個(gè)事物的存在就有他的價(jià)值,就算使用也不是那么頻繁了岭佳,盡管他有很多的坑血巍。但是對(duì)于一個(gè)開(kāi)發(fā)者來(lái)說(shuō),我們應(yīng)該取其精華去其糟粕驼唱,現(xiàn)如今出的很多的交互的bridge依舊是有部分交互邏輯沿用了WebViewJavaScriptBridge的思想藻茂。 這里就不得不提味精大神的一片文章驹暑,這篇文章里面深入淺出的談了談現(xiàn)如今Hybrid開(kāi)發(fā)時(shí)常用的一些橋方法玫恳。有興趣的可以去關(guān)注一下。廢話不多說(shuō)优俘,那么我們今天就從源碼開(kāi)始解析這個(gè)庫(kù)的使用以及原理京办。
簡(jiǎn)介
簡(jiǎn)單的來(lái)說(shuō),在最開(kāi)始的UIWebView時(shí)帆焕,原生跟JS之間的交互一般是兩種方式:
-
Native -> JS:這種方式很簡(jiǎn)單惭婿,只是是原生調(diào)用
stringByEvaluatingJavaScriptFromString:
方法,傳入要執(zhí)行的JS代碼就可以實(shí)現(xiàn)叶雹; -
JS -> Native:這種方式是在網(wǎng)頁(yè)上面加載一串Custom URL Scheme的URL财饥,然后通過(guò)原生去
UIWebView
的代理方法webView:shouldStartLoadWithRequest:navigationType:
中攔截相應(yīng)的URL做處理。
當(dāng)然這個(gè)其實(shí)也就是WebViewJavaScriptBridge的理論核心折晦。但是上面這種實(shí)現(xiàn)方法為什么沒(méi)有人使用呢?原因就是钥星,通過(guò)在代理方法里面攔截,我們就必不可少的要寫(xiě)很多的if else
的代碼满着。在項(xiàng)目中的混合插件越來(lái)越多的時(shí)候谦炒,就導(dǎo)致了這個(gè)代理方法里面的邏輯越來(lái)越臃腫,越來(lái)越難以維護(hù)风喇。 那么WebViewJavaScriptBridge的作用就是以更加優(yōu)雅的方式宁改,去實(shí)現(xiàn)Native與JS之間的互調(diào)。讓Native能像調(diào)用OC的方法一樣調(diào)用JS魂莫,同時(shí)JS也能像調(diào)用JS方法一樣去調(diào)用OC还蹲。這就在OC和JS中間搭起了一座友誼的橋梁。
使用
這里使用我就不多說(shuō)了耙考,直接pod 'WebViewJavascriptBridge'
就可以引入到項(xiàng)目了谜喊。 附上源碼地址:WebViewJavaScriptBridge
目錄結(jié)構(gòu)
- WebViewJavaScriptBridgeBase:bridge的核心類,用來(lái)初始化以及消息的處理琳骡;
- WebViewJavaScriptBridge:判斷WebView的類型锅论,并通過(guò)不同的類型進(jìn)行分發(fā)。針對(duì)UIWebView和WebView做的一層封裝楣号,主要從來(lái)執(zhí)行JS代碼最易,以及實(shí)現(xiàn)UIWebView和WebView的代理方法怒坯,并通過(guò)攔截URL來(lái)通知WebViewJavaScriptBridgeBase做的相應(yīng)操作;
- WKWebViewJavaScriptBridge:主要是針對(duì)WKWebView做的一些封裝藻懒,主要也是執(zhí)行JS代碼和實(shí)現(xiàn)WKWebView的代理方法的剔猿。同上面這個(gè)類類似;
- WebViewJavaScriptBridge_JS:里面主要寫(xiě)了一些JS的方法嬉荆,JS端與Native”互動(dòng)“的JS端的方法基 本上都在這個(gè)里面归敬;
主要流程
WebViewJavaScriptBridge參與交互的流程包括三個(gè)部分:初始化、JS調(diào)用Native鄙早、Native調(diào)用JS汪茧。接下來(lái)我們就一一分析其中的過(guò)程。
1限番、初始化
這里必須要說(shuō)一下舱污,WebViewJavaScriptBridge的這個(gè)設(shè)計(jì)很巧妙,他在JS端和Native端弥虐,都各自初始化了一個(gè)WebViewJavaScriptBridge對(duì)象扩灯,就像是兩邊各自安排了一個(gè)”通訊兵“,讓這兩個(gè)對(duì)象去完成消息的收發(fā)工作霜瘪。同時(shí)兩邊還各自維護(hù)一個(gè)管理相應(yīng)事件的messageHandlers容器珠插、一個(gè)管理回調(diào)的callbackId容器。所以這里的初始化颖对,我們得分為兩個(gè)部分的初始化捻撑,一個(gè)部分是Native端的初始化,一個(gè)是JS端的初始化惜互。這里我們都以UIWebView為例子講解布讹,WKWebView其實(shí)也是相類似的原理,可以類比一下训堆。
(1)描验、Native端的初始化
- 首先初始化
WebViewJavaScriptBridge
并且設(shè)置好代理
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
[_bridge setWebViewDelegate:self];
- (void) _setupInstance:(WKWebView*)webView {
_webView = webView;
_webView.navigationDelegate = self;
_base = [[WebViewJavascriptBridgeBase alloc] init];
_base.delegate = self;
}
然后其內(nèi)部初始化了WebViewJavaScriptBridgeBase
類和相關(guān)的屬性
- (id)init {
if (self = [super init]) {
self.messageHandlers = [NSMutableDictionary dictionary];
self.startupMessageQueue = [NSMutableArray array];
self.responseCallbacks = [NSMutableDictionary dictionary];
_uniqueId = 0;
}
return self;
}
- 注冊(cè)
handler
,這個(gè)handler
是提供給JS調(diào)用的
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];
注冊(cè)其實(shí)就是在messageHandlers
這個(gè)NSMutableDictionary
里面保存一下
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
(2)坑鱼、web view端的初始化
- 當(dāng)我們通過(guò)
loadRequest
加載URL之后膘流,網(wǎng)頁(yè)一加載就會(huì)執(zhí)行網(wǎng)頁(yè)JS中的bridge的初始化方法setupWebViewJavascriptBridge
函數(shù)
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)
}
這里主要做了兩件事情,一個(gè)是保存要執(zhí)行的一直自定義初始化函數(shù)鲁沥,比如注冊(cè)JS中的handler
呼股,第二個(gè)就是通過(guò)添加一個(gè)iframe
加載初始化鏈接https://__bridge_loaded__
。
- Native端會(huì)攔截
https://__bridge_loaded__
這個(gè)URL
- 在webview中執(zhí)行本地
WebViewJavaScriptBridge_JS
中的代碼画恰,初始化window.WebViewJavaScriptBridge
對(duì)象:首先在JS中創(chuàng)建一個(gè)WebViewJavaScriptBridge
對(duì)象彭谁,設(shè)置成window一個(gè)屬性,然后定義幾個(gè)用于管理消息的全局變量允扇,接著給WebViewJavaScriptBridge
對(duì)象定義幾個(gè)處理消息的方法和函數(shù)缠局,執(zhí)行Native端startupMessageQueue
中保存的消息则奥,也就是本地JS文件還未加載時(shí)就發(fā)送了的消息。
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
2狭园、JS調(diào)用Native
- JS中調(diào)用
callHandler()
方法读处,發(fā)消息給原生
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
復(fù)制代碼
然后我們看看callHandler
是怎么定義的
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
那么這個(gè)_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;
}
這下我們清楚了唱矛,原來(lái)我們?cè)趥魅?code>handlerName和data
被包裝成了一個(gè)message
傳入到_doSend
函數(shù)罚舱,然后生成一個(gè)callbackId
,也一道包裝到message
中去绎谦。這樣三個(gè)數(shù)據(jù)都被打包成了一個(gè)message
傳到Native管闷。 當(dāng)然為什么要傳入一個(gè)callbackId
進(jìn)去呢?這是因?yàn)橛糜谔幚碓卣{(diào)的responseCallback
是一個(gè)函數(shù)燥滑,是不能直接傳給原生的渐北,所以這里就把這個(gè)responseCallback
存到了一個(gè)全局的responseCallbacks
對(duì)象的屬性里面去,屬性名就是responseCallback
對(duì)應(yīng)的id铭拧。這個(gè)地方就是為了后面Native回調(diào)JS時(shí),根據(jù)id找到對(duì)應(yīng)的responseCallback
恃锉。
在上圖中的最后一步指的是JS會(huì)在
iframe
中加載發(fā)送消息的URL搀菩,此時(shí)原生就可以在相應(yīng)的代理中攔截到這個(gè)URL,然后就知道JS端給我傳遞消息了破托,然后Native端會(huì)去調(diào)用JS肪跋,把sendMessageQueue
中的message
取出來(lái),轉(zhuǎn)成JSON string的格式土砂。接著原生把JSON string解析成字典州既,取出相應(yīng)的data
、callbackId
和handlerName
萝映。最后根據(jù)handlerName
去先前的messageHanlers
里面取出相對(duì)應(yīng)的block(handler)
吴叶,然后調(diào)用這個(gè)block
,data
作為第一個(gè)參數(shù)序臂,第二個(gè)參數(shù)是根據(jù)callbackId
創(chuàng)建的responseCallback(block)
蚌卤,然后原生就可以在block(handler
)中處理接收到的data
以及回調(diào)JS了。如果說(shuō)需要原生給JS回調(diào)的話奥秆,當(dāng)這個(gè)
responseCallback
被回調(diào)的時(shí)候逊彭,會(huì)執(zhí)行下面的代碼
- (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];
}
這里就是直接創(chuàng)建了一個(gè)message
(NSMutableDictionary)對(duì)象,把data
构订、callbackId
和handlerName
封裝之后轉(zhuǎn)換成為JSON string侮叮,最后調(diào)用WebViewJavascriptBridge._handleMessageFromObjC('%@')
這個(gè)方法,把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接收到了這個(gè)message
之后囊榜,會(huì)根據(jù)里面的callbackId
找到之前的responseCallback
谷异,把data
作為參數(shù),回調(diào)這個(gè)responseCallback
锦聊。
2歹嘹、Native調(diào)用JS
其Native調(diào)用JS和上面JS調(diào)用Native是有很多的相似之處的。當(dāng)然孔庭,其實(shí)也是可以直接通過(guò)web view執(zhí)行JS腳本去實(shí)現(xiàn)的尺上。但是WebViewJavaScriptBridge
使用了一套更加規(guī)范的調(diào)用方式。接下來(lái)來(lái)介紹一下這種方式圆到。
- Native調(diào)用callHandler()方法怎抛,把消息發(fā)送給JS
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];
復(fù)制代碼
這個(gè)方法跟JS里面的這個(gè)方法名是一樣的,當(dāng)然實(shí)際的作用其實(shí)也是相似的芽淡。 在這里都是將handlerName
马绝、data
和responseCallback
對(duì)應(yīng)的id包裝成一個(gè)message
。然后把這個(gè)message
對(duì)象轉(zhuǎn)成JSON string挣菲。最后在調(diào)用WebViewJavascriptBridge._handleMessageFromObjC(messageJSON)
方法把數(shù)據(jù)給到JS富稻。這里至于為什么也是傳id,其實(shí)原理跟上面是一樣的白胀,block也是不能直接傳給JS的椭赋,所以這里把responseCallback
的這個(gè)block存到了全局的responseCallbacks
字典里面去了,key就是responseCallback
對(duì)應(yīng)的id或杠。JS回調(diào)Native的時(shí)候哪怔,就會(huì)來(lái)這個(gè)字典里面去取對(duì)應(yīng)的block。其實(shí)思想都是差不多的向抢。
- JS端拿到了這個(gè)message之后认境,會(huì)將它解析成為JS對(duì)象,然后去使用
data
挟鸠、callbackId
和handlerName
叉信。然后根據(jù)handlerName
去messageHandlers
里面去對(duì)應(yīng)的handler函數(shù),然后去執(zhí)行這個(gè)函數(shù)兄猩。第一個(gè)參數(shù)是傳過(guò)來(lái)的data
茉盏,第二個(gè)參數(shù)就是根據(jù)callbackId
創(chuàng)建的responseCallback
的function。這里就可以在handler
里面處理接收到的回調(diào)了枢冤。 - 這里與前面JS調(diào)Native時(shí)Native回調(diào)JS的處理不太一樣鸠姨,因?yàn)镴S調(diào)Native是不能直接調(diào)的。但是怎么去通知Native呢淹真?其實(shí)他這里就是直接走了JS調(diào)用Native的流程讶迁,就是上面提到的這個(gè)流程。不過(guò)還是有不同的:
- 一是
message
里面的東西不一樣了核蘸; - 二是Native對(duì)message的處理:
- 跟上面JS調(diào)用Native不一樣的就是
message
里面現(xiàn)在不需要你傳一個(gè)callbackId
了巍糯,因?yàn)檫@里本來(lái)就是JS回調(diào)給Native的啸驯,再傳這個(gè),兩邊就一直在回調(diào)來(lái)回調(diào)去了祟峦。但是呢罚斗,多了一個(gè)responseId
,這是因?yàn)镹ative執(zhí)行JS回調(diào)的時(shí)候宅楞,會(huì)根據(jù)這個(gè)responseId
從responseCallbacks
中去取對(duì)應(yīng)的block
- 跟上面JS調(diào)用Native不一樣的就是
- 一是
```
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
```
* Native在收到JS回調(diào)之后针姿,會(huì)根據(jù)`responseId`找到之前保存的`responseCallback`的block,然后把`message`中的`responseData`(其實(shí)就是data)作為參數(shù)回調(diào)給這個(gè)responseCallback厌衙。與JS調(diào)用Native不同的其實(shí)就是這里的`responseCallback`只有一個(gè)`data`參數(shù)了距淫,是沒(méi)有用于再次回調(diào)JS的block了。
總結(jié)
至此婶希,WebViewJavaScriptBridge的整體核心流程就基本上講完了榕暇。這樣看看,其實(shí)其中的原理還算是簡(jiǎn)單喻杈,但是很巧妙彤枢。兩邊都維護(hù)了一個(gè)WebViewJavaScriptBridge
的對(duì)象,消息都封裝成為一個(gè)message
奕塑,然后所有的callback
堂污,都巧妙的轉(zhuǎn)換成了id。通過(guò)直接傳遞id龄砰,然后根據(jù)id分別去對(duì)應(yīng)的地方去尋找到對(duì)應(yīng)的callback
。這種方式讨衣,其實(shí)也是值得我們?nèi)W(xué)習(xí)和使用的换棚。 接下來(lái)我會(huì)繼續(xù)的去研究現(xiàn)在比較火爆的JSCore的交互方式,對(duì)于Hybrid開(kāi)發(fā)有想法的朋友反镇,歡迎留言跟我交流固蚤。