iOS下JS与Swift互相调用(五)WebViewJavascriptBridge

WebViewJavascriptBridge 是 iOS 平台很流行的OC 与 JS 交互的第三方框架,作者目前没有支持 Swift ,而本文用到的都是 Swift,所以采取 OC 和 Swift 混编的形式,我没有找到相关的文章介绍用 Swift 使用 WebViewJavascriptBridge,但是我在本文 Demo 使用中没有发现什么问题

最新的 WebViewJavascriptBridge 支持同时支持 UIWeView 和WKWebView ,但是两者使用有一些区别,所以分成两篇文章讲,本文主要是讲 WebViewJavascriptBridge 在 UIWeView 中的基本使用

1.引入 WebViewJavascriptBridge 框架

  • 新建一个 Swift 语言的新工程,在工程里新建一个 OC 文件,Xcode 会提示创建一个 Objective-C bridging header 文件,点击 Creat Bridging Header,然后 Xcode 就为工程创建了一个 Bridging Header 文件,然后把刚才新建的 OC 文件删掉

  • 从 Github 上下载 WebViewJavascriptBridge, 解压压缩包,把压缩包里面的 WebViewJavascriptBridge 文件夹拖进工程,因为 WebViewJavascriptBridge 是 OC 框架工程,Swift 要使用这个框架需要在 Bridging Header 文件里 #import "WebViewJavascriptBridge.h"
  • 如果上面步骤之后提示错误找不到文件,可以去工程的 Build SettingsUser Header Search Paths 下面设置为 ${SRCROOT}recursive

上面几步完成之后就可以在 Swift 工程里面使用 WebViewJavascriptBridge 了

2. WebViewJavascriptBridge的使用

2.1 创建并初始化 UIWebView

创建 UIWebView 的部分代码:

1
2
3
4
5
6
7
8
9
self.webView = UIWebView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height))
// 取消不想要webView 的回弹效果
self.webView?.scrollView.bounces = false
// UIWebView 滚动的比较慢,这里设置为正常速度
self.webView?.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal
let url = Bundle.main.url(forResource: "index.html", withExtension: nil)
let request = NSURLRequest(url: url!)
self.webView?.loadRequest(request as URLRequest)
self.view.addSubview(self.webView!)

注意:这里不要为 UIWebView 设置代理,因为在下一步在创建WebViewJavascriptBridge 的时候,UIWebView 的代理需要赋值给 WebViewJavascriptBridge

2.2 创建并初始化 WebJavaScriptBridge

因为 WebViewJavascriptBridge 实例,在控制器中多个地方用到,因此定义一个全局的变量保存这个实例

1
2
3
4
5
WebViewJavascriptBridge.enableLogging()
self.webViewBridge = WebViewJavascriptBridge.init(forWebView: self.webView)
// {setWebViewDelegate}这个方法,可以将UIWebView的代理,从_webViewBridge中再传递出来。
// 所以如果你要在控制器中实现UIWebView的代理方法时,添加下面这样代码,否则可以不写。
self.webViewBridge?.setWebViewDelegate(self)

看一下 bridgeForWebView:(id)webView 的内部实现,它最终调用了下面的方法:

1
2
3
4
5
6
- (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView {
_webView = webView;
_webView.delegate = self;
_base = [[WebViewJavascriptBridgeBase alloc] init];
_base.delegate = self;
}

实际上就是内部初始化一个 WebViewJavascriptBridgeBase 对象赋值给 _base,把外部的 webView 赋值给 内部的 _webView ,并且把 webView 的代理设置为自己

3. JS 和 Swift 的相互调用

3.1 在控制器里注册 JS 要调用的 Native 的方法

先看一下部分示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func registerNativeFunctions() {
registerShareFunction()
}
func registerShareFunction()
// 所有js需要调用的原生功能都要先用registerHandler注册一下
self.webViewBridge?.registerHandler("shareClick", handler: { (data, responseCallback) in
//获取js传过来的数据
let dataDic = data as! [String: String]
//执行分享操作
let title = dataDic["title"]!
let content = dataDic["content"]!
let url = dataDic["url"]!
//分享结果返回给js
let result = "分享成功:\(title)\(content)\(url)"
responseCallback!(result)
})
}

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler,这个方法是注册一个 Native 的方法给 JS 调用,该方法有两个参数:第一个参数 handlerName,是注册的方法别名;第二个参数 handler,是个 Block,也就是 Native 实现的功能,JS 要调用的 Native 的功能实现其实就是 Block 内的代码功能,并且这个 Block 有两个参数一个参数是 JS 返回给 Native 的参数,另一个参数是一个 Block,调用这个block 可以把 Native 的结果返回给 JS ,而在 JS 代码里面使用 callHandler 调用注册的这个方法,这就实现了 JS 调用 Native 了:

