前言
在 iOS 开发中经常要用到 UIWebView ( iOS8 中可以用 WKWebView,本文章以 UIWebView 为例 ) 来展示一些东西,其中就难免要和网页进行交互。服务端提供 H5 供多个平台使用,我们就不用在 native 中开发了,是不是很棒。Hybrid App 的优势很明显,这就要求我们必须具备 native 和网页交互的技能。学点儿 JavaScript 的知识能帮我们更好的理解交互的原理。本文主要来介绍 UIWebView 和 JavaScript 的交互原理以及 WebViewJavascriptBridge (github 地址) 源码分析。
UIWebView 和 JavaScript 的交互原理
理解这个原理之前必须先明确以下两点:
1.JavaScript 能直接调用 native 方法吗?不可以。
2.native 能直接调用 JavaScript 代码吗?可以,可以通过以下方式来调用:[webView stringByEvaluatingJavaScriptFromString:javascriptCommand];
JavaScript 不能直接调用 native 的方法,但是可以间接的通过一些方法来实现。可以利用 UIWebView 的 webView: shouldStartLoadWithRequest: navigationType:
代理方法间接的来做。 WKWebView 中可以通过 webView: decidePolicyForNavigationAction: decisionHandler:
代理方法来做(本文以 UIWebView 为例,WKWebView 与 JavaScript 交互的原理同 UIWebView 一样,后面不会赘述。以下用 webView 来代指 UIWebView 和 WKWebView )。webView 发起的网络请求都会走上面的代理方法,那么就可以在代理里拦截,如果返回的是我们自己定义的 URL ,就不在加载网页,而是来处理一些我们想让它做的事情,从而实现 native 和 JavaScript 的交互。
WebViewJavascriptBridge 源码分析
WebViewJavascriptBridge 是封装好的 native 和 JavaScript 交互的组件。下面主要是对它源码的一些分析,以及一些简单的 JavaScript 知识(对只会 Objective-C 程序猿理解 WebViewJavascriptBridge 很有帮助哦)。上面分析的原理是利用 webView 的代理来拦截 URL 从而实现交互。说到这里可能很多做前端的小伙伴不理解 webview 的这个代理是什么东东,这里简单说明一下。在用 webview 来加载网页的时候会触发 webview 的代理,来询问是否要加载网页,这个由 native 来决定,发现是我们希望加载的网页可以进行直接加载,但发现不是自己希望加载的网页可以不加载,可以来处理我们想让它的干的事,简单的理解就是 webview 在加载网页的时候会征求我们的意见。那么 webView 的代理方法 webView: shouldStartLoadWithRequest: navigationType
什么时候会被调用呢?给出的回答是这样的: Sent before a web view begins loading a frame
。
native 和 JavaScript 交互的核心思想如下:当 webview 加载网页的时候通过 webView: shouldStartLoadWithRequest: navigationType
代理方法来拦截实现。比如 webview 加载 ExampleApp.html ,在加载前触发 webview 的代理方法,发现是 ExampleApp.html 的 url 后能让网页顺利加载。如果代理方法只调用一次的话,没办法对其中的 URL 拦截判断(这里指的是我们自定义的 URL),所以就必须想办法在 H5 里做处理来触发 webView 的代理事件。有很多办法能解决这个问题,比如以下两种:
- 创建 iframe 标签,WebViewJavascriptBridge 中就是用的这种方法。
- 设置 window 的 location,例如 window.location = “/www/phpStudy/JS/helloJS.html”;
通过在 ExampleApp.html 的 js 方法中创建一个隐藏的 iframe 标签,设置其 src 为一个自定义的 URL,触发 webview 的代理方法,发现是自定义的 URL,就可以让它做我们想让它做的事情了。
总体流程

