之前有中间件项目用到了WebViewJavascriptBridge这个库,当时捉摸了一下原理,现在拿出来给大家分享一下,还是以官方给的demo为例,把复杂的代码精简到早简单的一条调用线直观的给大家展示调用过程
在交互过程中,外部只需要调用两个方法就能完成这个交互的过程
JS端注册方法
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中的接收方法
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];
就完成了一次调用过程
基本原理解析
大家都知道的通常的manual调用OC方法的方案是通过自定义协议和url,再通过webview的delegate截获webview的url跳转事件来实现JS调用OC方法的,WVJB也很相似,但是多了几步,了解了这个基本原理就来看看WVJB在完成交互调用前做了什么工作
WVJB前置工作
1.OC端
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
[_bridge setWebViewDelegate:self];
在OC端做的前置工作比较少但是后续的还是比较多的,和交互有关的其实就这么两句话,第一句是创建一个桥接初始化,因为要兼容WKWebview和UIWebview所以在这个方法中做了判断,第二步是把要进行交互的webview的代理挂载到这个桥上,如刚才讲的基本原理一样,因为它需要代替我们截获url跳转事件所以要拿到这个代理,另一个前置工作是在代理事件里面在这个Webview的请求开始前注入了一段JS代码
2.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)
}
setupWebViewJavascriptBridge(function(bridge) {...}
WVJB在调用前需要在网页中写如上的JS代码,作用是创建了一个不可见的iframe进行发出url跳转事件用于让OC截获,之后把你想进行交互的代码放在一个匿名函数中当参数传给setupWebViewJavascriptBridge这个方法就完成了
这样一来基本原理中所需要的两个基本点就都具备了,一个发url跳转的东西和一个截获url跳转的东西,接下来开始详细解析一个JS代码调用OC的流程
具体调用流程
首先在OC中注册了这个方法用于接收JS发的消息
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];
registerHandler方法只做了一件事,把第一个参数当做key,后面的回调函数当做value存放到一个叫messageHandlers的字典中,OC端就完成了。
在JS中写的
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)
})
}
实际上是调用了在webview载入前注入网页中的一串实现准备好的JS代码中方法,这串JS详见WebViewJavascriptBridge_JS.m文件,
bridge.callHandler这个方法需要三个参数,第一个参数为调用方法名,第二个参数为方法要传的参数,但三个参数为方法回调
这个方法是这样的
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[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
callHandler这个方法把方法名和要传的参数合并成一个数组转发给doSend方法,doSend方法做了以下几件事:
1.把回调进行格式化并存放到回调方法数组中
2.给传进来的回调方法添加一个独一无二的ID并且根据ID存进回调方法数组,然后把这个ID也合并到messsage里
3.把传进的方法名和参数合并数据添加到信息数组
4.修改iframe的url发送跳转信息让OC截获,而这个跳转信息是定死的https://wvjb_queue_message,就只用来通知OC有消息可以接收了,不传送任何具体数据
之后OC的WVJB管理的webview delegate截获到这条消息就判断一下是不是这个wvjb_queue_message消息,是的话就开始进行方法调用,不是就让它正常跳转,当接收到这个wvjb_queue_message消息的时候OC会调用一个名为_fetchQueue的JS方法,这个方法也是预先注入到网页中的
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
可以看到这个方法把JS管理的消息数组中的消息取出来格式化成字符串返回给OC并把消息数组清空,之后OC把这个字符串发给flushMessageQueue方法进行处理,
- (void)flushMessageQueue:(NSString *)messageQueueString{
id messages = [self _deserializeMessageJSON:messageQueueString];
...
WVJBResponseCallback responseCallback = NULL;
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
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
};
}
handler(message[@"data"], responseCallback);
上面是精简过flushMessageQueue方法我,本来的这个方法还判断了传进来的参数什么的,并且还有OC调用JS的逻辑,这里我全部精简掉了只留下了JS调用OC所需的几句代码,然后可以清晰的看出他做的几件事
1.把这个字符串反解成Json
2.分别取出来参数和方法名
3.从之前OC注册时添加到的self.messageHandlers字典中通过key取出对应的匿名函数然后
4.调用这个匿名函数
5.查看是消息里面是否包含callbackId,如果有就通过_queueMessage处理一下
就这样一次调用就完成了
附加
说完了一次完整的调用过程,�然后来讲讲JS的回调是怎么完成的,其实也和之前的流程差不多,�callbackId在flushMessageQueue中发现后发给_queueMessage以后_queueMessage又调用了_dispatchMessage方法,这个方法就会处理处理这条消息之后返回给JS
- (void)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
[self _log:@"SEND" json:messageJSON];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
...
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}
这个方法做的几件事:
1.将message序列化
2.将序列化后的消息格式化转义,斜杠啊什么的
3.�由于JS是单线程的所以要拿到一条线程JS同步执行
4.调用JS的_handleMessageFromObjC方法
_handleMessageFromObjC这个方法也没什么好说的了和之前很类似,当这个方法拿到callbackID后会去回调数组通过这个key找到对应的回调方法然后调用,就完成了JS的完成回调
总结
总的来说就是OC添加了一个以方法名为key,匿名函数为value的值到消息接收数组中,之后JS调用方法时把方法名和传入参数合并作为一个数组放到消息数组中通知OC有消息可以接收,OC端截获有消息的通知调用JS的fetchqueue方法获取到消息数组中的内容然后反解出来方法名和参数再从OC注册的方法数组中通过方法名取出对应的方法调用