簡介
本篇爲大家介紹一個優秀的開源小項目:WebViewJavascriptBridge。
它優雅地實現了在使用UIWebView時JS與ios 的ObjC nativecode之間的互調,支持消息發送、接收、消息處理器的註冊與調用以及設置消息處理的回調。
就像項目的名稱一樣,它是連接UIWebView和Javascript的bridge。在加入這個項目之後,他們之間的交互處理方式變得很友好。
在native code中跟UIWebView中的js交互的時候,像下面這樣:
-
-
[_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交互的時候也變得很簡潔,比如在調用處理器的時候,就可以定義回調處理邏輯:
-
-
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
-
<span style="white-space:pre"> </span>log('Got response from testObjcCallback', response)
-
})
一起來看看它的實現吧,它總共就包含了三個文件:
-
WebViewJavascriptBridge.h
-
WebViewJavascriptBridge.m
-
WebViewJavascriptBridge.js.txt
它們是以如下的模式進行交互的:
很明顯:WebViewJavascriptBridge.js.txt主要用於銜接UIWebView中的web page,而WebViewJavascriptBridge.h/m則主要用於與ObjC的native code打交道。他們作爲一個整體,其實起到了一個“橋樑”的作用,這三個文件封裝了他們具體的交互處理方式,只開放出一些對外的涉及到業務處理的API,因此你在需要UIWebView與Native code交互的時候,引入該庫,則無需考慮太多的交互上的問題。整個的Bridge對你來說都是透明的,你感覺編程的時候,就像是web編程的前端和後端一樣清晰。
簡單地羅列一下它可以實現哪些功能吧:
出於表達上的需要,對於UIWebView相關的我就稱之爲UI端,而objc那端的處理代碼稱之爲Native端。
【1】UI端
(1) UI端在初始化時支持設置消息的默認處理器(這裏的消息指的是從Native端接收到的消息)
(2) 從UI端向Native端發送消息,並支持對於Native端響應後的回調處理的定義
(3) UI端調用Native定義的處理器,並支持Native端響應後的回調處理定義
(4) UI端註冊處理器(供Native端調用),並支持給Native端響應處理邏輯的定義
【2】 Native端
(1) Native端在初始化時支持設置消息的默認處理器(這裏的消息指的是從UI端發送過來的消息)
(2) 從Native端向UI端發送消息,並支持對於UI端響應後的回調處理邏輯的定義
(3) Native端調用UI端定義的處理器,並支持UI端給出響應後在Native端的回調處理邏輯的定義
(4) Native端註冊處理器(供UI端調用),並支持給UI端響應處理邏輯的定義
UI端以及Native端完全是對等的兩端,實現也是對等的。一段是消息的發送端,另一段就是接收端。這裏爲引起混淆,需要解釋一下我這裏使用的“響應”、“回調”在這個上下文中的定義:
(1) 響應:接收端給予發送端的應答
(2) 回調:發送端收到接收端的應答之後在接收端調用的處理邏輯
下面來分析一下源碼:
WebViewJavascriptBridge.js.txt:
主要完成了如下工作:
(1) 創建了一個用於發送消息的iFrame(通過創建一個隱藏的ifrmae,並設置它的URL 來發出一個請求,從而觸發UIWebView的shouldStartLoadWithRequest回調協議)
(2) 創建了一個核心對象WebViewJavascriptBridge,並給它定義了幾個方法,這些方法大部分是公開的API方法
(3) 創建了一個事件:WebViewJavascriptBridgeReady,並dispatch(觸發)了它。
代碼解讀
UI端實現
對於(1),相應的代碼如下:
-
-
-
-
function _createQueueReadyIframe(doc) {
-
messagingIframe = doc.createElement('iframe')
-
messagingIframe.style.display = 'none'
-
doc.documentElement.appendChild(messagingIframe)
-
}
對於(2)中的WebViewJavascriptBridge,其對象擁有如下方法:
-
window.WebViewJavascriptBridge = {
-
init: init,
-
send: send,
-
registerHandler: registerHandler,
-
callHandler: callHandler,
-
_fetchQueue: _fetchQueue,
-
_handleMessageFromObjC: _handleMessageFromObjC
-
}
方法的實現:
-
<span style="white-space:pre"> </span>
-
-
-
-
function init(messageHandler) {
-
if (WebViewJavascriptBridge._messageHandler) { throw new Error('WebViewJavascriptBridge.init called twice') }
-
WebViewJavascriptBridge._messageHandler = messageHandler
-
var receivedMessages = receiveMessageQueue
-
receiveMessageQueue = null
-
-
for (var i=0; i<receivedMessages.length; i++) {
-
_dispatchMessageFromObjC(receivedMessages[i])
-
}
-
}
-
-
-
<span style="white-space:pre"> </span>
-
-
-
function send(data, responseCallback) {
-
_doSend({ data:data }, responseCallback)
-
}
-
-
-
-
-
function registerHandler(handlerName, handler) {
-
messageHandlers[handlerName] = handler
-
}
-
-
-
-
-
-
function callHandler(handlerName, data, responseCallback) {
-
_doSend({ data:data, handlerName:handlerName }, responseCallback)
-
}
涉及到的兩個內部方法:
-
<span style="white-space:pre"> </span>
-
-
-
function _doSend(message, responseCallback) {
-
-
if (responseCallback) {
-
-
var callbackId = 'js_cb_'+(uniqueId++)
-
-
responseCallbacks[callbackId] = responseCallback
-
-
message['callbackId'] = callbackId
-
}
-
sendMessageQueue.push(JSON.stringify(message))
-
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE
-
}
-
-
-
<span style="white-space:pre"> </span>
-
-
-
function _dispatchMessageFromObjC(messageJSON) {
-
setTimeout(function _timeoutDispatchMessageFromObjC() {
-
var message = JSON.parse(messageJSON)
-
var messageHandler
-
-
if (message.responseId) {
-
-
var responseCallback = responseCallbacks[message.responseId]
-
responseCallback(message.error, message.responseData)
-
delete responseCallbacks[message.responseId]
-
} else {
-
var response
-
if (message.callbackId) {
-
var callbackResponseId = message.callbackId
-
response = {
-
respondWith: function(responseData) {
-
_doSend({ responseId:callbackResponseId, responseData:responseData })
-
},
-
respondWithError: function(error) {
-
_doSend({ responseId:callbackResponseId, error:error })
-
}
-
}
-
}
-
-
var handler = WebViewJavascriptBridge._messageHandler
-
-
if (message.handlerName) {
-
handler = messageHandlers[message.handlerName]
-
}
-
-
try {
-
handler(message.data, response)
-
} catch(exception) {
-
console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception)
-
}
-
}
-
})
-
}
還有兩個js方法是供native端直接調用的方法(它們本身也是爲native端服務的):
-
<span style="white-space:pre"> </span>
-
-
-
function _fetchQueue() {
-
var messageQueueString = sendMessageQueue.join(MESSAGE_SEPARATOR)
-
sendMessageQueue = []
-
return messageQueueString
-
}
-
-
-
<span style="white-space:pre"> </span>
-
-
-
function _handleMessageFromObjC(messageJSON) {
-
-
if (receiveMessageQueue) {
-
receiveMessageQueue.push(messageJSON)
-
} else {
-
_dispatchMessageFromObjC(messageJSON)
-
}
-
}
最後還有一段代碼就是,定義一個事件並觸發,同時設置設置上面定義的WebViewJavascriptBridge對象爲事件的一個屬性:
-
<span style="white-space:pre"> </span>var doc = document
-
_createQueueReadyIframe(doc)
-
-
var readyEvent = doc.createEvent('Events')
-
readyEvent.initEvent('WebViewJavascriptBridgeReady')
-
readyEvent.bridge = WebViewJavascriptBridge
-
-
doc.dispatchEvent(readyEvent)
Native端實現
其實大致跟上面的類似,只是因爲語法不同(所以我上面才說兩端是對等的):
WebViewJavascriptBridge.h/.m
它其實可以看作UIWebView的Controller,實現了UIWebViewDelegate協議:
-
@interface WebViewJavascriptBridge : NSObject <UIWebViewDelegate>
-
+ (id)bridgeForWebView:(UIWebView*)webView handler:(WVJBHandler)handler;
-
+ (id)bridgeForWebView:(UIWebView*)webView webViewDelegate:(id <UIWebViewDelegate>)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
方法的實現其實是跟前面類似的,這裏我們只看一下UIWebView的一個協議方法
shouldStartLoadWithRequest:
-
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
-
if (webView != _webView) { return YES; }
-
NSURL *url = [request URL];
-
if ([[url scheme] isEqualToString:CUSTOM_PROTOCOL_SCHEME]) {
-
-
if ([[url host] isEqualToString:QUEUE_HAS_MESSAGE]) {
-
-
[self _flushMessageQueue];
-
} else {
-
NSLog(@"WebViewJavascriptBridge: WARNING: Received unknown WebViewJavascriptBridge command %@://%@", CUSTOM_PROTOCOL_SCHEME, [url path]);
-
}
-
return NO;
-
} else if (self.webViewDelegate) {
-
return [self.webViewDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
-
} else {
-
return YES;
-
}
-
}
使用示例
UI端
-
<span style="white-space:pre"> </span>
-
document.addEventListener('WebViewJavascriptBridgeReady', onBridgeReady, false)
-
<span style="white-space:pre"> </span>
-
function onBridgeReady(event) {
-
var bridge = event.bridge
-
var uniqueId = 1
-
<span style="white-space:pre"> </span>
-
function log(message, data) {
-
var log = document.getElementById('log')
-
var el = 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) }
-
}
-
<span style="white-space:pre"> </span>
-
bridge.init(function(message) {
-
log('JS got a message', message)
-
})
-
<span style="white-space:pre"> </span>
-
bridge.registerHandler('testJavascriptHandler', function(data, response) {
-
log('JS handler testJavascriptHandler was called', data)
-
response.respondWith({ 'Javascript Says':'Right back atcha!' })
-
})
-
-
<span style="white-space:pre"> </span>
-
var button = document.getElementById('buttons').appendChild(document.createElement('button'))
-
button.innerHTML = 'Send message to ObjC'
-
button.ontouchstart = function(e) {
-
e.preventDefault()
-
<span style="white-space:pre"> </span>
-
bridge.send('Hello from JS button')
-
}
-
-
document.body.appendChild(document.createElement('br'))
-
-
<span style="white-space:pre"> </span>
-
var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
-
callbackButton.innerHTML = 'Fire testObjcCallback'
-
callbackButton.ontouchstart = function(e) {
-
e.preventDefault()
-
log("Calling handler testObjcCallback")
-
-
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
-
log('Got response from testObjcCallback', response)
-
})
-
}
-
}
Native端
-
-
UIWebView* webView = [[UIWebView alloc] initWithFrame:self.window.bounds];
-
[self.window addSubview:webView];
-
-
-
[WebViewJavascriptBridge enableLogging];
-
-
-
_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];
-
}];
-
-
-
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponse *response) {
-
NSLog(@"testObjcCallback called: %@", data);
-
[response respondWith:@"Response from testObjcCallback"];
-
}];
-
-
-
[_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);
-
}];
-
-
-
[_bridge callHandler:@"testJavascriptHandler" data:[NSDictionary dictionaryWithObject:@"before ready" forKey:@"foo"]];
-
-
[self renderButtons:webView];
-
[self loadExamplePage:webView];
-
-
-
[_bridge send:@"A string sent from ObjC after Webview has loaded."];
項目運行截圖:
轉載自:http://blog.csdn.net/yanghua_kobe/article/details/8209751