網(wǎng)上好多都是在介紹 WebViewJavascriptBridge如何使用岔绸,這篇文章就來說說WebViewJavascriptBridge 設(shè)計(jì)原理。
主要從兩個(gè)過程來講一下:js調(diào)用UIViewController中的代碼(Native)误褪,Native調(diào)用js
1.概述
首先有兩個(gè)問題:
a. Native(中的UIWebView)是否可以直接調(diào)用js method(方法)? 可以碾褂。
b. js 是否可以直接調(diào)用Native的 method?不行历葛。
明確上述兩個(gè)問題正塌,那么上圖就不難明白了,webpage中的 js method和 webview 本地的method之間關(guān)系恤溶。那WebViewJavascriptBridge出現(xiàn)是否解決這個(gè)問題(這個(gè)問題就是讓js可以直接調(diào)用native的method)呢乓诽?答案是否定的?沒有本質(zhì)還是用uiwebview的代理方法進(jìn)行字段攔截(判斷url scheme)咒程,實(shí)現(xiàn)js間接調(diào)用native的method鸠天。
我們來看WebViewJavascriptBridge提供的demo:
主要的核心是下面兩個(gè),接下來我們就來討論一下其設(shè)計(jì)原理帐姻。
2. js調(diào)用Native method
在概述中說過稠集,js是不能直接調(diào)用native的method所以,需要借助
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
這個(gè)方法大家不陌生饥瓷,每次在重新定向URL的時(shí)候剥纷,這個(gè)方法就會(huì)被觸發(fā),通常情況呢铆,我們會(huì)在這里做一些攔截完成js和本地的間接交互什么的晦鞋。那么WebViewJavascriptBridge也不另外,也是這么做棺克。
我們先來看看在ExampleApp.html文件中點(diǎn)擊一個(gè)按鈕發(fā)起請(qǐng)求的代碼:
var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
callbackButton.innerHTML ='Fire testObjcCallback'
callbackButton.onclick = function(e) {
e.preventDefault()
log('JS calling handler "testObjcCallback"')
//1
bridge.callHandler('testObjcCallback', {'foo':'cccccccccccc'}, function(response) {
log('JS got response', response)
})
}
估計(jì)大家大體都能看懂悠垛,唯獨(dú)有疑問的地方是:
bridge.callHandler('testObjcCallback', {'foo':'cccccccccccc'}, function(response) {
log('JS got response', response)
})
}
這段代碼先不說,上面代碼就是一個(gè)按鈕的普通單擊事件方法娜谊。我們一起想一下确买,如果這個(gè)按鈕需要被點(diǎn)擊之后調(diào)用native中的funtion函數(shù),之后需要把這個(gè)(native的)funtion函數(shù)處理結(jié)果返回給js中的方法繼續(xù)處理因俐。這個(gè)是我們需求拇惋,帶著這個(gè)需求我們看一下這個(gè)方法,testObjcCallBack這個(gè)我們猜測(cè)一下應(yīng)該native中的方法或者一個(gè)能夠調(diào)用到方法的name/id抹剩,后面這個(gè)是個(gè)json{‘foo’:‘ccccccccccccc’},應(yīng)該是個(gè)參數(shù)撑帖,那么后面這個(gè)方法一看log應(yīng)該知道,是對(duì)native返回的result進(jìn)行處理的方法澳眷。拿具體是不是呢胡嘿?只要找到callHandler方法就知道了。
在文件WebView JavascriptBridge.js.txt里面我們找找這個(gè)方法:
function callHandler(handlerName, data, responseCallback) {
_doSend({ handlerName:handlerName, data:data }, responseCallback)
}
這里又多了一個(gè)方法叫_doSend連個(gè)參數(shù) 第1個(gè)是字典key-value定義钳踊,第二個(gè)是一個(gè)方法的指針(看看上面的方法你就知道了)衷敌,那我們必須在同一個(gè)文件里面看看能不能找到這個(gè)_doSend方法:
function _doSend(message, responseCallback) {
if(responseCallback) {
var callbackId ='cb_'+(uniqueId++)+'_'+newDate().getTime()
responseCallbacks[callbackId] = responseCallback
message['callbackId'] = callbackId
}
sendMessageQueue.push(message)
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME +'://'+ QUEUE_HAS_MESSAGE
}
找到了勿侯。
逐行分析一下,變量callbackId是個(gè)字符串缴罗,responseCallBacks[] 一看就知道是個(gè)字典助琐,這個(gè)字典把回掉(我們猜測(cè))的方法responseCallback給保存起來,這Key(也就是callbackId)應(yīng)該是唯一的面氓,通過計(jì)數(shù)和時(shí)間應(yīng)該知道這個(gè)字符串應(yīng)該是唯一的兵钮,message也是一個(gè)字典,這是給message添加了一個(gè)新的key-value舌界。干嘛呢掘譬?我也不知道,我們來看看sendMessageQueue是什么呻拌,大家一個(gè)push就知道應(yīng)該是個(gè)數(shù)組葱轩。他吧一個(gè)字典放到一個(gè)消息隊(duì)列中(數(shù)組隊(duì)列),讓后產(chǎn)生一個(gè)src(url scheme)藐握。
有兩個(gè)變量我們看看:
var CUSTOM_PROTOCOL_SCHEME ='wvjbscheme'
var QUEUE_HAS_MESSAGE ='__WVJB_QUEUE_MESSAGE__'
干嘛用靴拱,肯定是給webview 的 delegate判斷用的,你感覺呢趾娃?(肯定是)
下面是在文件:WebViewJavascriptBridge.m
好了到了這里大家猜猜這個(gè)要干嘛缭嫡?肯定是要發(fā)url讓web截取對(duì)吧?那還用問啊抬闷,肯定是啊妇蛀,已經(jīng)說過了js能不能調(diào)用native的funtion函數(shù)?不能笤成。我們來看看這個(gè)messagingIframe是:
function _createQueueReadyIframe(doc) {
messagingIframe = doc.createElement('iframe')
messagingIframe.style.display ='none'
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME +'://'+ QUEUE_HAS_MESSAGE
doc.documentElement.appendChild(messagingIframe)
}
原來就是iframe评架,這個(gè)就不同給大家解釋了。好了src一產(chǎn)生就會(huì)出現(xiàn)什么炕泳,uiwebview代理回掉截獲纵诞,此時(shí)我們把目光回到UIWebview的Native下面:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if(webView != _webView) {returnYES; }
NSURL *url = [request URL];
__strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
if([[url scheme] isEqualToString:kCustomProtocolScheme])
{
if([[url host] isEqualToString:kQueueHasMessage])
{
//會(huì)走這里
[self _flushMessageQueue];
}
else
{
NSLog(@"WebViewJavascriptBridge: WARNING: Received unknown WebViewJavascriptBridge command %@://%@", kCustomProtocolScheme, [url path]);
}
returnNO;
}
elseif(strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)])
{
return[strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
}
else
{
returnYES;
}
}
一看就頭大,哈哈培遵,是嚼隘,我也頭大疚宇」瞪常看看上面的注釋說 會(huì)走這里施戴,我們看看為什么會(huì)走那里,最外圈的if([url scheme])判斷是
#define kCustomProtocolScheme @"wvjbscheme"
這個(gè)定義是什么意思皇耗,我們先不做解釋南窗,剛才我們說過js不能直接調(diào)用native的function,大家只要記住這點(diǎn),接著往下走就是了万伤。至于為什么走這里窒悔,自己看代碼(上文有提到),我們看看_flushMessageQueue:
- (void)_flushMessageQueue {
NSString *messageQueueString = [_webView stringByEvaluatingJavaScriptFromString:@"WebViewJavascriptBridge._fetchQueue();"];
//json轉(zhuǎn)成數(shù)組
id messages = [self _deserializeMessageJSON:messageQueueString];
if(![messages isKindOfClass:[NSArrayclass]]) {
NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [messagesclass], messages);
return;
}
for(WVJBMessage* message in messages) {
if(![message isKindOfClass:[WVJBMessageclass]]) {
NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [messageclass], message);
continue;
}
[self _log:@"RCVD"json:message];
//用于js回掉
NSString* responseId = message[@"responseId"];
if(responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[_responseCallbacks removeObjectForKey:responseId];
}else{
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if(callbackId) {
responseCallback = ^(id responseData) {
if(responseData == nil) {
responseData = [NSNullnull];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
}else{
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};}
WVJBHandler handler;
if(message[@"handlerName"]) {
handler = _messageHandlers[message[@"handlerName"]];
}else{
handler = _messageHandler;
}
if(!handler) {
[NSException raise:@"WVJBNoHandlerException"format:@"No handler for message from JS: %@", message];
}
handler(message[@"data"], responseCallback);
}}}
這下牛逼了敌买,不忍直視凹蛑椤!這么多虹钮,哈哈北救,多不可怕,可怕是你堅(jiān)持不下去了芜抒。
我們逐行來看:
NSString *messageQueueString = [_webView stringByEvaluatingJavaScriptFromString:@"WebViewJavascriptBridge._fetchQueue();"];
我們必須回去到j(luò)s文件中去,這里是webview直接調(diào)用js中的方法:
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue)
sendMessageQueue = []
return messageQueueString
}
謝天謝地這個(gè)方法代碼不多托启,這個(gè)消息很眼熟宅倒,SendMessageQueue,剛才我們說什么來屯耸?他是一個(gè)字典拐迁,那里面有哪些東西,我么來看看
handlerName:handlerName,
data:data,
callbackId:callbackId
這個(gè)消息字典此時(shí)被取出來準(zhǔn)備做什么疗绣,這里提示下我們已經(jīng)走到webview 的delegate里面了线召,所以拿到這些信息肯定是調(diào)用native的method對(duì)吧?肯定是的多矮。接著往下走缓淹,接著會(huì)把json字符串轉(zhuǎn)成數(shù)組,然后進(jìn)行判斷塔逃,
NSString* responseId = message[@"responseId"];
有沒有responseid讯壶,你說又沒,肯定沒有巴宓痢(你不行看看上面)伏蚊,所以就這這里了
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if(callbackId) {
responseCallback = ^(id responseData) {
if(responseData == nil) {
responseData = [NSNullnull];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
}else{
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};}
WVJBHandler handler;
if(message[@"handlerName"]) {
handler = _messageHandlers[message[@"handlerName"]];
}else{
handler = _messageHandler;
}
if(!handler) {
[NSException raise:@"WVJBNoHandlerException"format:@"No handler for message from JS: %@", message];
}
handler(message[@"data"], responseCallback);
這部分是重點(diǎn),到底他是怎么要調(diào)用本地function的格粪,callbackId大家熟悉吧躏吊,判斷是否為空,不為空給他指定一個(gè)block帐萎,這個(gè)不說了比伏,block指定,此時(shí)不調(diào)用(手動(dòng)調(diào)用才會(huì)執(zhí)行)吓肋,這個(gè)剛才說了用來處理native的function處理的result用于把處理后的值返回給js的凳怨,接著往下去,看到handler這個(gè)方法會(huì)從message找到handlerName,這里我們看一下多了一個(gè)_messageHandlers字典肤舞,從這個(gè)字典獲取一個(gè)block(WVJBHandler是一個(gè)block)紫新,直接執(zhí)行了。那我們看看_messageHandlers是怎么被添加block的:
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_messageHandlers[handlerName] = [handler copy];
}
那又是誰調(diào)用了這個(gè)方法:
找到了(在文件 ExampleAppViewController.m的viewdidload中)李剖,這里有方法testObjecCallback
[_bridge registerHandler:@"testObjcCallback"handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];
有點(diǎn)亂了芒率。剛才我們的思路都是倒推的,如果我們整過來篙顺,首先肯定是viewdidload初始化偶芍,初始化之后會(huì)把這個(gè)block加入到_messageHandlers中,之后因?yàn)閖s調(diào)用動(dòng)態(tài)讀取這個(gè)block調(diào)用德玫,在調(diào)用之前匪蟀,我們又把定一個(gè)block付給回掉處理的responseCallback的block,這個(gè)block在handler中調(diào)用而調(diào)用宰僧,有點(diǎn)繞材彪,自己可以多想想。
我們接著來看看:
responseCallback = ^(id responseData) {
if(responseData == nil) {
responseData = [NSNullnull];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
這個(gè)就是你繞的地方琴儿,他是后被定義的段化,所以一開不執(zhí)行,只有在處理數(shù)據(jù)后回調(diào)才會(huì)被調(diào)用造成,這里有個(gè)方法_queueMessage:
- (void)_queueMessage:(WVJBMessage*)message {
if(_startupMessageQueue) {
[_startupMessageQueue addObject:message];
}else{
[self _dispatchMessage:message];
}}
這里面還有個(gè)方法:
- (void)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message];
[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]) {
[_webView stringByEvaluatingJavaScriptFromString:javascriptCommand];
}else{
__strong WVJB_WEBVIEW_TYPE* strongWebView = _webView;
dispatch_sync(dispatch_get_main_queue(), ^{
[strongWebView stringByEvaluatingJavaScriptFromString:javascriptCommand];
});}}
我們?cè)诨氐絎ebViewJavascriptBridge.js.txt文件中看到
function _handleMessageFromObjC(messageJSON) {
if(receiveMessageQueue) {
receiveMessageQueue.push(messageJSON)
}else{//肯定走這個(gè)? 為什么呢显熏?
_dispatchMessageFromObjC(messageJSON)
}}
再來看看:
function _dispatchMessageFromObjC(messageJSON) {
setTimeout(function _timeoutDispatchMessageFromObjC() {
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({ responseId:callbackResponseId, responseData:responseData })
}}
var handler = WebViewJavascriptBridge._messageHandler
if(message.handlerName) {
handler = messageHandlers[message.handlerName]
}
try{
handler(message.data, responseCallback)
}catch(exception) {
if(typeof console !='undefined') {
console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception)
}}}
})
}
大家還記得我們返回的對(duì)象是:
@{ @"responseId":callbackId, @"responseData":responseData }
所以這里messageHandlers剛才也說過了用來存方法的,callbackId被換了個(gè)名字叫responseId意思一樣晒屎,只要值沒變就行喘蟆,所以就會(huì)執(zhí)行:
bridge.callHandler('testObjcCallback', {'foo':'cccccccccccc'}, function(response) {
log('JS got response', response)
})
中的方法,好了夷磕,完了履肃。
總結(jié)一下:js這邊
先把方法名字、參數(shù)坐桩、處理方法保存成一個(gè)字典在轉(zhuǎn)成json字符串尺棋,在通過UIWebview調(diào)用js中某個(gè)方法把這個(gè)json字符串傳到Native中
去(不是通過url傳的,這樣太low了)绵跷,同時(shí)把這個(gè)處理的方法以key-value形式放到一個(gè)js的字典中膘螟。
UIWebView在收到這個(gè)json之后,進(jìn)行數(shù)據(jù)處理碾局、還有js的回掉的處理方法(就是那個(gè)callbackId)處理完成后也會(huì)拼成一個(gè)key-value字典通過調(diào)用js傳回去(可以直接調(diào)用js)荆残。
js在接到這個(gè)json后,根據(jù)responseId讀取responseCallbacks中處理方法進(jìn)行處理Native code返回的數(shù)據(jù)净当。
3.Native調(diào)用js method
過程不是直接調(diào)用js内斯,也是通過js調(diào)用Native過程一樣的處理方式蕴潦。
大體來看一下,先看一個(gè)按鈕的單擊事件:
- (void)callHandler:(id)sender {
id data = @{ @"greetingFromObjC": @"Hi there, JS!"};
[_bridge callHandler:@"testJavascriptHandler"data:data responseCallback:^(id response) {
NSLog(@"testJavascriptHandler responded: %@", response);
}];
}
看看callHandler:
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
[self _sendData:data responseCallback:responseCallback handlerName:handlerName];
}
看看_sendData:
- (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];
_responseCallbacks[callbackId] = [responseCallback copy];
message[@"callbackId"] = callbackId;
}
if(handlerName) {
message[@"handlerName"] = handlerName;
}
[self _queueMessage:message];}
到_queueMessage:之后流程就和上面一樣了俘闯,這里面native也有個(gè):
NSString* responseId = message[@"responseId"];
if(responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[_responseCallbacks removeObjectForKey:responseId];
}
這個(gè)和js中的處理思想是一樣的潭苞。
總結(jié):native將方法名、參數(shù)真朗、回到的id放到一個(gè)對(duì)象中傳給js此疹。
js根據(jù)方法名字調(diào)用相應(yīng)方法,之后將返回?cái)?shù)據(jù)和responseId拼裝遮婶,最后通過src 重定向到UIWebview 的delegate蝗碎。
native得到數(shù)據(jù)后根據(jù)responseId調(diào)用事先裝入_responseCallbacks的block,動(dòng)態(tài)讀取調(diào)用旗扑,從而完成交互蹦骑。