对于任意hybrid APP,不可避免进行native与web之间的交互。WebViewJavascriptBridge 就是一款用于实现原生端与web端无缝交互的三方库,应用广泛,支持UIWebView、WKWebView(iOS)以及WebView(OSX),原理一致,本文借助OC的UIWebView进行分析。
框架简介
所谓交互,无非就是两端(native端与JS端)能够互相调用方法,并传递数据。iOS7之后,随着JavaScriptCore框架的推出,为native与JS的交互铺平了道路,使用该框架即可实现OC与JS的互调。而在iOS7之前,OC与jS的交互只能借助于WebView,OC调用JS依赖于WebView提供的执行JS的方法,而JS调用OC则需要采用URL拦截的方式。WebViewJavascriptBridge就是为WebView中的JS交互而生的,采用后者的交互方式。
大致原理为OC端和JS端各自保存一个bridge对象,并各自维护开放给另一端调用的方法集合以及回调方法集合,两端之间的交互通过传递handleName(可以理解为方法id)以及callBackId来实现方法调用以及回调的。
代码文件结构如下(V6.0.2版本):
WebViewJavascriptBridge
基于UIWebView/WebView的OC端交互逻辑处理类,面向OC业务层,提供了注册OC方法、调用JS方法等接口。
WebViewJavascriptBridgeBase
OC端bridge对象基础服务类,维护OC端开放给JS端的方法以及OC回调方法,实现OC向JS发送数据的具体逻辑。
WebViewJavascriptBridge_JS
维护了一份JS代码,用于JS环境的注入。同时维护JS端的bridge对象,管理JS端注册的方法集合以及回调方法集合,面向Web端提供注册JS方法、调用OC端方法的接口。
WKWebViewJavascriptBridge
基于WKWebView的OC端交互逻辑处理类,职能同WebViewJavascriptBridge。
ExampleApp.html
非框架文件,由于我们借助WebViewJavascriptBridge的官方example讲解,该文件作为模拟开发环境的web页面示例。
Bridge环境初始化
OC端bridge初始化
业务层创建好webView实例之后,需要根据此webView创建对应的Bridge,即初始化bridge对象:
|
|
相关实现如下(仅保留相关代码):
|
|
OC端的bridge初始化即为webView实例创建了一个bridge对象,并初始化了相关的数据结构。
JS端Bridge初始化
JS端bridge对象的初始化要比OC端复杂得多,因为JS环境的初始化也是在原生端完成的,涉及原生端对webView初始化JS环境操作指令的捕捉,以及JS代码的注入。
webView加载时,执行了如下JS:
|
|
传入的参数是一个函数,暂且不管该函数的内容。首先判断当前环境中是否存在WebViewJavascriptBridge对象,若存在则直接调用传入的函数并已WebViewJavascriptBridge作为参数。该WebViewJavascriptBridge就是JS端的bridge对象,页面首次加载时,JS环境没有初始化好,也就是该Bridge对象没有创建好,因此会先将传入的函数暂存在数组WVJBCallbacks中。
重点看 WVJBIframe.src = 'https://__bridge_loaded__'; ,iframe可以理解为webView的窗口,当改变iframe的src时,页面就会进行刷新并加载指定的URL,此处试图加载 https://__bridge_loaded__。
我们知道,UIWebView刷新页面前,会先回调 shouldStartLoadWithRequest 方法。因此可以在该代理方法中拦截该URL的加载,并执行相应的逻辑。可以在 WebViewJavascriptBridge.m 文件中看到如下代码:
|
|
重点看中间部分,主要拦截了两种请求:JS环境初始化请求和web端交互请求,都是由WebViewJavascriptBridge内部发起的。
|
|
因此,之前页面中加载的 https://__bridge_loaded__ 就会被拦截,并识别为JS环境初始化请求,由OC端执行相应的的初始化逻辑。
|
|
该方法中执行了WebViewJavascriptBridge_JS文件中的js代码。为方便阅读,我把JS代码拎出来如下:
|
|
分析一下,所做的事情很简单,就是创建了一个bridge对象,该对象维护了若干函数,OC端可以通过bridge对象获取到相应的函数来执行调用。
registerHandler
用于JS端注册供OC端调用的方法
callHandler
用于调用OC方法
disableJavscriptAlertBoxSafetyTimeout
关闭消息异步发送
_fetchQueue
获取当前JS端待发送的消息
_handleMessageFromObjC
接收并处理OC端发送给JS端的数据
另外,代码最后执行了_callWVJBCallbacks 函数 ,还记得之前webView加载时暂存在WVJBCallbacks的函数吗?那时因为bridge未初始化先暂存起来,现在bridge初始化完毕了再来执行。
至此JS环境初始化完成。不过上面 injectJavascriptFile 方法中除了执行JS代码,还执行了如下代码:
|
|
startupMessageQueue是OC端bridge维护的属性,之前介绍过其用于暂存JS环境初始化之前OC发起的JS消息调用。现在JS环境初始化好了,便可取出其中的消息发送了。
OC调用JS
OC调用JS可以分为4个过程:
- JS注册方法给OC
- OC调用JS注册的方法
- JS方法被调用,执行自身逻辑
- JS回调数据给OC
JS注册方法
毫无疑问,这一步应该在webView加载时执行。还记得ExampleApp.html在加载时传了一个函数给 setupWebViewJavascriptBridge() 吗。这个函数后面被执行,函数内容就脑阔相关JS方法的注册。示例中该函数相关内容如下:
|
|
没错,就是通过调用bridge对象的registerHandler方法来注册的。实现很简单:
|
|
单纯只是把handleName及方法实现绑定在一起,保存在messageHandlers中。
OC发起调用
OC端通过JS注册的handleName调用JS相应的方法,同时可以设置回调操作。
|
|
|
|
JS接收调用
JS通过bridge的 _handleMessageFromObjC 方法接收OC的消息,具体实现如下:
|
|
首先解析OC传过来的数据,然后判断数据中是否存在responseId字段,若存在,则被认为这是一个OC回调的消息,否则被认为是OC发起的消息。此处不存在responseId,因为当前场景为OC发起的消息。但是因为消息数据中含有callbackId,被认为是OC需要回调,因此创建一个callBack函数,函数内容为调用 _doSend 向OC发送指定格式的数据(重点是包含responseId字段,OC端会根据此字段识别这是一个JS回调调用,同JS端)。随后,根据OC传过来的handleName获取到注册的方法,传入data及callBack函数来调用。至此,web端的方法得到调用:
|
|
OC回调
JS注册的方法被调用的时候,接收了一个回调OC的函数,业务端根据实际情况选择合适的时机调用该函数以回调数据给OC端。关于该回调函数的创建上面已经有说明,回调的方式就是向OC发送消息,属于JS调用OC的范畴,将在下面介绍。
JS调用OC
和OC调用JS一样,JS调用OC也是同样的4个过程:
- OC注册方法给JS
- JS发起调用
- OC接收调用并执行相应逻辑
- OC回调数据给JS
OC注册方法
和JS类似,OC端也是通过bridge对象注册方法。
|
|
实现也一致,绑定handleName与block,保存到bridge的messageHandlers属性中:
|
|
JS发起调用
web业务端根据实际场景调用OC,示例中是在ExampleApp.html加载时向DOM中添加了一个按钮元素,点击按钮调用OC注册的方法。
|
|
可以看到,JS端通过bridge对象的 callHandler 执行调用。和OC调用JS很类似,也是传了3个参数:handleName、data、回调函数。相关方法实现如下:
|
|
可以看到最终执行的是 _doSend 函数,这个函数之前在讲解JS回调OC的时候也遇到过,其作用就是向OC发送消息。_doSend 执行时,若JS需要回调,即responseCallback参数不为空,则会创建唯一callbackId,并保存回调函数。随后,将数据封装成指定格式的对象(格式同OC调用JS时一致),发送给OC。此处,我们又见到了眼熟的src,之前在web端初始化JS环境的时候遇到过。没错,JS无法直接调用OC,此处依旧是通过URL拦截的方式实现的。首先将要发送给OC的数据保存在全局变量中,然后修改iframe的src来加载特定URL:https://__wvjb_queue_message__。
OC接收调用
因为webView要加载URL,同样先回调了webView的 shouldStartLoadWithRequest 方法。方法中识别了这是一个由WebViewJavascriptBridge发起的web端交互请求,进行拦截处理:
|
|
其中 webViewJavascriptFetchQueyCommand 的作用的获取之前保存在JS端的需要传递给OC的数据:
|
|
通过JS端bridge对象的 _fetchQueue 获取:
|
|
可以看到传过来的数据是一个被序列化的数组字符串。
OC端拿到数据后,调用 flushMessageQueue 执行相应的逻辑。
|
|
处理逻辑和JS接收数据后的处理逻辑几乎一致,同样是根据传过来的数据中是否包含responseId字段来判断这是一个JS回调调用还是由JS发起的调用。若是JS回调调用,则从responseCallbacks集合中根据responseId取出回调block并调用;若是JS发起的调用,则最终从messageHandlers中根据handlerName取出对应的block调用。
JS回调
OC端在处理JS的调用时,若识别出这是一个JS发起的调用,而非回调调用,则根据传过来的数据中是否包含callbackId字段来决定是否需要回调JS。若需要,则创建回调block,并将该回调block传入OC方法,由OC业务层决定何时进行回调,以及回调什么样的数据。而该回调block的内容,无可厚非就是向JS发送特定格式的数据:
|
|
_queueMessage 方法正是之前介绍的OC调用JS的方法。另外,JS在接收到回调数据后的处理也在之前介绍过了,不再赘述。
总结
- 交互前需要先对OC环境和JS环境进行初始化,JS环境的初始化通过Web页面加载时发送特定的URL来完成。
- WebViewJavascriptBridge在OC端和JS端各自维护一个bridge对象来保存开放给另一端的方法,以及自身调用另一端后的回调方法。前者通过handlerName来映射,后者通过callBackId标识唯一性。方法调用时必定携带handlerName,若需要回调,还需携带callBackId。
- WebViewJavascriptBridge中OC调用JS采用的是WebView提供的JS执行方法;而JS调用OC采用的是URL拦截的方式,OC端通过识别特定的URL来区分是否需要拦截,并做相应的逻辑处理。