本篇為大家介紹一個(gè)優(yōu)秀的開源小項(xiàng)目:WebViewJavascriptBridge谎倔。
它優(yōu)雅地實(shí)現(xiàn)了在使用UIWebView時(shí)JS與ios 的ObjC nativecode之間的互調(diào)褂策,支持消息發(fā)送、接收粪般、消息處理器的注冊(cè)與調(diào)用以及設(shè)置消息處理的回調(diào)。
就像項(xiàng)目的名稱一樣恕刘,它是連接UIWebView和Javascript的bridge桑嘶。在加入這個(gè)項(xiàng)目之后屈尼,他們之間的交互處理方式變得很友好册着。
在native code中跟UIWebView中的js交互的時(shí)候,像下面這樣:
[cpp]view plaincopy
//發(fā)送一條消息給UI端并定義回調(diào)處理邏輯
[_bridge?send:@"A?string?sent?from?ObjC?before?Webview?has?loaded."responseCallback:^(id?error,?id?responseData)?{
if(error)?{?NSLog(@"Uh?oh?-?I?got?an?error:?%@",?error);?}
NSLog(@"objc?got?response!?%@?%@",?error,?responseData);
}];
而在UIWebView中的js跟native code交互的時(shí)候也變得很簡(jiǎn)潔脾歧,比如在調(diào)用處理器的時(shí)候甲捏,就可以定義回調(diào)處理邏輯:
[javascript]view plaincopy
//調(diào)用名為testObjcCallback的native端處理器,并傳遞參數(shù)鞭执,同時(shí)設(shè)置回調(diào)處理邏輯
bridge.callHandler('testObjcCallback',?{'foo':'bar'},function(response)?{
??log('Got?response?from?testObjcCallback',?response)
})
一起來(lái)看看它的實(shí)現(xiàn)吧摊鸡,它總共就包含了三個(gè)文件:
[plain]view plaincopy
WebViewJavascriptBridge.h
WebViewJavascriptBridge.m
WebViewJavascriptBridge.js.txt
它們是以如下的模式進(jìn)行交互的:
很明顯:WebViewJavascriptBridge.js.txt主要用于銜接UIWebView中的web page绽媒,而WebViewJavascriptBridge.h/m則主要用于與ObjC的native code打交道。他們作為一個(gè)整體免猾,其實(shí)起到了一個(gè)“橋梁”的作用是辕,這三個(gè)文件封裝了他們具體的交互處理方式,只開放出一些對(duì)外的涉及到業(yè)務(wù)處理的API猎提,因此你在需要UIWebView與Native code交互的時(shí)候获三,引入該庫(kù),則無(wú)需考慮太多的交互上的問(wèn)題锨苏。整個(gè)的Bridge對(duì)你來(lái)說(shuō)都是透明的疙教,你感覺編程的時(shí)候,就像是web編程的前端和后端一樣清晰伞租。
簡(jiǎn)單地羅列一下它可以實(shí)現(xiàn)哪些功能吧:
出于表達(dá)上的需要贞谓,對(duì)于UIWebView相關(guān)的我就稱之為UI端,而objc那端的處理代碼稱之為Native端葵诈。
【1】UI端
(1)???UI端在初始化時(shí)支持設(shè)置消息的默認(rèn)處理器(這里的消息指的是從Native端接收到的消息)
(2)???從UI端向Native端發(fā)送消息裸弦,并支持對(duì)于Native端響應(yīng)后的回調(diào)處理的定義
(3)???UI端調(diào)用Native定義的處理器,并支持Native端響應(yīng)后的回調(diào)處理定義
(4)???UI端注冊(cè)處理器(供Native端調(diào)用)作喘,并支持給Native端響應(yīng)處理邏輯的定義
【2】 Native端
(1)???Native端在初始化時(shí)支持設(shè)置消息的默認(rèn)處理器(這里的消息指的是從UI端發(fā)送過(guò)來(lái)的消息)
(2)???從Native端向UI端發(fā)送消息理疙,并支持對(duì)于UI端響應(yīng)后的回調(diào)處理邏輯的定義
(3)???Native端調(diào)用UI端定義的處理器,并支持UI端給出響應(yīng)后在Native端的回調(diào)處理邏輯的定義
(4)???Native端注冊(cè)處理器(供UI端調(diào)用)泞坦,并支持給UI端響應(yīng)處理邏輯的定義
UI端以及Native端完全是對(duì)等的兩端窖贤,實(shí)現(xiàn)也是對(duì)等的。一段是消息的發(fā)送端贰锁,另一段就是接收端赃梧。這里為引起混淆,需要解釋一下我這里使用的“響應(yīng)”豌熄、“回調(diào)”在這個(gè)上下文中的定義:
(1)???響應(yīng):接收端給予發(fā)送端的應(yīng)答
(2)???回調(diào):發(fā)送端收到接收端的應(yīng)答之后在接收端調(diào)用的處理邏輯
下面來(lái)分析一下源碼:
WebViewJavascriptBridge.js.txt:
主要完成了如下工作:
(1) 創(chuàng)建了一個(gè)用于發(fā)送消息的iFrame(通過(guò)創(chuàng)建一個(gè)隱藏的ifrmae槽奕,并設(shè)置它的URL 來(lái)發(fā)出一個(gè)請(qǐng)求,從而觸發(fā)UIWebView的shouldStartLoadWithRequest回調(diào)協(xié)議)
(2) 創(chuàng)建了一個(gè)核心對(duì)象WebViewJavascriptBridge房轿,并給它定義了幾個(gè)方法,這些方法大部分是公開的API方法
(3) 創(chuàng)建了一個(gè)事件:WebViewJavascriptBridgeReady所森,并dispatch(觸發(fā))了它囱持。
對(duì)于(1),相應(yīng)的代碼如下:
[javascript]view plaincopy
/*
*創(chuàng)建一個(gè)iFrame焕济,設(shè)置隱藏并加入到DOM中
*/
function_createQueueReadyIframe(doc)?{
messagingIframe?=?doc.createElement('iframe')
messagingIframe.style.display?='none'
doc.documentElement.appendChild(messagingIframe)
}
對(duì)于(2)中的WebViewJavascriptBridge纷妆,其對(duì)象擁有如下方法:
[plain]view plaincopy
window.WebViewJavascriptBridge?=?{
init:?init,
send:?send,
registerHandler:?registerHandler,
callHandler:?callHandler,
_fetchQueue:?_fetchQueue,
_handleMessageFromObjC:?_handleMessageFromObjC
}
方法的實(shí)現(xiàn):
[javascript]view plaincopy
??/*
*初始化方法,注入默認(rèn)的消息處理器
*默認(rèn)的消息處理器用于在處理來(lái)自objc的消息時(shí)晴弃,如果該消息沒有設(shè)置處理器掩幢,則采用默認(rèn)處理器處理
*/
functioninit(messageHandler)?{
if(WebViewJavascriptBridge._messageHandler)?{thrownewError('WebViewJavascriptBridge.init?called?twice')?}
WebViewJavascriptBridge._messageHandler?=?messageHandler
varreceivedMessages?=?receiveMessageQueue
receiveMessageQueue?=null
//如果接收隊(duì)列有消息逊拍,則處理
for(vari=0;?i
_dispatchMessageFromObjC(receivedMessages[i])
}
}
??/*
*發(fā)送消息并設(shè)置回調(diào)
*/
functionsend(data,?responseCallback)?{
_doSend({?data:data?},?responseCallback)
}
/*
*注冊(cè)消息處理器
*/
functionregisterHandler(handlerName,?handler)?{
messageHandlers[handlerName]?=?handler
}
/*
*調(diào)用處理器并設(shè)置回調(diào)
*/
functioncallHandler(handlerName,?data,?responseCallback)?{
_doSend({?data:data,?handlerName:handlerName?},?responseCallback)
}
涉及到的兩個(gè)內(nèi)部方法:
[javascript]view plaincopy
??/*
*內(nèi)部方法:消息的發(fā)送
*/
function_doSend(message,?responseCallback)?{
//如果定義了回調(diào)
if(responseCallback)?{
//為回調(diào)對(duì)象產(chǎn)生唯一標(biāo)識(shí)
varcallbackId?='js_cb_'+(uniqueId++)
//并存儲(chǔ)到一個(gè)集合對(duì)象里
responseCallbacks[callbackId]?=?responseCallback
//新增一個(gè)key-value對(duì)-?'callbackId':callbackId
message['callbackId']?=?callbackId
}
sendMessageQueue.push(JSON.stringify(message))
messagingIframe.src?=?CUSTOM_PROTOCOL_SCHEME?+'://'+?QUEUE_HAS_MESSAGE
}
??/*
*內(nèi)部方法:處理來(lái)自objc的消息
*/
function_dispatchMessageFromObjC(messageJSON)?{
setTimeout(function_timeoutDispatchMessageFromObjC()?{
varmessage?=?JSON.parse(messageJSON)
varmessageHandler
if(message.responseId)?{
//取出回調(diào)函數(shù)對(duì)象并執(zhí)行
varresponseCallback?=?responseCallbacks[message.responseId]
responseCallback(message.error,?message.responseData)
deleteresponseCallbacks[message.responseId]
}else{
varresponse
if(message.callbackId)?{
varcallbackResponseId?=?message.callbackId
response?=?{
respondWith:function(responseData)?{
_doSend({?responseId:callbackResponseId,?responseData:responseData?})
},
respondWithError:function(error)?{
_doSend({?responseId:callbackResponseId,?error:error?})
}
}
}
varhandler?=?WebViewJavascriptBridge._messageHandler
//如果消息中已包含消息處理器,則使用該處理器际邻;否則使用默認(rèn)處理器
if(message.handlerName)?{
handler?=?messageHandlers[message.handlerName]
}
try{
handler(message.data,?response)
}catch(exception)?{
console.log("WebViewJavascriptBridge:?WARNING:?javascript?handler?threw.",?message,?exception)
}
}
})
}
還有兩個(gè)js方法是供native端直接調(diào)用的方法(它們本身也是為native端服務(wù)的):
[javascript]view plaincopy
??/*
*獲得隊(duì)列芯丧,將隊(duì)列中的每個(gè)元素用分隔符分隔之后連成一個(gè)字符串【native端調(diào)用】
*/
function_fetchQueue()?{
varmessageQueueString?=?sendMessageQueue.join(MESSAGE_SEPARATOR)
sendMessageQueue?=?[]
returnmessageQueueString
}
??/*
*處理來(lái)自O(shè)bjC的消息【native端調(diào)用】
*/
function_handleMessageFromObjC(messageJSON)?{
//如果接收隊(duì)列對(duì)象存在則入隊(duì)該消息,否則直接處理
if(receiveMessageQueue)?{
receiveMessageQueue.push(messageJSON)
}else{
_dispatchMessageFromObjC(messageJSON)
}
}
最后還有一段代碼就是世曾,定義一個(gè)事件并觸發(fā)缨恒,同時(shí)設(shè)置設(shè)置上面定義的WebViewJavascriptBridge對(duì)象為事件的一個(gè)屬性:
[javascript]view plaincopy
??vardoc?=?document
_createQueueReadyIframe(doc)
//創(chuàng)建并實(shí)例化一個(gè)事件對(duì)象
varreadyEvent?=?doc.createEvent('Events')
readyEvent.initEvent('WebViewJavascriptBridgeReady')
readyEvent.bridge?=?WebViewJavascriptBridge
//觸發(fā)事件
doc.dispatchEvent(readyEvent)
其實(shí)大致跟上面的類似,只是因?yàn)檎Z(yǔ)法不同(所以我上面才說(shuō)兩端是對(duì)等的):
WebViewJavascriptBridge.h/.m
它其實(shí)可以看作UIWebView的Controller轮听,實(shí)現(xiàn)了UIWebViewDelegate協(xié)議:
[cpp]view plaincopy
@interface?WebViewJavascriptBridge?:?NSObject?
+?(id)bridgeForWebView:(UIWebView*)webView?handler:(WVJBHandler)handler;
+?(id)bridgeForWebView:(UIWebView*)webView?webViewDelegate:(id?)webViewDelegate?handler:(WVJBHandler)handler;
+?(void)enableLogging;
-?(void)send:(id)message;
-?(void)send:(id)message?responseCallback:(WVJBResponseCallback)responseCallback;
-?(void)registerHandler:(NSString*)handlerName?handler:(WVJBHandler)handler;
-?(void)callHandler:(NSString*)handlerName;
-?(void)callHandler:(NSString*)handlerName?data:(id)data;
-?(void)callHandler:(NSString*)handlerName?data:(id)data?responseCallback:(WVJBResponseCallback)responseCallback;
@end
方法的實(shí)現(xiàn)其實(shí)是跟前面類似的骗露,這里我們只看一下UIWebView的一個(gè)協(xié)議方法
shouldStartLoadWithRequest:
[cpp]view plaincopy
-?(BOOL)webView:(UIWebView?*)webView?shouldStartLoadWithRequest:(NSURLRequest?*)request?navigationType:(UIWebViewNavigationType)navigationType?{
if(webView?!=?_webView)?{returnYES;?}
NSURL?*url?=?[request?URL];
if([[url?scheme]?isEqualToString:CUSTOM_PROTOCOL_SCHEME])?{
//隊(duì)列中有數(shù)據(jù)
if([[url?host]?isEqualToString:QUEUE_HAS_MESSAGE])?{
//刷出隊(duì)列中數(shù)據(jù)
[self?_flushMessageQueue];
}else{
NSLog(@"WebViewJavascriptBridge:?WARNING:?Received?unknown?WebViewJavascriptBridge?command?%@://%@",?CUSTOM_PROTOCOL_SCHEME,?[url?path]);
}
returnNO;
}elseif(self.webViewDelegate)?{
return[self.webViewDelegate?webView:webView?shouldStartLoadWithRequest:request?navigationType:navigationType];
}else{
returnYES;
}
}
[javascript]view plaincopy
??//給WebViewJavascriptBridgeReady事件注冊(cè)一個(gè)Listener
document.addEventListener('WebViewJavascriptBridgeReady',?onBridgeReady,false)
??//事件的響應(yīng)處理
functiononBridgeReady(event)?{
varbridge?=?event.bridge
varuniqueId?=?1
??//日志記錄
functionlog(message,?data)?{
varlog?=?document.getElementById('log')
varel?=?document.createElement('div')
el.className?='logLine'
el.innerHTML?=?uniqueId++?+'.?'+?message?+?(data??':?'+?JSON.stringify(data)?:'')
if(log.children.length)?{?log.insertBefore(el,?log.children[0])?}
else{?log.appendChild(el)?}
}
??//初始化操作,并定義默認(rèn)的消息處理邏輯
bridge.init(function(message)?{
log('JS?got?a?message',?message)
})
??//注冊(cè)一個(gè)名為testJavascriptHandler的處理器血巍,并定義用于響應(yīng)的處理邏輯
bridge.registerHandler('testJavascriptHandler',function(data,?response)?{
log('JS?handler?testJavascriptHandler?was?called',?data)
response.respondWith({'Javascript?Says':'Right?back?atcha!'})
})
??//創(chuàng)建一個(gè)發(fā)送消息給native端的按鈕
varbutton?=?document.getElementById('buttons').appendChild(document.createElement('button'))
button.innerHTML?='Send?message?to?ObjC'
button.ontouchstart?=function(e)?{
e.preventDefault()
??????//發(fā)送消息
bridge.send('Hello?from?JS?button')
}
document.body.appendChild(document.createElement('br'))
??//創(chuàng)建一個(gè)用于調(diào)用native端處理器的按鈕
varcallbackButton?=?document.getElementById('buttons').appendChild(document.createElement('button'))
callbackButton.innerHTML?='Fire?testObjcCallback'
callbackButton.ontouchstart?=function(e)?{
e.preventDefault()
log("Calling?handler?testObjcCallback")
//調(diào)用名為testObjcCallback的native端處理器萧锉,并傳遞參數(shù),同時(shí)設(shè)置回調(diào)處理邏輯
bridge.callHandler('testObjcCallback',?{'foo':'bar'},function(response)?{
log('Got?response?from?testObjcCallback',?response)
})
}
}
[cpp]view plaincopy
//實(shí)例化一個(gè)webview并加入到window中去
UIWebView*?webView?=?[[UIWebView?alloc]?initWithFrame:self.window.bounds];
[self.window?addSubview:webView];
//啟用日志記錄
[WebViewJavascriptBridge?enableLogging];
//實(shí)例化WebViewJavascriptBridge并定義native端的默認(rèn)消息處理器
_bridge?=?[WebViewJavascriptBridge?bridgeForWebView:webView?handler:^(id?data,?WVJBResponse?*response)?{
NSLog(@"ObjC?received?message?from?JS:?%@",?data);
UIAlertView?*alert?=?[[UIAlertView?alloc]?initWithTitle:@"ObjC?got?message?from?Javascript:"message:data?delegate:nil?cancelButtonTitle:@"OK"otherButtonTitles:nil];
[alert?show];
}];
//注冊(cè)一個(gè)供UI端調(diào)用的名為testObjcCallback的處理器述寡,并定義用于響應(yīng)的處理邏輯
[_bridge?registerHandler:@"testObjcCallback"handler:^(id?data,?WVJBResponse?*response)?{
NSLog(@"testObjcCallback?called:?%@",?data);
[response?respondWith:@"Response?from?testObjcCallback"];
}];
//發(fā)送一條消息給UI端并定義回調(diào)處理邏輯
[_bridge?send:@"A?string?sent?from?ObjC?before?Webview?has?loaded."responseCallback:^(id?error,?id?responseData)?{
if(error)?{?NSLog(@"Uh?oh?-?I?got?an?error:?%@",?error);?}
NSLog(@"objc?got?response!?%@?%@",?error,?responseData);
}];
//調(diào)用一個(gè)在UI端定義的名為testJavascriptHandler的處理器柿隙,沒有定義回調(diào)
[_bridge?callHandler:@"testJavascriptHandler"data:[NSDictionary?dictionaryWithObject:@"before?ready"forKey:@"foo"]];
[self?renderButtons:webView];
[self?loadExamplePage:webView];
//單純發(fā)送一條消息給UI端
[_bridge?send:@"A?string?sent?from?ObjC?after?Webview?has?loaded."];
項(xiàng)目運(yùn)行截圖: