iOS原生代码和HTML中的JS进行交互有以下几种方法

  1. 拦截URL
  2. 使用JavaScriptCore
  3. 使用WKScriptMessageBridge
  4. 使用开源框架WebViewJavascriptBridge

这几种方法各有特点,其中 方法1、2、4可以进行iOS 、Android跨平台操作 方法3只支持iOS平台,如果想要使用1套js支持iOS Android两端,那么使用使用WKScriptMessageBridge可以排除在外

拦截URL

js调用原生代码

拦截url使用比较简单,和后端同事协商好URL协议,比如jsaction://openAlbum表示打开相册
实现UIWebView的代理方法:shouldStartLoadWithRequest:navigationType,在方法中对url进行拦截判断

1
2
3
4
5
6
7
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
if ([request.URL.absoluteString hasPrefix:@"jsaction://openAlbum
//调用原生扫描二维码
return NO;
}
return YES;
}

HTML代码:

1
<a href="jsaction://openAlbum">扫一扫(拦截url)</a>

原生调用js代码:

在打开相册后如果想反馈给HTML页面,可以执行UIWebView的stringByEvaluatingJavaScriptFromString:方法,或者WKWebView的 evaluateJavaScript:completionHandler:方法。

1
[self.webView stringByEvaluatingJavaScriptFromString:@"openResult(‘成功打开相册')”];

拦截URL使用简单,js端只要约定好url协议就可以,但是缺点同样明显,代理方法中讲充斥大量判断URL的硬编码,约定好的连接都是写死的,而且通信是单向的,不支持return和callBack,只能做send操作,不能做get操作,所以拦截URL适用于业务相对简单的需求。

使用JavaScriptCore

JavaScriptCore只支持UIWebView,支持系统版本为iOS7+
通过 JSContext 获取 UIWebView 的 JS 执行上下文。
然后通过这个上下文,进行 OC & JS 的双端交互。

js调用原生

你需要创建一个对象继承自NSObject,并遵循JSExport协议,代理的方法和js的方法保持一致,.m中实现代理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//JSObject.h
#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>
@protocol JSObjectDelegate <JSExport>
-(void)openAlbum:(NSString *)message;
@end
@interface JSObject : NSObject<JSObjectDelegate>
@property(nonatomic,weak) id<AppJSObjectDelegate> delegate;
@end
//JSObject.m
#import "AppJSObject.h"
@implementation JSObject
-(void)openAlbum:(NSString *)message{
[self.delegate openAlbum:message];
}
@end

1
2
3
4
5
6
7
8
9
JS代码:
<script type="text/javascript">
function openAlbum(){
app.scan(‘openResult');
}
function openResult(result){
document.getElementById("result").innerHTML = ‘打开结果:' + result;
}
</script>

在UIWebView加载完成的代理中把SObject实例对象类注入到JS中,那么在js中调用方法就会调用到原生JSObject实例对象中对应的方法了。

1
2
3
4
5
6
7
-(void)webViewDidFinishLoad:(UIWebView *)webView
{
JSContext *context=[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
JSObject *jsObject = [JSObject new];
jsObject.delegate = self;
context[@"app"] = jsObject;
}

原生调用js

1
2
3
JSContext *context=[_mainWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
NSString *alertJS= [NSString stringWithFormat:@"%@('%@')",_photoMethod,fileUrl];
[context evaluateScript:alertJS];

JavaScriptCore 是跨平台的,你不需要在代理方法中添加大量的硬编码,支持双向的return,js调用原生方法可以拿到return,native调用js也可以拿到return,缺点就是JavaScriptCore只支持UIWebView不支持WKWebView,而UIWebView在内存优化,加载速度上都存在一些问题,所以使用JavaScriptCore的话只能舍弃WKWebView,而且只支持return,不支持callback,所以在异步调用的时候拿不动返回值。

WKScriptMessageBridge

我们之前说过,WKScriptMessageBridge无法做到跨平台,所以这里做一个简单介绍:
通过 userContentController 把需要观察的 JS 执行函数注册起来。
然后通过一个协议方法,将所有注册过的 JS 函数执行的参数传递到此协议方法中。
初始化WKWebView时,调用addScriptMessageHandler:name:方法,name为js中的方法名,如openAlbum:

1
2
3
4
5
6
7
- (void)setupWKWebView{
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
configuration.userContentController = [[WKUserContentController alloc] init];
[configuration.userContentController addScriptMessageHandler:self name:@"openAlbum"];
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];
webView.UIDelegate = self;
}

js代码

1
window.webkit.messageHandlers.openAlbum.postMessage()

实现WKScriptMessageHandler代理方法,当js调用openAlbum方法时,会回调此代理方法:

1
2
3
4
5
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
if ([message.name isEqualToString:@"openAlbum"]) {
//调用原生扫码
}
}

WebViewJavascriptBridge

WebViewJavascriptBridge是github上的开源库,用于UIWebView和WKWebView中原生代码和JS的交互,另外iOS和Android的WebViewJavascriptBridge在github上不是一个地址,但是实现思路和原理都是一样的,所以可以做到跨平台使用

实现原理

把 原生 的方法注册到桥梁中,让 JS 去调用。
把 JS 的方法注册在桥梁中,让 原生代码 去调用。

1.install JSBridge

pod ‘WebViewJavascriptBridge’, ‘~> 6.0’

2.导出头文件

1
#import <WKWebViewJavascriptBridge.h>

3.初始化WebView和JSBridge

1
2
3
4
5
6
7
8
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
WKUserContentController *userController = [[WKUserContentController alloc] init];
configuration.userContentController = userController;
_mainWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, KMainWidth, KMainHeight) configuration:configuration];
NSString *path = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"index.html"];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]];
[_mainWebView loadRequest: request];
self.bridge = [WKWebViewJavascriptBridge bridgeForWebView:self.mainWebView];