本文以尽可能按照代码的执行顺序来分析 WebViewJavascriptBridge 源代码。那现在正式开始,native 创建好 webView 后,来 load ExampleApp.html。先来看看 ExampleApp.html,其中主要的是 script 标签里面的代码,代码如下:
<script> |
对 Objective-C 程序猿来说猛的一眼看不懂这是什么,其实就是一个简单的 JavaScript 函数调用,只不过是把另一个函数当做参数传给了 setupWebViewJavascriptBridge 函数。简化后如下:
function setupWebViewJavascriptBridge(callback) {}
setupWebViewJavascriptBridge(function(bridge) {})
上面的 setupWebViewJavascriptBridge
函数中对 window.WebViewJavascriptBridge
和 window.WVJBCallbacks 做判断,第一次请求 H5 这两个属性都为空,根本就不回执行 if 里面的语句,可以像上面代码中注释的那样,用 alert 来证实。callback 被加到了 WVJBCallbacks 数组里,这里先记住,后面会用,这里提个醒留个印象。接着函数中还创建了一个隐藏的 iframe 标签,并设置它的 src 属性为 wvjbscheme://__BRIDGE_LOADED__
。这样我们才能在 webView 的代理方法中对 URL 做判断,并做进一步的处理。 来看一下 wevView 的代理方法:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { |
在这里面拦截到是我们定义的 wvjbscheme
后就调用相应的逻辑处理,否则的话就不处理。在这里拦截到 wvjbscheme://__BRIDGE_LOADED__
,执行了 WebViewJavascriptBridge_js 里的 JavaScript 代码。来看看 WebViewJavascriptBridge_js 里的代码都干了点儿什么,先挑出现在能用上的,具体的用的时候再说。看看下面的代码片段:
messagingIframe = document.createElement('iframe'); |
一眼就能看到其中又创建了个 iframe 标签,把 src 设置成了 wvjbscheme://__WVJB_QUEUE_MESSAGE__
,这样的话就能在 webView 的代理方法中拦截了。来说一下 setTimeout 的作用:setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式(setTimeout 函数介绍)。接着调用了 _callWVJBCallbacks 函数,在这个函数里面取出了上面让暂时记住的 window.WVJBCallbacks (在 ExampleApp.html 里把 callback 放入进 WVJBCallbacks 里),取出里面所有的方法(其实现在就一个 callback 方法)并且执行。让我们来跳到 ExampleApp.html 里面,看看 callback (就是setupWebViewJavascriptBridge 调用处传进来的匿名函数)里都做了什么。log 方法用来打印信息。接着调用了 bridge 的 registerHandler 方法(这个主要是供 native 端来调用的)。创建了一个 callbackButton 并绑定它的 onclick 事件。先来看看 registerHandler 方法都干了什么,跳到 WebViewJavascriptBridge_JS.m 里找到的 registerHandler 如下:
function registerHandler(handlerName, handler) { |
这里是把 registerHandler 里传过来的参数放到了 messageHandlers 数组里,以后 native 调用 JavaScript 方法的时候就会来这里取。
Objective-C 程序猿的加油站:JavaScript 拥有动态类型
我们申明 messageHandlers 时是这样申明的 var messageHandlers = {}; 可是在用的时候就把它当数组用了,对 Objective-C 程序猿来说是很奇怪的。其实JavaScript 拥有动态类型。这意味着相同的变量可用作不同的类型。
native 调用 JavaScript 方法
native 调用 JavaScript 方法是这样的:
|
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback { |
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName { |
sendData: responseCallback: handlerName:
方法需要简单说一下,这里会把传过来的要调用 js 的函数名,参数和回掉id放到一个 message 的字典中,其中的回掉的 id 和 回掉的方法存到了 responseCallbacks 字典中以供调用 js 成功后,js 能回掉到 native 的方法。
- (void)_queueMessage:(WVJBMessage*)message { |
- (void)_dispatchMessage:(WVJBMessage*)message { |
通过上面的函数,一路跟下去,发现是把调用的 JavaScript 的方法名,参数和回调 id 拼装好后在本地执行了 _handleMessageFromObjC
方法,其中 _handleMessageFromObjC
的参数是上面拼装好的字典。来看看 _handleMessageFromObjC
里做了什么。
function _handleMessageFromObjC(messageJSON) { |
在 _handleMessageFromObjC
里面又调用了 _dispatchMessageFromObjC
。该函数里首先对 message.responseId 做了判断,我们知道它肯定是空的,因为传过来的参数只有 data ,callbackId 和 handlerName。在 else 里面取出来 callbackId 赋值给了 callbackResponseId。从 messageHandlers 中取出 handlerName 给了 handler,这里的 handler 就是 JavaScript 注册到这里的方法。执行 handler 并把回调给 responseCallback。执行回调的过程中使用了 _doSend 函数,我们来看一下:
function _doSend(message, responseCallback) { |
此时的 responseCallback 为空,因为在调用它的时候只传了 { handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData }
一个参数,不进 if 里面。然后把 message 放到了 sendMessageQueue 数组里,并把 messagingIframe.src 的设置成了 wvjbscheme://__WVJB_QUEUE_MESSAGE__
,就又会执行 webView 的代理方法,在代理方法里判断 URL 为 wvjbscheme://__WVJB_QUEUE_MESSAGE__
的话就会去 通过 _fetchQueue 来取 sendMessageQueue 的数据,并且回调 native 方法。至此,native 调用 JavaScript 注册的方法结束。
Objective-C 程序猿的加油站:stringify()
在上面的 _fetchQueue 中用到了 stringify(),下面说说 stringify() 是干什么用的
stringify()用于从一个对象解析出字符串,例如:
var a = {a:1,b:2};
执行JSON.stringify(a)后的结果为:
“{“a”:1,”b”:2}”
JavaScript 调用 native 方法
native 调用的 JavaScript 的方法是在 JavaScript 里提前注册好的。同理,JavaScript 想调用 native 的方法,native 必须也要先注册。
在 ExampleUIWebViewController.m 中[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@{ @"name":@"OC回调给js的参数" });
}];
//在 WebViewJavascriptBridge.m 中
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
在代码层面来看,native 的注册其实就是把 native 中需要被调用的名字和回调放到了 messageHandlers 的字典中。那我们来看看调用 native 方法的具体流程。
在点击 webView 上的按钮(在 ExampleApp.html 里面创建的 callbackButton)后,调用了 callHandler 方法,传递的参数为要调用的 native 的方法名,参数和回调。来看看 callHandler 里有什么。
function callHandler(handlerName, data, responseCallback) { |
这里做了容错判断(在下面的 Objective-C 程序猿加油站中有说明)后,和 native 调用 JavaScript 一样,都是调用了 _doSend 方法。在里面为 responseCallback 生成了 唯一的 callbackId 并放到 message 里,接下来的流程和 native 调用 JavaScript 大同小异,这里不再赘述。
Objective-C 程序猿的 js 加油站:arguments
在 JavaScript 中 arguments 对象是比较特别的一个对象,实际上是当前函数的一个内置属性,它的长度是由实参个数而不是形参个数决定的。那么就很容易理解了,上面的是一个冗错处理,也就是说在调用 callHandler 的时候可以不传调用 native 方法中的参数,只传递调用 nativie 方法中的方法名字和回调方法即可。