WebViewJavascriptBridge
可以從github上看一下庫(kù)的簡(jiǎn)介,這是一個(gè)iOS/OSX上,用于WKWebView和UIWebView的讓Obj-C
和JavaScript
相互發(fā)送消息(交互)的橋接庫(kù)。
我們clone源碼,直接進(jìn)入主題。以下將基于源碼中的 /Example/ExampleApp-iOS 工程做分析
1.客戶端使用
ExampleUIWebViewController
- (void)viewWillAppear:(BOOL)animated {
if (_bridge) { return; }
UIWebView* webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:webView];
[WebViewJavascriptBridge enableLogging];
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
[_bridge setWebViewDelegate:self];
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];
[self renderButtons:webView];
[self loadExamplePage:webView];
}
創(chuàng)建webView田轧,enableLogging 方法用于打開調(diào)試信息。 bridgeForWebView 方法中創(chuàng)建了 WebViewJavascriptBridge 實(shí)例鞍恢,并設(shè)置webView的代理給self傻粘。著重看一下 registerHandler:handler: 方法。
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
messageHandlers是一個(gè)可變字典帮掉,key為要注冊(cè)的事件名稱弦悉,value為該事件的具體執(zhí)行。也就是蟆炊,當(dāng)js調(diào)用這個(gè)已經(jīng)注冊(cè)的handlerName事件時(shí)稽莉,會(huì)執(zhí)行OC中handler閉包的內(nèi)容。這也就實(shí)現(xiàn)了JS到OC的調(diào)用涩搓。
2.JS調(diào)用OC流程
## 假設(shè)webview頁(yè)面上有一個(gè)按鈕污秆,點(diǎn)擊后調(diào)用native方法后室。看一下js代碼:
var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
callbackButton.innerHTML = '點(diǎn)擊我,我會(huì)調(diào)用oc的方法'
callbackButton.onclick = function(e) {
e.preventDefault()
bridge.callHandler('loginAction', {'userId':'110','name': 'mcy'}, function(response) {
alert('收到oc過來的回調(diào):'+response)
})
}
主要用到了js文件中的callHandler方法混狠,它主要調(diào)用了 _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;
}
前文已經(jīng)提過,對(duì)于每一個(gè)JS和OC交互的事件疾层,OC都會(huì)向bridge庫(kù)注冊(cè)将饺。當(dāng)JS調(diào)用某一事件時(shí),會(huì)調(diào)用到function _doSend(message, responseCallback){}
方法:
- a.如果webview需要回調(diào)痛黎,即native方法執(zhí)行后需要回調(diào)給webview予弧,我們還需要給message附加一些額外信息。首先湖饱,根據(jù)當(dāng)前時(shí)間生成一個(gè)callbackId掖蛤,并以它為key值,將回調(diào)函數(shù)存放到responseCallbacks散列表中井厌,目的是客戶端能取到這個(gè)回調(diào)方法并執(zhí)行它蚓庭。
- b.把傳參message存入sendMessageQueue中(sendMessageQueue:就是一個(gè)數(shù)組,里面存放了交互所需的事件名稱)仅仆。
- c.所有準(zhǔn)備都做好后器赞,再修改iframe實(shí)例的src。
并不是有了 WebViewJavascriptBridge 才能實(shí)現(xiàn)web與native的交互墓拜。在webview的代理方法中攔截URL Scheme港柜,對(duì)指定的URL Scheme進(jìn)行處理也可以實(shí)現(xiàn)交互。(你最好對(duì)url scheme了解多一些咳榜。)
WebViewJavascriptBridge也是做了同樣的事夏醉。它生成了一個(gè)特定的scheme,便于客戶端攔截的時(shí)候識(shí)別它涌韩。
## 客戶端攔截scheme做了什么事情呢畔柔?
- (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;
if ([_base isWebViewJavascriptBridgeURL:url]) {
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];
} else {
[_base logUnkownMessage:url];
}
return NO;
} else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
} else {
return YES;
}
}
方法中isWebViewJavascriptBridgeURL
會(huì)判斷url是否是指定的scheme,然后區(qū)分是load型url還是message型url贸辈。
1)load型:注入javascript文件释树。
計(jì)算機(jī)學(xué)科里有一句名言:所有計(jì)算機(jī)中的問題,都能用添加一個(gè)中間層解決擎淤。我們能讓OC和JS兩種語(yǔ)言完成交互奢啥,主要是靠添加了一個(gè)中間層。所以嘴拢,加載完webview
之后桩盲,我們需要把這份JavaScript代碼
注入。
- (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];
}
}
}
a.WebViewJavascriptBridge_js()其實(shí)是一個(gè)js文件席吴,老的版本是寫在txt里的赌结,后來放在.m文件中捞蛋,聲明為一個(gè)string變量了。聲明如下柬姚,#define的寫法是為了免去換行要加\的煩惱拟杉。_evaluateJavascript會(huì)去執(zhí)行這個(gè)js文件。
b.startupMessageQueue 顧名思義就是存放消息事件的隊(duì)列量承,從中取出每一個(gè)事件搬设,一一分發(fā)。_dispatchMessage后文再分析撕捍。
我個(gè)人認(rèn)為拿穴,這個(gè)JavaScript中間層
就是一個(gè)中間調(diào)度層,讓2種語(yǔ)言在這里改裝適配
之后能被對(duì)方識(shí)別
忧风。
2)message型:
先看:
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
它會(huì)去調(diào)用js文件中的_fetchQueue()默色,如下:
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
sendMessageQueue 是一個(gè)消息隊(duì)列,存放web中需要調(diào)用native的事件消息狮腿。
flushMessageQueue: 方法自然就是處理拿到的事件腿宰。如下:
- (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;
}
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"];
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
} else {
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
handler(message[@"data"], responseCallback);
}
}
}
它先用 _deserializeMessageJSON 做了一下json解析。然后遍歷每一個(gè)事件:
1)有responseId:從_responseCallbacks散列表中取出響應(yīng)事件的回調(diào)作為responseCallback蚤霞。
2)無responseId酗失,有callbackId:生成一個(gè)新的responseCallback。
生成新的responseCallback昧绣,從messageHandlers中取出handlerName對(duì)應(yīng)的事件(最開始我們r(jià)egisterHandler時(shí)注冊(cè)進(jìn)去的)规肴,執(zhí)行該事件的回調(diào)。
到這里夜畴,JS就成功調(diào)用了OC的方法拖刃。我們還可以在回調(diào)中調(diào)用 responseCallback 告訴JS它調(diào)用成功了。
我們先看一下新生成的 responseCallback 里面做了什么事情贪绘。它是怎么"聯(lián)系"上JS的兑牡。
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
queueMessage 最后調(diào)用了dispatchMessage,如下:
- (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];
});
}
}
回傳參數(shù)被編碼為json格式税灌,然后在主線程中調(diào)用了JS文件中的 _handleMessageFromObjC('%@') 方法均函。而JS文件中最終被執(zhí)行的函數(shù)為:
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);
}
}
}
}
在 _doDispatchMessageFromObjC 中,如果有responseId菱涤,取出該responseId對(duì)應(yīng)的responseCallback苞也,并執(zhí)行(即web頁(yè)面的callHandler回調(diào))。
OC調(diào)用JS流程
前面JS調(diào)用OC講了很多代碼粘秆,估計(jì)一時(shí)半會(huì)兒很難消化如迟。別擔(dān)心,后面的內(nèi)容其實(shí)是相似的。native調(diào)用bridge的callHandler方法殷勘,這個(gè)方法的實(shí)現(xiàn)在bridge的.m文件中(JS調(diào)用native時(shí)也會(huì)調(diào)用callHandler此再,這個(gè)方法的實(shí)現(xiàn)在bridge的JS文件中)。它會(huì)去調(diào)用 sendData:responseCallback:handlerName: 方法玲销。
- (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];
}
這個(gè)方法最后還是調(diào)用了 dispatchMessage 去執(zhí)行JS文件中的 handleMessageFromObjC 方法输拇。這個(gè)方法做的事情和OC里的flushMessageQueue做的事情是相同的。
回頭對(duì)比一下2個(gè)核心bridge類的方法
OC: WebViewJavascriptBridgeBase(WebViewJavascriptBridge將其封裝了一份贤斜,便于我們使用它的功能)
JS: WebViewJavascriptBridge_JS(內(nèi)部就是一個(gè)js方法)
1)交互前都需要調(diào)用registerHandler淳附,在各自的messageHandlers散列表里存放事件和信息。
2)調(diào)用另一端的時(shí)候蠢古,都要調(diào)用callHandler,分別觸發(fā) sendData 和 _doSend 方法别凹。將各自的回調(diào)信息存放在responseCallbacks散列表中草讶。
不同的是,OC調(diào)用JS時(shí)炉菲,可以直接調(diào)用JS文件中的 handleMessageFromObjC 方法堕战。而JS調(diào)用OC就沒那么直接了。JS的doSend改變了iframe的src后拍霜,需要在webview的代理方法shouldStartLoadWithRequest中截取url scheme嘱丢,最終調(diào)用flushMessageQueue方法處理。
相信到這里之后祠饺,對(duì)于WebViewJavaScriptBridge是如何處理webview和js的交互越驻,你已經(jīng)有了一個(gè)大體的了解了。當(dāng)然道偷,有一些內(nèi)容還是可以去細(xì)想的缀旁。
這種實(shí)現(xiàn)并不是唯一的,但整體思路都是一致的勺鸦。我看了一下微信的JS橋接文件并巍,寫法和WebViewJavascriptBridge_JS略有不同,后續(xù)再研究研究~