4.在js文件中加入以下两个初始化函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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(function(bridge){

});

接下来我们描述一个场景

1.HTML中加一个按钮,点击调用原生代码 ,随机修改一个colorView的背景颜色
2.原生页面添加一个按钮,点击调用js代码,用来修改HTML页面颜色

在原生代码中注册一个修改colorView背景颜色的block

1
2
3
4
[self.bridge registerHandler:@"colorClick" handler:^(id data, WVJBResponseCallback responseCallback) {
self.colorView.backgroundColor = [UIColor colorWithRed:arc4random_uniform(256) / 255.0 green:arc4random_uniform(256) / 255.0 blue:arc4random_uniform(256) / 255.0 alpha:1.0];
responseCallback(@"颜色修改完毕!");
}];

在js文件里找到button的点击事件,添加以下代码

1
2
3
4
WebViewJavascriptBridge.callHandler('colorClick',function(dataFromOC) {
alert("JS 调用了 OC 注册的 colorClick 方法");
document.getElementById("returnValue").value = dataFromOC;
})

此时我们就完成了HTML中点击按钮,修改原生代码中colorView的背景颜色,接下来继续
在JS中添加一个function 用来修改body的背景颜色

1
2
3
4
5
6
setupWebViewJavascriptBridge(function(bridge){
bridge.registerHandler('changeBGColor',function(data,responseCallback){
document.body.style.backgroundColor = "orange";
document.getElementById("returnValue").value = data;
});
});

然后在原生页面的button点击事件中添加以下代码

1
[self.bridge callHandler:@"changeBGColor" data:@“修改body背景颜色为橙色”];

现在我们就完成从js调用原生代码修改colorview的背景颜色,原生页面的button点击时修改了HTML的背景颜色。
关于内存泄漏
在页面退出时需要删除在JSBridge中注入的block,否则可能会导致页面无法释放,造成内存泄漏的问题

1
2
3
4
- (void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
[self.bridge removeHandler:@"colorClick"];
}

关于传参

在OC和JS相互调用时候支持传递参数和添加回调

OC调用JS

1
2
3
4
5
[self.bridg callHandler:@"changeBGColor"];
[self.bridg callHandler:@"changeBGColor" data:@"修改body背景颜色为橙色"];
[self.bridg callHandler:@"changeBGColor" data:@"传递给 JS 的参数" responseCallback:^(id responseData) {
NSLog(@"JS 的返回值: %@",responseData);
}];

#####JS调用OC

1
2
3
4
5
6
WebViewJavascriptBridge.callHandler('scanClick');
WebViewJavascriptBridge.callHandler('scanClick',"JS 参数");
WebViewJavascriptBridge.callHandler('scanClick',{data : "这是 JS 传递到 OC 的扫描数据"},function(dataFromOC){
alert("JS 调用了 OC 的扫描方法!");
document.getElementById("returnValue").value = dataFromOC;
});

到这里WebViewJavascriptBridge就介绍完了,可以看到使用起来也很简单,你可以到WebViewJavascriptBridge的github主页上查看更多的信息。

WebViewJavascriptBridge虽然是一个三方库,但是已经是一个很成熟原生与JS交互的解决方案,在他的主页上可以看到Facbook也在使用它,WebViewJavascriptBridge特点是跨品台的,支持双向callback,可以异步回调,这也是我们项目组在用的方案