1
2
3
4
5
6
7
function shareClick() {
var params = {'title':'测试分享的标题','content':'测试分享的内容','url':'http://www.baidu.com'};
WebViewJavascriptBridge.callHandler('shareClick',params,function(response) {
alert(response);
document.getElementById("returnValue").value = response;
});
}

3.2 Native 调用 JS 的实现

由于 WebViewJavascriptBridge 也是拦截URL来实现的调用原生功能,所以有一些代码跟之前 iOS下JS与Swift互相调用(一)UIWebView拦截URL 中的 HTML 的 JS 代码很相似

HTML 中有一个必须要添加的 JS 方法,然后需要自动调用一次该方法:

1
2
3
4
5
6
7
8
9
10
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 = 'wvjbscheme://__BRIDGE_LOADED__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}

添加完 setupWebViewJavascriptBridge 方法,需要在 JS 中主动调用一次该方法:

1
2
3
4
5
6
7
setupWebViewJavascriptBridge(function(bridge) {
//这里注册了一个叫 “testJSFunction” 的方法
bridge.registerHandler('testJSFunction', function(data, responseCallback) {
alert('JS方法被调用:'+data);
responseCallback('js执行过了');
})
})

这是 Native 调用 JS 的功能的代码:

1
2
3
4
5
6
7
8
9
// 如果不需要参数,不需要回调,使用这个
// self.webViewBridge?.callHandler("testJSFunction")
// 如果需要参数,不需要回调,使用这个
// self.webViewBridge?.callHandler("testJSFunction", data: "传一个字符串")
self.webViewBridge?.callHandler("testJSFunction", data: "传一个字符串", responseCallback: { (resposeData) in
print("调用完JS后的回调:\(String(describing: resposeData))")
})

Native 需要调用的 JS 功能,也是需要先注册,然后再执行的,如果Native 需要调用的 JS 功能有多个,那么这些功能都要在上面这个方法了先注册,注册之后才能够被 Native 调用

下面需要好好分析一下 JS 中 setupWebViewJavascriptBridge 这个方法的作用:

  1. 首先调用 setupWebViewJavascriptBridge,第一次执行的时候,由于 window.WebViewJavascriptBridgewindow.WVJBCallbacks 都不存在,所以会继续往下执行,将参数callback(它是一个function)装进数组赋值给window.WVJBCallbacks
    JS 支持动态添加属性并赋值,这里 window.WVJBCallbacks = [callback] 就是动态添加属性并赋值,另外 JS 中的全局变量都可以使用 window.xxxx 来调用,动态添加的属性也可以不加 window.,直接使用

  2. WebViewJavascriptBridge 帮助 JS 调 用 Native 的 url 有两种,一种是 wvjbscheme://__BRIDGE_LOADED__ 而另一种是wvjbscheme://__WVJB_QUEUE_MESSAGE__ 前者只有在调用setupWebViewJavascriptBridge 的时候执行一次,一般来说这个url 如果没有页面应该只会执行一次;第二种 url 所有 JS 调用 Native 功能时,都会使用到

  3. 在拦截到自定义的 url 时,WebViewJavascriptBridge 分了三种情况,如果是 wvjbscheme://__BRIDGE_LOADED__,就往 HMTL 中注入已经写好的 JS ,这个 JS 在 WebViewJavascriptBridge_JS 中;如果是 wvjbscheme://__WVJB_QUEUE_MESSAGE__,那就利用stringByEvaluatingJavaScriptFromString,取回调用 JS 中callHandler 传进去的参数。
    然后再从 WebViewJavascriptBridge 之前保存的 Native 方法对应的 Block,调用对应的 Block

