强烈推荐:iOS源码补完计划-WebViewJavascriptBridge实现原理
iOS下JS与OC互相调用(六)--WKWebView + WebViewJavascriptBridge
JS调用OC
//将版本信息发送给Html
[_WKwebViewBridge registerHandler:@"GetCurrentVersion" handler:^(id data, WVJBResponseCallback responseCallback) {
// 获取当前版本号
NSString *appVersion = @"1.0.0";
// 反馈给JS
responseCallback(appVersion);
}];
#import "WebViewJavascriptBridge.h"
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
- _base
@implementation WKWebViewJavascriptBridge {
WebViewJavascriptBridgeBase *_base;
}
WebViewJavascriptBridge所持有的WebViewJavascriptBridgeBase(简称base)对象。 - messageHandlers
@property (strong, nonatomic) NSMutableDictionary* messageHandlers;
字典。存储了注册的方法名、ballback。
就是在注册的时候将方法名、block。存储起来备用。
然后、线索断了。也就是说、ios这边主动做的事情、已经没了。
既然存储起来了,那又在什么地方使用了呢?我们搜索messageHandlers
看看
进一步,我们查看messageHandlers调用的地方 - (void)flushMessageQueue:(NSString *)messageQueueString
- (void)flushMessageQueue:(NSString *)messageQueueString{
if (messageQueueString == nil || messageQueueString.length == 0) {
// 省略 ..........
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
handler(message[@"data"], responseCallback);
}
}
}
messageQueueString :是一个字符串,内部数据格式如下:
"[{"handlerName":"GetCurrentVersion","data":{},"callbackId":"cb_2_1548142676517"}]"
message[@"data"]: 我们注册时候的参数。
responseCallback:显而易见是我们注册时候的回调函数。
当handler(message[@"data"], responseCallback);
运行我们注册的回调函数,会回到我们注册的地方:
运行OC代码,向JS反馈信息调用responseCallback(appVersion)
,又回到了- (void)flushMessageQueue:(NSString *)messageQueueString
中
- (void)flushMessageQueue:(NSString *)messageQueueString{
// 省略 ..............
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
};
}
// 省略 ..............
}
发现我们传递过来的appVersion 就是 responseData 的值,并且重新整理成一个WVJBMessage(也就是NSDictionary)对象。
- (void)_queueMessage:(WVJBMessage*)message {
if (self.startupMessageQueue) {
[self.startupMessageQueue addObject:message];
} else {
[self _dispatchMessage:message];
}
}
- (void)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
[self _log:@"SEND" json:messageJSON];
// 对json字符串进行一系列格式化处理
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];
});
}
}
message 的值
{
responseData = "1.1.3";
responseId = "cb_2_1548206601903";
}
javascriptCommand 的值
WebViewJavascriptBridge._handleMessageFromObjC('{\"responseId\":\"cb_2_1548206601903\",\"responseData\":\"1.1.3\"}');
将获取的数据整理成一个WVJBMessage(也就是NSDictionary)对象后,调用_evaluateJavascript:方法,底层是让webview去注入这段js函数
至于_handleMessageFromObjC的实现,就是属于WebViewJavascriptBridge_js文件中的范畴了。一会从js端切入的时候再去看。
再回过头来看看-(void)flushMessageQueue:(NSString *)messageQueueString;方法是如何被调用的
再次搜索、很明显了、是拦截协议并且判断复合要求之后直接调用的。没什么太绕的东西。
- (void)webView:(WKWebView *)webView
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (webView != _webView) { return; }
NSURL *url = navigationAction.request.URL;
__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
// js通过Bridge发起的url
if ([_base isCorrectProcotocolScheme:url]) {
if ([_base isBridgeLoadedURL:url]) {
// 注入js(WebViewJavascriptBridge_js)
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
// js主动调启oc,也就是我们上面分析的步骤
[self WKFlushMessageQueue];
} else {
[_base logUnkownMessage:url];
}
// 拦截
decisionHandler(WKNavigationActionPolicyCancel);
}
else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
[_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
} else {
// 不拦截,正常回调给webView的VC
decisionHandler(WKNavigationActionPolicyAllow);
}
}
- (void)WKFlushMessageQueue {
// webView执行JS `WebViewJavascriptBridge._fetchQueue();`
[_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) {
if (error != nil) {
NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
}
[_base flushMessageQueue:result];
}];
}
WKWebView执行JSWebViewJavascriptBridge._fetchQueue();
,得到result
"[{"handlerName":"GetCurrentVersion","data":{},"callbackId":"cb_2_1548208471860"}]"
但是又如何触发 webView:decidePolicyForNavigationAction:decisionHandler:
这个代理方法呢?
js中调用Native注册的方法
// app.html
bridge.callHandler('getUserId','参数不需要的话可以省略不谢',function(response){
log(response.userId)
})
// WebViewJavascriptBridge_JS
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
进行了一些参数处理(js中很多都会根据传入参数数量的不同、内部进行进一步处理),处理结束直接丢给_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的callback函数在这里会被保存起来。以callbackId为键保存在responseCallbacks这个字典中、将来可以根据callbackId获取、完成回调。
callbackId也作为新的参数、添加进了message字典中。
messagingIframe:
这个应该比较容易理解。iframe是一个内嵌的网页标签。你既然修改了对应的src(链接)、webView自然会收到一个重定向的请求。sendMessageQueue
既然修改了iframe的src、让webVIew拦截了协议。sendMessageQueue自然就是为了提供参数而存在的了。
我们来找找看(搜索sendMessageQueue)。
//WebViewJavascriptBridge_JS
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
//#import "WebViewJavascriptBridgeBase.h"
- (NSString *)webViewJavascriptFetchQueyCommand {
return @"WebViewJavascriptBridge._fetchQueue();";
}
- (void)webView:(WKWebView *)webView
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
/*** 省略 ***/
// js主动调启oc
[self WKFlushMessageQueue];
/*** 省略 ***/
}
- (void)WKFlushMessageQueue {
// webView执行JS `WebViewJavascriptBridge._fetchQueue();`
[_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) {
if (error != nil) {
NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
}
[_base flushMessageQueue:result];
}];
}
这里修改src =
wvjbscheme://__WVJB_QUEUE_MESSAGE__
,也就是我们后面拦截协议时,拦截到的url,那么参数也就是 webview执行JSWebViewJavascriptBridge._fetchQueue();
从而获取sendMessageQueue.push(message)
传递出来的值。
总结
1、OC端注册,将 方法名 + 回调 存储到
messageHandlers
中;
2、JS发出请求,修改iframe的src,进行重定向,webview触发代理回调webView: decidePolicyForNavigationAction: decisionHandler:
进行协议拦截;
3、OC拦截到URL,注意这里的URL并不是将参数、callbackId等直接作为url发送出来,而是wvjbscheme://__WVJB_QUEUE_MESSAGE__
,那么参数又是怎么来的呢?
4、wkwebview执行JS代码WebViewJavascriptBridge._fetchQueue();
从而获得了具体的参数:"[{"handlerName":"GetCurrentVersion","data":{},"callbackId":"cb_2_1548208471860"}]"
;
5、拿到这些参数后,将存储在messageHandlers
中的block执行,也就是执行注册时的回掉了(这里可以执行OC相关代码,我们这里是获取版本号)。
6、当拿到版本号后,需要反馈给JS,将拿到的数据整理成新的数据@{ @"responseId":callbackId, @"responseData":responseData };
,调用bridgejs文件中的_handleMessageFromObjC
方法。将返回值callback给js中的指定callback。
OC调用JS
先看js文件,还是想先从注册看起。
js中的bridge实例初始化。
var _setupWebViewJavascriptBridge = function(callback){ //ios桥函数
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 = 'wvjbscheme://__BRIDGE_LOADED__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
};
js中注册方法的代码
bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
var responseData = { 'Javascript Says':'Right back atcha!' };
if(responseCallback) {
responseCallback(responseData);
};
});
// WebViewJavascriptBridge_JS.h
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
OC又是如何调用的呢?
[_WKwebViewBridge callHandler:@"testJavascriptHandler" data:@"some message"];
- (void)_dispatchMessage:(WVJBMessage*)message {
/*** 省略 ***/
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];
}
/*** 省略 ***/
}
_handleMessageFromObjC又做了啥?
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
拿到OC发来的messageJSON。里面有responseId/handlerName以及responseData。然后通过responseId将js中对应的callback调起/执行指定已经注册函数。