WebViewJavascriptBridge
可以从github上看一下库的简介,这是一个iOS/OSX上,用于WKWebView和UIWebView的让Obj-C
和JavaScript
相互发送消息(交互)的桥接库。
我们clone源码,直接进入主题。以下将基于源码中的 /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];
}
创建webView,enableLogging 方法用于打开调试信息。 bridgeForWebView 方法中创建了 WebViewJavascriptBridge 实例,并设置webView的代理给self。着重看一下 registerHandler:handler: 方法。
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
messageHandlers是一个可变字典,key为要注册的事件名称,value为该事件的具体执行。也就是,当js调用这个已经注册的handlerName事件时,会执行OC中handler闭包的内容。这也就实现了JS到OC的调用。
2.JS调用OC流程
## 假设webview页面上有一个按钮,点击后调用native方法。看一下js代码:
var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
callbackButton.innerHTML = '点击我,我会调用oc的方法'
callbackButton.onclick = function(e) {
e.preventDefault()
bridge.callHandler('loginAction', {'userId':'110','name': 'mcy'}, function(response) {
alert('收到oc过来的回调:'+response)
})
}
主要用到了js文件中的callHandler方法,它主要调用了 _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;
}
前文已经提过,对于每一个JS和OC交互的事件,OC都会向bridge库注册。当JS调用某一事件时,会调用到function _doSend(message, responseCallback){}
方法:
- a.如果webview需要回调,即native方法执行后需要回调给webview,我们还需要给message附加一些额外信息。首先,根据当前时间生成一个callbackId,并以它为key值,将回调函数存放到responseCallbacks散列表中,目的是客户端能取到这个回调方法并执行它。
- b.把传参message存入sendMessageQueue中(sendMessageQueue:就是一个数组,里面存放了交互所需的事件名称)。
- c.所有准备都做好后,再修改iframe实例的src。
并不是有了 WebViewJavascriptBridge 才能实现web与native的交互。在webview的代理方法中拦截URL Scheme,对指定的URL Scheme进行处理也可以实现交互。(你最好对url scheme了解多一些。)
WebViewJavascriptBridge也是做了同样的事。它生成了一个特定的scheme,便于客户端拦截的时候识别它。
## 客户端拦截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
会判断url是否是指定的scheme,然后区分是load型url还是message型url。
1)load型:注入javascript文件。
计算机学科里有一句名言:所有计算机中的问题,都能用添加一个中间层解决。我们能让OC和JS两种语言完成交互,主要是靠添加了一个中间层。所以,加载完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()其实是一个js文件,老的版本是写在txt里的,后来放在.m文件中,声明为一个string变量了。声明如下,#define的写法是为了免去换行要加\的烦恼。_evaluateJavascript会去执行这个js文件。
b.startupMessageQueue 顾名思义就是存放消息事件的队列,从中取出每一个事件,一一分发。_dispatchMessage后文再分析。
我个人认为,这个JavaScript中间层
就是一个中间调度层,让2种语言在这里改装适配
之后能被对方识别
。
2)message型:
先看:
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
它会去调用js文件中的_fetchQueue(),如下:
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
sendMessageQueue 是一个消息队列,存放web中需要调用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解析。然后遍历每一个事件:
1)有responseId:从_responseCallbacks散列表中取出响应事件的回调作为responseCallback。
2)无responseId,有callbackId:生成一个新的responseCallback。
生成新的responseCallback,从messageHandlers中取出handlerName对应的事件(最开始我们registerHandler时注册进去的),执行该事件的回调。
到这里,JS就成功调用了OC的方法。我们还可以在回调中调用 responseCallback 告诉JS它调用成功了。
我们先看一下新生成的 responseCallback 里面做了什么事情。它是怎么"联系"上JS的。
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
queueMessage 最后调用了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];
});
}
}
回传参数被编码为json格式,然后在主线程中调用了JS文件中的 _handleMessageFromObjC('%@') 方法。而JS文件中最终被执行的函数为:
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对应的responseCallback,并执行(即web页面的callHandler回调)。
OC调用JS流程
前面JS调用OC讲了很多代码,估计一时半会儿很难消化。别担心,后面的内容其实是相似的。native调用bridge的callHandler方法,这个方法的实现在bridge的.m文件中(JS调用native时也会调用callHandler,这个方法的实现在bridge的JS文件中)。它会去调用 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];
}
这个方法最后还是调用了 dispatchMessage 去执行JS文件中的 handleMessageFromObjC 方法。这个方法做的事情和OC里的flushMessageQueue做的事情是相同的。
回头对比一下2个核心bridge类的方法
OC: WebViewJavascriptBridgeBase(WebViewJavascriptBridge将其封装了一份,便于我们使用它的功能)
JS: WebViewJavascriptBridge_JS(内部就是一个js方法)
1)交互前都需要调用registerHandler,在各自的messageHandlers散列表里存放事件和信息。
2)调用另一端的时候,都要调用callHandler,分别触发 sendData 和 _doSend 方法。将各自的回调信息存放在responseCallbacks散列表中。
不同的是,OC调用JS时,可以直接调用JS文件中的 handleMessageFromObjC 方法。而JS调用OC就没那么直接了。JS的doSend改变了iframe的src后,需要在webview的代理方法shouldStartLoadWithRequest中截取url scheme,最终调用flushMessageQueue方法处理。
相信到这里之后,对于WebViewJavaScriptBridge是如何处理webview和js的交互,你已经有了一个大体的了解了。当然,有一些内容还是可以去细想的。
这种实现并不是唯一的,但整体思路都是一致的。我看了一下微信的JS桥接文件,写法和WebViewJavascriptBridge_JS略有不同,后续再研究研究~