4.WebViewJavascriptBridge 在 JS 和 Native 相互调用的实现原理

4.1 JS 调用 Native 的原理

从上面第三部分可以看到,JS 和 Native 的相互调用都是需要其中一方注册方法,另一方就可以根据注册的方法名调用到相应的方法

JS 调用 Native 的时候,在 JS 里调用 native 的代码如下:

1
2
3
4
5
6
7
function payClick() {
var params = {'order_no':'201511120981234','channel':'wx','amount':'1','subject':'粉色外套'};
WebViewJavascriptBridge.callHandler('payClick',params,function(response) {
alert(response);
document.getElementById("returnValue").value = response;
});
}

这里 callHandler 前的 WebViewJavascriptBridge,其实就是上一步注入到 JS 中的代码中,动态创建属性,动态赋值的属性,如下代码片段可以在 WebViewJavascriptBridge_JS 中找到:

1
2
3
4
5
6
7
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
}

callHandler 内部调用了另一个 JS function _doSend,而_doSend 内部其实就是把 handlerName 和参数 data,再加上callbackId 装成键值对,然后保存到数组 sendMessageQueue,同时加载一次 wvjbscheme://__WVJB_QUEUE_MESSAGE__ 到此利用 WebViewJavascriptBridge 实现 JS 调用 iOS Native 就完成了

4.2 Native 调用 JS 的原理

Native 调用 JS 的功能,也需要先在 JS 中为要调用的功能注册一个别名:

1
2
3
4
5
6
7
8
setupWebViewJavascriptBridge(function(bridge) {
bridge.registerHandler('testJSFunction', function(data, responseCallback) {
alert('JS方法被调用:'+data);
responseCallback('js执行过了');
})
// 注册其他的功能
//bridge.regsiterHandler.....
})

Native 调用功能的通过别名 handlerName 调用 JS:

1
2
3
[_webViewBridge callHandler:@"testJSFunction" data:@"一个字符串" responseCallback:^(id responseData) {
NSLog(@"调用完JS后的回调:%@",responseData);
}];

callHandler 方法又是如何实现调用 JS 方法的呢?
callHandler 内部是将传递给 JS 的参数、handlerNamecallbackId 组合成字典,然后把字典转换成字符串,将转换后的字符串以参数的形式,通过 stringByEvaluatingJavaScriptFromString 传递给 JS ,JS 中将传递过来的字符串转成 json,然后通过 handlerName 获取对应的 function执行,这是关键的几个代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (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];
});
}
}

下面这里是在 WebViewJavascriptBridge_JS 里面找到 handlerName 对应的 function,并执行function

1
2
3
4
5
6
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}

至此利用 WebViewJavascriptBridge 实现 Native 和 JS 相互调用的功能就完成了

5 总结

利用 WebViewJavascriptBridge 来实现 JS 与 Native 的交互的优点:

  • 获取参数时,更方便一些,如果参数中有一些特殊符号或者url带参数,能够很好的解析
    也有一些缺点:
  • 做一次交互,需要执行的 JS 与原生的交互步骤较多,至少有两次。
  • 需要花较多的时间,理解WebViewJavascriptBridge的原理和使用步骤

原文地址: iOS下JS与OC互相调用(五)–UIWebView + WebViewJavascriptBridge

原文是讨论 OC 与 JS 的交互,我按照作者的思路,用 Swift 实现 Native 与 JS 的交互,这是 代码地址

本人刚开始写博客,主要是为了给自己的知识点做一个笔记,方便自己以后查阅,如果能让别人有所启发也是荣幸之至!如有错误,欢迎指正!