WebViewJavaScriptBridge深入剖析
栏目: JavaScript · 发布时间: 5年前
内容简介:前一篇文章中,我们大致的讲述了一下JavaScriptCore这个库在iOS开发中的应用。在文中最后的阶段,我们提到了简单的来说,在最开始的UIWebView时,原生跟JS之间的交互一般是两种方式:当然这个其实也就是
前一篇文章中,我们大致的讲述了一下JavaScriptCore这个库在iOS开发中的应用。在文中最后的阶段,我们提到了 WebViewJavaScriptBridge 这个库。提到这个库,可能有一些人就要说了,现在都什么时代了,谁还会用这个库啊?全是坑!不错,早在三年前,这个库有过一段辉煌的时光,在苹果除了WKWebView之后,渐渐的使用这个库的人越来越少,尽管这个库也是支持了WKWebView的。 但是一个事物的存在就有他的价值,就算使用也不是那么频繁了,尽管他有很多的坑。但是对于一个开发者来说,我们应该取其精华去其糟粕,现如今出的很多的交互的bridge依旧是有部分交互逻辑沿用了WebViewJavaScriptBridge的思想。 这里就不得不提味精大神的一片文章,这篇文章里面深入浅出的谈了谈现如今Hybrid开发时常用的一些桥方法。有兴趣的可以去关注一下。废话不多说,那么我们今天就从源码开始解析这个库的使用以及原理。
简介
简单的来说,在最开始的UIWebView时,原生跟JS之间的交互一般是两种方式:
- Native -> JS :这种方式很简单,只是是原生调用
stringByEvaluatingJavaScriptFromString:
方法,传入要执行的JS代码就可以实现; - JS -> Native :这种方式是在网页上面加载一串Custom URL Scheme的URL,然后通过原生去
UIWebView
的代理方法webView:shouldStartLoadWithRequest:navigationType:
中拦截相应的URL做处理。
当然这个其实也就是 WebViewJavaScriptBridge 的理论核心。但是上面这种实现方法为什么没有人使用呢?原因就是,通过在代理方法里面拦截,我们就必不可少的要写很多的 if else
的代码。在项目中的混合插件越来越多的时候,就导致了这个代理方法里面的逻辑越来越臃肿,越来越难以维护。 那么 WebViewJavaScriptBridge 的作用就是以更加优雅的方式,去实现Native与JS之间的互调。让Native能像调用OC的方法一样调用JS,同时JS也能像调用JS方法一样去调用OC。这就在OC和JS中间搭起了一座友谊的桥梁。
使用
这里使用我就不多说了,直接 pod 'WebViewJavascriptBridge'
就可以引入到项目了。 附上源码地址: WebViewJavaScriptBridge
目录结构
- WebViewJavaScriptBridgeBase :bridge的核心类,用来初始化以及消息的处理;
- WebViewJavaScriptBridge :判断WebView的类型,并通过不同的类型进行分发。针对UIWebView和WebView做的一层封装,主要从来执行JS代码,以及实现UIWebView和WebView的代理方法,并通过拦截URL来通知WebViewJavaScriptBridgeBase做的相应操作;
- WKWebViewJavaScriptBridge :主要是针对WKWebView做的一些封装,主要也是执行JS代码和实现WKWebView的代理方法的。同上面这个类类似;
- WebViewJavaScriptBridge_JS:里面主要写了一些JS的方法,JS端与Native”互动“的JS端的方法基 本上都在这个里面;
主要流程
WebViewJavaScriptBridge参与交互的流程包括三个部分:初始化、JS调用Native、Native调用JS。接下来我们就一一分析其中的过程。
1、初始化
这里必须要说一下,WebViewJavaScriptBridge的这个设计很巧妙,他在JS端和Native端,都各自初始化了一个WebViewJavaScriptBridge对象,就像是两边各自安排了一个”通讯兵“,让这两个对象去完成消息的收发工作。同时两边还各自维护一个管理相应事件的messageHandlers容器、一个管理回调的callbackId容器。所以这里的初始化,我们得分为两个部分的初始化,一个部分是Native端的初始化,一个是JS端的初始化。这里我们都以UIWebView为例子讲解,WKWebView其实也是相类似的原理,可以类比一下。
(1)、Native端的初始化
- 首先初始化
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]; } 复制代码
(2)、web view端的初始化
- 当我们通过
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__
。
- Native端会拦截
https://__bridge_loaded__
这个URL
- 在webview中执行本地
WebViewJavaScriptBridge_JS
中的代码,初始化window.WebViewJavaScriptBridge
对象:首先在JS中创建一个WebViewJavaScriptBridge
对象,设置成window一个属性,然后定义几个用于管理消息的全局变量,接着给WebViewJavaScriptBridge
对象定义几个处理消息的方法和函数,执行Native端startupMessageQueue
中保存的消息,也就是本地JS文件还未加载时就发送了的消息。
window.WebViewJavascriptBridge = { registerHandler: registerHandler, callHandler: callHandler, disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout, _fetchQueue: _fetchQueue, _handleMessageFromObjC: _handleMessageFromObjC }; 复制代码
2、JS调用Native
- JS中调用
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
。
2、Native调用JS
其Native调用JS和上面JS调用Native是有很多的相似之处的。当然,其实也是可以直接通过web view执行JS脚本去实现的。但是 WebViewJavaScriptBridge
使用了一套更加规范的调用方式。接下来来介绍一下这种方式。
- Native调用callHandler()方法,把消息发送给JS
[_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。其实思想都是差不多的。
- JS端拿到了这个message之后,会将它解析成为JS对象,然后去使用
data
、callbackId
和handlerName
。然后根据handlerName
去messageHandlers
里面去对应的handler函数,然后去执行这个函数。第一个参数是传过来的data
,第二个参数就是根据callbackId
创建的responseCallback
的function。这里就可以在handler
里面处理接收到的回调了。 - 这里与前面JS调Native时Native回调JS的处理不太一样,因为JS调Native是不能直接调的。但是怎么去通知Native呢?其实他这里就是直接走了JS调用Native的流程,就是上面提到的这个流程。不过还是有不同的:
- 一是
message
里面的东西不一样了; - 二是Native对message的处理:
-
跟上面JS调用Native不一样的就是
message
里面现在不需要你传一个callbackId
了,因为这里本来就是JS回调给Native的,再传这个,两边就一直在回调来回调去了。但是呢,多了一个responseId
,这是因为Native执行JS回调的时候,会根据这个responseId
从responseCallbacks
中去取对应的block`` WVJBResponseCallback responseCallback = _responseCallbacks[responseId]; ``` 复制代码
- Native在收到JS回调之后,会根据
responseId
找到之前保存的responseCallback
的block,然后把message
中的responseData
(其实就是data)作为参数回调给这个responseCallback。与JS调用Native不同的其实就是这里的responseCallback
只有一个data
参数了,是没有用于再次回调JS的block了。
- Native在收到JS回调之后,会根据
-
- 一是
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
RESTful Web Services Cookbook
Subbu Allamaraju / Yahoo Press / 2010-3-11 / USD 39.99
While the REST design philosophy has captured the imagination of web and enterprise developers alike, using this approach to develop real web services is no picnic. This cookbook includes more than 10......一起来看看 《RESTful Web Services Cookbook》 这本书的介绍吧!