對(duì)于任意hybrid APP,不可避免進(jìn)行native與web之間的交互婚苹。WebViewJavascriptBridge 就是一款用于實(shí)現(xiàn)原生端與web端無(wú)縫交互的三方庫(kù)颖变,應(yīng)用廣泛法牲,支持UIWebView、WKWebView(iOS)以及WebView(OSX)虏杰,原理一致,本文借助OC的UIWebView進(jìn)行分析勒虾。
框架簡(jiǎn)介
所謂交互纺阔,無(wú)非就是兩端(native端與JS端)能夠互相調(diào)用方法,并傳遞數(shù)據(jù)从撼。iOS7之后州弟,隨著JavaScriptCore框架的推出,為native與JS的交互鋪平了道路低零,使用該框架即可實(shí)現(xiàn)OC與JS的互調(diào)。而在iOS7之前拯杠,OC與jS的交互只能借助于WebView掏婶,OC調(diào)用JS依賴于WebView提供的執(zhí)行JS的方法,而JS調(diào)用OC則需要采用URL攔截的方式潭陪。WebViewJavascriptBridge就是為WebView中的JS交互而生的雄妥,采用后者的交互方式。
大致原理為OC端和JS端各自保存一個(gè)bridge對(duì)象依溯,并各自維護(hù)開(kāi)放給另一端調(diào)用的方法集合以及回調(diào)方法集合老厌,兩端之間的交互通過(guò)傳遞handleName(可以理解為方法id)以及callBackId來(lái)實(shí)現(xiàn)方法調(diào)用以及回調(diào)的。
代碼文件結(jié)構(gòu)如下(V6.0.2版本):
WebViewJavascriptBridge
基于UIWebView/WebView的OC端交互邏輯處理類黎炉,面向OC業(yè)務(wù)層枝秤,提供了注冊(cè)O(shè)C方法、調(diào)用JS方法等接口慷嗜。WebViewJavascriptBridgeBase
OC端bridge對(duì)象基礎(chǔ)服務(wù)類淀弹,維護(hù)OC端開(kāi)放給JS端的方法以及OC回調(diào)方法,實(shí)現(xiàn)OC向JS發(fā)送數(shù)據(jù)的具體邏輯庆械。WebViewJavascriptBridge_JS
維護(hù)了一份JS代碼薇溃,用于JS環(huán)境的注入。同時(shí)維護(hù)JS端的bridge對(duì)象缭乘,管理JS端注冊(cè)的方法集合以及回調(diào)方法集合沐序,面向Web端提供注冊(cè)JS方法、調(diào)用OC端方法的接口堕绩。WKWebViewJavascriptBridge
基于WKWebView的OC端交互邏輯處理類策幼,職能同WebViewJavascriptBridge。ExampleApp.html
非框架文件逛尚,由于我們借助WebViewJavascriptBridge的官方example講解垄惧,該文件作為模擬開(kāi)發(fā)環(huán)境的web頁(yè)面示例。
Bridge環(huán)境初始化
OC端bridge初始化
業(yè)務(wù)層創(chuàng)建好webView實(shí)例之后绰寞,需要根據(jù)此webView創(chuàng)建對(duì)應(yīng)的Bridge到逊,即初始化bridge對(duì)象:
//初始化OC端Bridge對(duì)象
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
//設(shè)置代理铣口,避免框架對(duì)webView代理的入侵
[_bridge setWebViewDelegate:self];
相關(guān)實(shí)現(xiàn)如下(僅保留相關(guān)代碼):
+ (instancetype)bridgeForWebView:(id)webView {
return [self bridge:webView];
}
+ (instancetype)bridge:(id)webView {
if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) {
WebViewJavascriptBridge* bridge = [[self alloc] init];
[bridge _platformSpecificSetup:webView];
return bridge;
}
return nil;
}
- (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView {
_webView = webView;
_webView.delegate = self;
_base = [[WebViewJavascriptBridgeBase alloc] init];
_base.delegate = self;
}
//WebViewJavascriptBridgeBase實(shí)例初始化
//messageHandlers:保存OC端注冊(cè)給JS端的方法集合,key為handleName觉壶,value為方法實(shí)現(xiàn)block
//startupMessageQueue:由于OC端調(diào)用JS方法時(shí)脑题,JS環(huán)境可能還沒(méi)有初始化好,該數(shù)組暫存JS環(huán)境初始化之前的JS方法調(diào)用操作(保存調(diào)用的handleName)
//responseCallbacks:保存OC端調(diào)用JS方法的回調(diào)block铜靶,key為callBackId叔遂,value會(huì)回調(diào)實(shí)現(xiàn)block
//_uniqueId:表示消息的唯一性
- (id)init {
if (self = [super init]) {
self.messageHandlers = [NSMutableDictionary dictionary];
self.startupMessageQueue = [NSMutableArray array];
self.responseCallbacks = [NSMutableDictionary dictionary];
_uniqueId = 0;
}
return self;
}
OC端的bridge初始化即為webView實(shí)例創(chuàng)建了一個(gè)bridge對(duì)象,并初始化了相關(guān)的數(shù)據(jù)結(jié)構(gòu)争剿。
JS端Bridge初始化
JS端bridge對(duì)象的初始化要比OC端復(fù)雜得多已艰,因?yàn)镴S環(huán)境的初始化也是在原生端完成的,涉及原生端對(duì)webView初始化JS環(huán)境操作指令的捕捉蚕苇,以及JS代碼的注入哩掺。
webView加載時(shí),執(zhí)行了如下JS:
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)
}
傳入的參數(shù)是一個(gè)函數(shù)涩笤,暫且不管該函數(shù)的內(nèi)容嚼吞。首先判斷當(dāng)前環(huán)境中是否存在WebViewJavascriptBridge對(duì)象,若存在則直接調(diào)用傳入的函數(shù)并已WebViewJavascriptBridge作為參數(shù)蹬碧。該WebViewJavascriptBridge就是JS端的bridge對(duì)象舱禽,頁(yè)面首次加載時(shí),JS環(huán)境沒(méi)有初始化好恩沽,也就是該Bridge對(duì)象沒(méi)有創(chuàng)建好誊稚,因此會(huì)先將傳入的函數(shù)暫存在數(shù)組WVJBCallbacks中。
重點(diǎn)看 WVJBIframe.src = 'https://__bridge_loaded__';
飒筑,iframe可以理解為webView的窗口片吊,當(dāng)改變iframe的src時(shí),頁(yè)面就會(huì)進(jìn)行刷新并加載指定的URL协屡,此處試圖加載 https://__bridge_loaded__
俏脊。
我們知道,UIWebView刷新頁(yè)面前肤晓,會(huì)先回調(diào) shouldStartLoadWithRequest
方法爷贫。因此可以在該代理方法中攔截該URL的加載,并執(zhí)行相應(yīng)的邏輯补憾÷眩可以在 WebViewJavascriptBridge.m 文件中看到如下代碼:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if (webView != _webView) { return YES; }
NSURL *url = [request URL];
__strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
//判斷是否是WebViewJavascriptBridge內(nèi)部發(fā)起的請(qǐng)求
if ([_base isWebViewJavascriptBridgeURL:url]) {
//判斷是否是JS環(huán)境初始化請(qǐng)求
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
}
//判斷是否是web端交互請(qǐng)求
else if ([_base isQueueMessageURL:url]) {
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];
}
//未知請(qǐng)求
else {
[_base logUnkownMessage:url];
}
return NO;
}
//常規(guī)webView加載請(qǐng)求,交給業(yè)務(wù)層的webView自行處理
else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
} else {
return YES;
}
}
重點(diǎn)看中間部分盈匾,主要攔截了兩種請(qǐng)求:JS環(huán)境初始化請(qǐng)求和web端交互請(qǐng)求腾务,都是由WebViewJavascriptBridge內(nèi)部發(fā)起的。
#define kOldProtocolScheme @"wvjbscheme"
#define kNewProtocolScheme @"https"
#define kQueueHasMessage @"__wvjb_queue_message__"
#define kBridgeLoaded @"__bridge_loaded__"
//判斷是否是WebViewJavascriptBridge內(nèi)部發(fā)起的請(qǐng)求
- (BOOL)isWebViewJavascriptBridgeURL:(NSURL*)url {
if (![self isSchemeMatch:url]) {
return NO;
}
return [self isBridgeLoadedURL:url] || [self isQueueMessageURL:url];
}
//判斷請(qǐng)求的協(xié)議是否符合WebViewJavascriptBridge的約定
- (BOOL)isSchemeMatch:(NSURL*)url {
NSString* scheme = url.scheme.lowercaseString;
return [scheme isEqualToString:kNewProtocolScheme] || [scheme isEqualToString:kOldProtocolScheme];
}
//判斷是否是WebViewJavascriptBridge發(fā)起的web交互請(qǐng)求
- (BOOL)isQueueMessageURL:(NSURL*)url {
NSString* host = url.host.lowercaseString;
return [self isSchemeMatch:url] && [host isEqualToString:kQueueHasMessage];
}
//判斷是否是WebViewJavascriptBridge發(fā)起的JS環(huán)境初始化請(qǐng)求
- (BOOL)isBridgeLoadedURL:(NSURL*)url {
NSString* host = url.host.lowercaseString;
return [self isSchemeMatch:url] && [host isEqualToString:kBridgeLoaded];
}
因此削饵,之前頁(yè)面中加載的 https://__bridge_loaded__
就會(huì)被攔截岩瘦,并識(shí)別為JS環(huán)境初始化請(qǐng)求未巫,由OC端執(zhí)行相應(yīng)的的初始化邏輯。
- (void)injectJavascriptFile {
NSString *js = WebViewJavascriptBridge_js();
[self _evaluateJavascript:js];
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
[self _dispatchMessage:queuedMessage];
}
}
}
該方法中執(zhí)行了WebViewJavascriptBridge_JS文件中的js代碼启昧。為方便閱讀叙凡,我把JS代碼拎出來(lái)如下:
;(function() {
if (window.WebViewJavascriptBridge) {
return;
}
if (!window.onerror) {
window.onerror = function(msg, url, line) {
console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
}
}
//創(chuàng)建Bridge對(duì)象
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
//初始化一些變量
var messagingIframe;
//消息隊(duì)列,存放發(fā)送給OC的數(shù)據(jù)
var sendMessageQueue = [];
//存放JS端注冊(cè)的函數(shù)
var messageHandlers = {};
var CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
//存放回調(diào)函數(shù)
var responseCallbacks = {};
//標(biāo)識(shí)消息的唯一性
var uniqueId = 1;
//是否異步執(zhí)行
var dispatchMessagesWithTimeoutSafety = true;
//注冊(cè)給OC調(diào)用的方法
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
//調(diào)用OC方法
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
//禁止異步發(fā)送消息
function disableJavscriptAlertBoxSafetyTimeout() {
dispatchMessagesWithTimeoutSafety = false;
}
//向OC發(fā)送消息
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
//responseCallBacks對(duì)象對(duì)用callBackId存放回調(diào)函數(shù)
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
//獲取當(dāng)前消息隊(duì)列待發(fā)送的消息
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
//接收從OC發(fā)來(lái)的消息
function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
}
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}
}
}
}
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);
registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
//Bridge初始化完畢后密末,執(zhí)行webView加載時(shí)待執(zhí)行的JS代碼
setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i=0; i<callbacks.length; i++) {
callbacks[i](WebViewJavascriptBridge);
}
}
})();
分析一下握爷,所做的事情很簡(jiǎn)單,就是創(chuàng)建了一個(gè)bridge對(duì)象严里,該對(duì)象維護(hù)了若干函數(shù)新啼,OC端可以通過(guò)bridge對(duì)象獲取到相應(yīng)的函數(shù)來(lái)執(zhí)行調(diào)用。
registerHandler
用于JS端注冊(cè)供OC端調(diào)用的方法callHandler
用于調(diào)用OC方法disableJavscriptAlertBoxSafetyTimeout
關(guān)閉消息異步發(fā)送_fetchQueue
獲取當(dāng)前JS端待發(fā)送的消息_handleMessageFromObjC
接收并處理OC端發(fā)送給JS端的數(shù)據(jù)
另外田炭,代碼最后執(zhí)行了_callWVJBCallbacks
函數(shù) 师抄,還記得之前webView加載時(shí)暫存在WVJBCallbacks的函數(shù)嗎?那時(shí)因?yàn)閎ridge未初始化先暫存起來(lái)教硫,現(xiàn)在bridge初始化完畢了再來(lái)執(zhí)行。
至此JS環(huán)境初始化完成辆布。不過(guò)上面 injectJavascriptFile
方法中除了執(zhí)行JS代碼瞬矩,還執(zhí)行了如下代碼:
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
[self _dispatchMessage:queuedMessage];
}
}
startupMessageQueue是OC端bridge維護(hù)的屬性,之前介紹過(guò)其用于暫存JS環(huán)境初始化之前OC發(fā)起的JS消息調(diào)用》媪幔現(xiàn)在JS環(huán)境初始化好了景用,便可取出其中的消息發(fā)送了。
OC調(diào)用JS
OC調(diào)用JS可以分為4個(gè)過(guò)程:
- JS注冊(cè)方法給OC
- OC調(diào)用JS注冊(cè)的方法
- JS方法被調(diào)用惭蹂,執(zhí)行自身邏輯
- JS回調(diào)數(shù)據(jù)給OC
JS注冊(cè)方法
毫無(wú)疑問(wèn)伞插,這一步應(yīng)該在webView加載時(shí)執(zhí)行。還記得ExampleApp.html在加載時(shí)傳了一個(gè)函數(shù)給 setupWebViewJavascriptBridge()
嗎盾碗。這個(gè)函數(shù)后面被執(zhí)行媚污,函數(shù)內(nèi)容就腦闊相關(guān)JS方法的注冊(cè)。示例中該函數(shù)相關(guān)內(nèi)容如下:
function(bridge) {
...
//注冊(cè)testJavascriptHandler方法
bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
log('ObjC called testJavascriptHandler with', data)
var responseData = { 'Javascript Says':'Right back atcha!' }
log('JS responding with', responseData)
responseCallback(responseData)
})
...
}
沒(méi)錯(cuò)廷雅,就是通過(guò)調(diào)用bridge對(duì)象的registerHandler方法來(lái)注冊(cè)的耗美。實(shí)現(xiàn)很簡(jiǎn)單:
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
單純只是把handleName及方法實(shí)現(xiàn)綁定在一起,保存在messageHandlers中航缀。
OC發(fā)起調(diào)用
OC端通過(guò)JS注冊(cè)的handleName調(diào)用JS相應(yīng)的方法商架,同時(shí)可以設(shè)置回調(diào)操作。
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" } responseCallback:^(id responseData) {
NSLog(@"testJavascriptHandler callBack: %@", responseData);
}];
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
[_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
NSMutableDictionary* message = [NSMutableDictionary dictionary];
if (data) {
message[@"data"] = data;
}
//若OC端需要回調(diào)芥玉,則創(chuàng)建唯一的callbackId保存該回調(diào)block
if (responseCallback) {
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
self.responseCallbacks[callbackId] = [responseCallback copy];
message[@"callbackId"] = callbackId;
}
if (handlerName) {
message[@"handlerName"] = handlerName;
}
//OC端將數(shù)據(jù)封裝成特定格式的字典蛇摸,JS端在拿到數(shù)據(jù)后按照該格式解封裝
[self _queueMessage:message];
}
- (void)_queueMessage:(WVJBMessage*)message {
if (self.startupMessageQueue) {
//若startupMessageQueue不為空,說(shuō)明JS環(huán)境未初始化灿巧,暫存消息
[self.startupMessageQueue addObject:message];
} else {
//直接發(fā)送消息
[self _dispatchMessage:message];
}
}
- (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"];
//序列化消息數(shù)據(jù)并進(jìn)行轉(zhuǎn)義后赶袄,通過(guò)webView的stringByEvaluatingJavaScriptFromString執(zhí)行JS代碼揽涮。JS內(nèi)容為調(diào)用bridge對(duì)象的_handleMessageFromObjC方法,并傳入序列化后的數(shù)據(jù)
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接收調(diào)用
JS通過(guò)bridge的 _handleMessageFromObjC
方法接收OC的消息弃鸦,具體實(shí)現(xiàn)如下:
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
function _dispatchMessageFromObjC(messageJSON) {
//根據(jù)dispatchMessagesWithTimeoutSafety標(biāo)志量決定是否異步執(zhí)行
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
//若responseId不為空绞吁,說(shuō)明這是一個(gè)回調(diào)消息
if (message.responseId)
{
//從responseCallbacks中取出事先保存好的回調(diào)函數(shù)執(zhí)行
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
//執(zhí)行完畢后移除回調(diào)函數(shù)
delete responseCallbacks[message.responseId];
}
//若responseId不存在,說(shuō)明這是一個(gè)OC發(fā)起的消息
else
{
//若callbackId不為空唬格,說(shuō)明需要回調(diào)OC
if (message.callbackId) {
var callbackResponseId = message.callbackId;
//創(chuàng)建回調(diào)函數(shù)家破,傳入JS方法中,由web業(yè)務(wù)方?jīng)Q定何時(shí)執(zhí)行該回調(diào)函數(shù)來(lái)回調(diào)OC
responseCallback = function(responseData) {
//通過(guò)調(diào)用_doSend向OC發(fā)送包含responseId的數(shù)據(jù)购岗,來(lái)指明這是一個(gè)回調(diào)調(diào)用汰聋,responseId的值為OC傳過(guò)來(lái)的callbackId。
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
}
//從messageHandlers中根據(jù)handlerName取出之前注冊(cè)的方法
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
//執(zhí)行JS方法自身邏輯
handler(message.data, responseCallback);
}
}
}
}
首先解析OC傳過(guò)來(lái)的數(shù)據(jù)喊积,然后判斷數(shù)據(jù)中是否存在responseId字段烹困,若存在,則被認(rèn)為這是一個(gè)OC回調(diào)的消息乾吻,否則被認(rèn)為是OC發(fā)起的消息髓梅。此處不存在responseId,因?yàn)楫?dāng)前場(chǎng)景為OC發(fā)起的消息绎签。但是因?yàn)橄?shù)據(jù)中含有callbackId枯饿,被認(rèn)為是OC需要回調(diào),因此創(chuàng)建一個(gè)callBack函數(shù)诡必,函數(shù)內(nèi)容為調(diào)用 _doSend
向OC發(fā)送指定格式的數(shù)據(jù)(重點(diǎn)是包含responseId字段奢方,OC端會(huì)根據(jù)此字段識(shí)別這是一個(gè)JS回調(diào)調(diào)用,同JS端)爸舒。隨后蟋字,根據(jù)OC傳過(guò)來(lái)的handleName獲取到注冊(cè)的方法,傳入data及callBack函數(shù)來(lái)調(diào)用扭勉。至此鹊奖,web端的方法得到調(diào)用:
bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
log('ObjC called testJavascriptHandler with', data)
var responseData = { 'Javascript Says':'Right back atcha!' }
log('JS responding with', responseData)
responseCallback(responseData)
})
OC回調(diào)
JS注冊(cè)的方法被調(diào)用的時(shí)候,接收了一個(gè)回調(diào)OC的函數(shù)剖效,業(yè)務(wù)端根據(jù)實(shí)際情況選擇合適的時(shí)機(jī)調(diào)用該函數(shù)以回調(diào)數(shù)據(jù)給OC端嫉入。關(guān)于該回調(diào)函數(shù)的創(chuàng)建上面已經(jīng)有說(shuō)明,回調(diào)的方式就是向OC發(fā)送消息璧尸,屬于JS調(diào)用OC的范疇咒林,將在下面介紹。
JS調(diào)用OC
和OC調(diào)用JS一樣爷光,JS調(diào)用OC也是同樣的4個(gè)過(guò)程:
- OC注冊(cè)方法給JS
- JS發(fā)起調(diào)用
- OC接收調(diào)用并執(zhí)行相應(yīng)邏輯
- OC回調(diào)數(shù)據(jù)給JS
OC注冊(cè)方法
和JS類似垫竞,OC端也是通過(guò)bridge對(duì)象注冊(cè)方法。
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];
實(shí)現(xiàn)也一致,綁定handleName與block欢瞪,保存到bridge的messageHandlers屬性中:
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
JS發(fā)起調(diào)用
web業(yè)務(wù)端根據(jù)實(shí)際場(chǎng)景調(diào)用OC活烙,示例中是在ExampleApp.html加載時(shí)向DOM中添加了一個(gè)按鈕元素,點(diǎn)擊按鈕調(diào)用OC注冊(cè)的方法遣鼓。
var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
callbackButton.innerHTML = 'Fire testObjcCallback'
callbackButton.onclick = function(e) {
e.preventDefault()
log('JS calling handler "testObjcCallback"')
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
}
可以看到啸盏,JS端通過(guò)bridge對(duì)象的 callHandler
執(zhí)行調(diào)用。和OC調(diào)用JS很類似骑祟,也是傳了3個(gè)參數(shù):handleName回懦、data、回調(diào)函數(shù)次企。相關(guān)方法實(shí)現(xiàn)如下:
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
//responseCallBacks對(duì)象對(duì)用callBackId存放回調(diào)函數(shù)
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
可以看到最終執(zhí)行的是 _doSend
函數(shù)怯晕,這個(gè)函數(shù)之前在講解JS回調(diào)OC的時(shí)候也遇到過(guò),其作用就是向OC發(fā)送消息缸棵。_doSend
執(zhí)行時(shí)放坏,若JS需要回調(diào)冯事,即responseCallback參數(shù)不為空,則會(huì)創(chuàng)建唯一callbackId抄瓦,并保存回調(diào)函數(shù)愚战。隨后饵隙,將數(shù)據(jù)封裝成指定格式的對(duì)象(格式同OC調(diào)用JS時(shí)一致)蠕啄,發(fā)送給OC慨亲。此處,我們又見(jiàn)到了眼熟的src狰贯,之前在web端初始化JS環(huán)境的時(shí)候遇到過(guò)。沒(méi)錯(cuò)赏廓,JS無(wú)法直接調(diào)用OC涵紊,此處依舊是通過(guò)URL攔截的方式實(shí)現(xiàn)的。首先將要發(fā)送給OC的數(shù)據(jù)保存在全局變量中幔摸,然后修改iframe的src來(lái)加載特定URL:https://__wvjb_queue_message__
摸柄。
OC接收調(diào)用
因?yàn)閣ebView要加載URL,同樣先回調(diào)了webView的 shouldStartLoadWithRequest
方法既忆。方法中識(shí)別了這是一個(gè)由WebViewJavascriptBridge發(fā)起的web端交互請(qǐng)求驱负,進(jìn)行攔截處理:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
...
if ([_base isQueueMessageURL:url]) {
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];
}
...
}
其中 webViewJavascriptFetchQueyCommand
的作用的獲取之前保存在JS端的需要傳遞給OC的數(shù)據(jù):
- (NSString *)webViewJavascriptFetchQueyCommand {
return @"WebViewJavascriptBridge._fetchQueue();";
}
通過(guò)JS端bridge對(duì)象的 _fetchQueue
獲取:
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
可以看到傳過(guò)來(lái)的數(shù)據(jù)是一個(gè)被序列化的數(shù)組字符串患雇。
OC端拿到數(shù)據(jù)后跃脊,調(diào)用 flushMessageQueue
執(zhí)行相應(yīng)的邏輯。
- (void)flushMessageQueue:(NSString *)messageQueueString{
if (messageQueueString == nil || messageQueueString.length == 0) {
NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
return;
}
//反序列化json
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
if (![message isKindOfClass:[WVJBMessage class]]) {
NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
continue;
}
[self _log:@"RCVD" json:message];
NSString* responseId = message[@"responseId"];
//若傳過(guò)來(lái)的數(shù)據(jù)中包含responseId字段苛吱,說(shuō)明這是一個(gè)JS回調(diào)調(diào)用
if (responseId)
{
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
}
//若數(shù)據(jù)中不含responseId酪术,說(shuō)明這是一個(gè)JS發(fā)起的調(diào)用
else
{
WVJBResponseCallback responseCallback = NULL;
//若數(shù)據(jù)中含callbackId,說(shuō)明JS端需要回調(diào)
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
//創(chuàng)建回調(diào)JS的Block
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
//向JS發(fā)送特定格式的數(shù)據(jù)來(lái)執(zhí)行回調(diào)
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}
//根據(jù)handlerName獲取messageHandlers中保存的方法
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
//執(zhí)行OC方法
handler(message[@"data"], responseCallback);
}
}
}
處理邏輯和JS接收數(shù)據(jù)后的處理邏輯幾乎一致,同樣是根據(jù)傳過(guò)來(lái)的數(shù)據(jù)中是否包含responseId字段來(lái)判斷這是一個(gè)JS回調(diào)調(diào)用還是由JS發(fā)起的調(diào)用绘雁。若是JS回調(diào)調(diào)用橡疼,則從responseCallbacks集合中根據(jù)responseId取出回調(diào)block并調(diào)用;若是JS發(fā)起的調(diào)用庐舟,則最終從messageHandlers中根據(jù)handlerName取出對(duì)應(yīng)的block調(diào)用欣除。
JS回調(diào)
OC端在處理JS的調(diào)用時(shí),若識(shí)別出這是一個(gè)JS發(fā)起的調(diào)用挪略,而非回調(diào)調(diào)用历帚,則根據(jù)傳過(guò)來(lái)的數(shù)據(jù)中是否包含callbackId字段來(lái)決定是否需要回調(diào)JS。若需要瘟檩,則創(chuàng)建回調(diào)block抹缕,并將該回調(diào)block傳入OC方法,由OC業(yè)務(wù)層決定何時(shí)進(jìn)行回調(diào)墨辛,以及回調(diào)什么樣的數(shù)據(jù)卓研。而該回調(diào)block的內(nèi)容,無(wú)可厚非就是向JS發(fā)送特定格式的數(shù)據(jù):
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
//向JS發(fā)送特定格式的數(shù)據(jù)來(lái)執(zhí)行回調(diào)
[self _queueMessage:msg];
};
_queueMessage
方法正是之前介紹的OC調(diào)用JS的方法睹簇。另外奏赘,JS在接收到回調(diào)數(shù)據(jù)后的處理也在之前介紹過(guò)了,不再贅述太惠。
總結(jié)
- 交互前需要先對(duì)OC環(huán)境和JS環(huán)境進(jìn)行初始化磨淌,JS環(huán)境的初始化通過(guò)Web頁(yè)面加載時(shí)發(fā)送特定的URL來(lái)完成。
- WebViewJavascriptBridge在OC端和JS端各自維護(hù)一個(gè)bridge對(duì)象來(lái)保存開(kāi)放給另一端的方法凿渊,以及自身調(diào)用另一端后的回調(diào)方法梁只。前者通過(guò)handlerName來(lái)映射,后者通過(guò)callBackId標(biāo)識(shí)唯一性埃脏。方法調(diào)用時(shí)必定攜帶handlerName搪锣,若需要回調(diào),還需攜帶callBackId彩掐。
- WebViewJavascriptBridge中OC調(diào)用JS采用的是WebView提供的JS執(zhí)行方法构舟;而JS調(diào)用OC采用的是URL攔截的方式,OC端通過(guò)識(shí)別特定的URL來(lái)區(qū)分是否需要攔截堵幽,并做相應(yīng)的邏輯處理狗超。