前一篇文章中,我们大致的讲述了一下JavascriptCore这个库在iOS开发中的应用。在文中最后的阶段,我们提到了 WebViewJavascriptBridge 这个库。提到这个库,可能有一些人就要说了,现在都什么时代了,谁还会用这个库啊?全是坑!不错,早在三年前,这个库有过一段辉煌的时光,在苹果除了WKWebView之后,渐渐的使用这个库的人越来越少,尽管这个库也是支持了WKWebView的。 但是一个事物的存在就有他的价值,就算使用也不是那么频繁了,尽管他有很多的坑。但是对于一个开发者来说,我们应该取其精华去其糟粕,现如今出的很多的交互的bridge依旧是有部分交互逻辑沿用了WebViewJavascriptBridge的思想。 这里就不得不提味精大神的一片文章,这篇文章里面深入浅出的谈了谈现如今Hybrid开发时常用的一些桥方法。有兴趣的可以去关注一下。废话不多说,那么我们今天就从源码开始解析这个库的使用以及原理。
简单的来说,在最开始的UIWebView时,原生跟JS之间的交互一般是两种方式:
stringByEvaluatingJavascriptFromString:
方法,传入要执行的JS代码就可以实现; UIWebView
的代理方法 webView:shouldStartLoadWithRequest:navigationType:
中拦截相应的URL做处理。 当然这个其实也就是 WebViewJavascriptBridge 的理论核心。但是上面这种实现方法为什么没有人使用呢?原因就是,通过在代理方法里面拦截,我们就必不可少的要写很多的 if else
的代码。在项目中的混合插件越来越多的时候,就导致了这个代理方法里面的逻辑越来越臃肿,越来越难以维护。 那么 WebViewJavascriptBridge 的作用就是以更加优雅的方式,去实现Native与JS之间的互调。让Native能像调用OC的方法一样调用JS,同时JS也能像调用JS方法一样去调用OC。这就在OC和JS中间搭起了一座友谊的桥梁。
这里使用我就不多说了,直接 pod 'WebViewJavascriptBridge'
就可以引入到项目了。 附上源码地址: WebViewJavascriptBridge
WebViewJavascriptBridge参与交互的流程包括三个部分:初始化、JS调用Native、Native调用JS。接下来我们就一一分析其中的过程。
这里必须要说一下,WebViewJavascriptBridge的这个设计很巧妙,他在JS端和Native端,都各自初始化了一个WebViewJavascriptBridge对象,就像是两边各自安排了一个”通讯兵“,让这两个对象去完成消息的收发工作。同时两边还各自维护一个管理相应事件的messageHandlers容器、一个管理回调的callbackId容器。所以这里的初始化,我们得分为两个部分的初始化,一个部分是Native端的初始化,一个是JS端的初始化。这里我们都以UIWebView为例子讲解,WKWebView其实也是相类似的原理,可以类比一下。
WebViewJavascriptBridge
并且设置好代理 _bridge = [WebViewJavascriptBridge bridgeForWebView:webView]; [_bridge setWebViewDelegate:self];
- (void) _setupInstance:(WKWebView*)webView { _webView = webView; _webView.navigatiOnDelegate= self; _base = [[WebViewJavascriptBridgeBase alloc] init]; _base.delegate = self; }
然后其内部初始化了 WebViewJavascriptBridgeBase
类和相关的属性
- (id)init { if (self = [super init]) { self.messageHandlers = [NSMutableDictionary dictionary]; self.startupMessageQueue = [NSMutableArray array]; self.respOnseCallbacks= [NSMutableDictionary dictionary]; _uniqueId = 0; } return self; }
handler
,这个 handler
是提供给JS调用的 [_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) { NSLog(@"testObjcCallback called: %@", data); responseCallback(@"Response from testObjcCallback"); }];
注册其实就是在 messageHandlers
这个 NSMutableDictionary
里面保存一下
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler { _base.messageHandlers[handlerName] = [handler copy]; }
loadRequest
加载URL之后,网页一加载就会执行网页JS中的bridge的初始化方法 setupWebViewJavascriptBridge
函数 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) }
这里主要做了两件事情,一个是保存要执行的一直自定义初始化函数,比如注册JS中的 handler
,第二个就是通过添加一个 iframe
加载初始化链接 https://__bridge_loaded__
。
https://__bridge_loaded__
这个URL WebViewJavascriptBridge_JS
中的代码,初始化 window.WebViewJavascriptBridge
对象:首先在JS中创建一个 WebViewJavascriptBridge
对象,设置成window一个属性,然后定义几个用于管理消息的全局变量,接着给 WebViewJavascriptBridge
对象定义几个处理消息的方法和函数,执行Native端 startupMessageQueue
中保存的消息,也就是本地JS文件还未加载时就发送了的消息。 window.WebViewJavascriptBridge = { registerHandler: registerHandler, callHandler: callHandler, disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout, _fetchQueue: _fetchQueue, _handleMessageFromObjC: _handleMessageFromObjC };
callHandler()
方法,发消息给原生 bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) { log('JS got response', response) })
然后我们看看 callHandler
是怎么定义的
function callHandler(handlerName, data, responseCallback) { if (arguments.length == 2 && typeof data == 'function') { respOnseCallback= data; data = null; } _doSend({ handlerName:handlerName, data:data }, responseCallback); }
那么这个 _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; }
这下我们清楚了,原来我们在传入 handlerName
和 data
被包装成了一个 message
传入到 _doSend
函数,然后生成一个 callbackId
,也一道包装到 message
中去。这样三个数据都被打包成了一个 message
传到Native。 当然为什么要传入一个 callbackId
进去呢?这是因为用于处理原生回调的 responseCallback
是一个函数,是不能直接传给原生的,所以这里就把这个 responseCallback
存到了一个全局的 responseCallbacks
对象的属性里面去,属性名就是 responseCallback
对应的id。这个地方就是为了后面Native回调JS时,根据id找到对应的 responseCallback
。
在上图中的最后一步指的是JS会在 iframe
中加载发送消息的URL,此时原生就可以在相应的代理中拦截到这个URL,然后就知道JS端给我传递消息了,然后Native端会去调用JS,把 sendMessageQueue
中的 message
取出来,转成 JSON string 的格式。接着原生把 JSON string 解析成字典,取出相应的 data
、 callbackId
和 handlerName
。最后根据 handlerName
去先前的 messageHanlers
里面取出相对应的 block(handler)
,然后调用这个 block
, data
作为第一个参数,第二个参数是根据 callbackId
创建的 responseCallback(block)
,然后原生就可以在 block(handler
)中处理接收到的 data
以及回调JS了。
如果说需要原生给JS回调的话,当这个 responseCallback
被回调的时候,会执行下面的代码
- (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]; }
这里就是直接创建了一个 message
(NSMutableDictionary)对象,把 data
、 callbackId
和 handlerName
封装之后转换成为JSON string,最后调用 WebViewJavascriptBridge._handleMessageFromObjC('%@')
这个方法,把 message
传给JS。
- (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]; }); } }
在JS接收到了这个 message
之后,会根据里面的 callbackId
找到之前的 responseCallback
,把 data
作为参数,回调这个 responseCallback
。
其Native调用JS和上面JS调用Native是有很多的相似之处的。当然,其实也是可以直接通过web view执行JS脚本去实现的。但是 WebViewJavascriptBridge
使用了一套更加规范的调用方式。接下来来介绍一下这种方式。
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];
这个方法跟JS里面的这个方法名是一样的,当然实际的作用其实也是相似的。 在这里都是将 handlerName
、 data
和 responseCallback
对应的id包装成一个 message
。然后把这个 message
对象转成JSON string。最后在调用 WebViewJavascriptBridge._handleMessageFromObjC(messageJSON)
方法把数据给到JS。这里至于为什么也是传id,其实原理跟上面是一样的,block也是不能直接传给JS的,所以这里把 responseCallback
的这个block存到了全局的 responseCallbacks
字典里面去了,key就是 responseCallback
对应的id。JS回调Native的时候,就会来这个字典里面去取对应的block。其实思想都是差不多的。
data
、 callbackId
和 handlerName
。然后根据 handlerName
去 messageHandlers
里面去对应的handler函数,然后去执行这个函数。第一个参数是传过来的 data
,第二个参数就是根据 callbackId
创建的 responseCallback
的function。这里就可以在 handler
里面处理接收到的回调了。 message
里面的东西不一样了; 跟上面JS调用Native不一样的就是 message
里面现在不需要你传一个 callbackId
了,因为这里本来就是JS回调给Native的,再传这个,两边就一直在回调来回调去了。但是呢,多了一个 responseId
,这是因为Native执行JS回调的时候,会根据这个 responseId
从 responseCallbacks
中去取对应的block
`` WVJBResponseCallback respOnseCallback= _responseCallbacks[responseId]; ```
responseId
找到之前保存的 responseCallback
的block,然后把 message
中的 responseData
(其实就是data)作为参数回调给这个responseCallback。与JS调用Native不同的其实就是这里的 responseCallback
只有一个 data
参数了,是没有用于再次回调JS的block了。 以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 我们