最近整理了一下原生与H5之间的交互方式,简单的做个总结。
OC端与JS的交互,大致有这几种:拦截协议、JavaScriptCore库、WKWebView、自定义NSURLProtocol拦截、WebViewJavascriptBridge。
- JavaScriptCore一个iOS7引进的标准库,iOS7以前也有开发者自行导入使用。Web端也比较容易统一。
- WebViewJavascriptBridge是一个第三方库,其原理还是使用了对web view的请求拦截,支持WKWebview。封装了完整的OC与JS相互调用的方法,不过需要Web端配合编写相应的方法。安卓方面听说有一个同名的库,如果不是统一使用的话,需要Web端写2套JS的话,那就有点蛋疼了。
- WKWebView,iOS8加入的WebKit。相对于UIWebView,具有更强大的功能。提供一个
WKScriptMessageHandler
,可以实现JS对WebView的调用。 - 协议的拦截,比较常用的一种方式。
- 在自定义NSURLProtocol中,拦截请求,也可以实现相应的方法调用。
1. JavaScriptCore
JavaScriptCore中类及协议:
- JSContext:给JavaScript提供运行的上下文环境,通过-evaluateScript:方法就可以执行一JS代码
- JSValue:封装了JS与ObjC中的对应的类型,以及调用JS的API等
- JSManagedValue:管理数据和方法的类
- JSVirtualMachine:处理线程相关,使用较少
- JSExport:这是一个协议,如果采用协议的方法交互,自己定义的协议必须遵守此协议,在协议中声明的API都会在JS中暴露出来,才能调用
对于JSContext和JSValue的更多使用方式可以看下这篇,介绍的比较完整ios7 JavaScriptCore.framework 本文主要简单总结下交互相关内容。
ObjC调用JS
在JavaScriptCore中提供的调用JS的方法
- (JSValue *)evaluateScript:(NSString *)script;
方法就可以执行一段JavaScript脚本,并且如果其中有方法、变量等信息都会被存储在其中以便在需要的时候使用。
JSValue提供了- (JSValue *)callWithArguments:(NSArray *)arguments;
方法可以反过来将参数传进去来调用方法
// 一个JSContext对象,就类似于Js中的window,
// 只需要创建一次即可。
JSContext *context = [[JSContext alloc] init];
// 执行一段js
[context evaluateScript:@"function add(a, b) { return a + b; }"];
// 根据下标取出方法
JSValue *add = context[@"add"];
NSLog(@"Func: %@", add);
// 传入参数 调用取到的方法
JSValue *sum = [add callWithArguments:@[@(7), @(21)]];
NSLog(@"Sum: %d",[sum toInt32]);
//OutPut:
// Func: function add(a, b) { return a + b; }
// Sum: 28
再来个栗子
self.jsContext = [[JSContext alloc] init];
[self.jsContext evaluateScript:@"var num = 10"];
[self.jsContext evaluateScript:@"var squareFunc = function(value) { return value * 2 }"];
// 调用 计算面积
JSValue *square = [self.jsContext evaluateScript:@"squareFunc(num)"];
// 可以通过下标的方式获取到方法
JSValue *squareFunc = self.jsContext[@"squareFunc"];
// 传入参数 调用取到的方法
JSValue *value = [squareFunc callWithArguments:@[@"20"]];
NSLog(@"%@", square.toNumber);
NSLog(@"%@", value.toNumber);
JS调用OC
使用JavaScriptCore进行原生与js的交互主要是2种方式,block和注入模型使用协议代理。
Block方式
JSContext *context = [[JSContext alloc] init];
// 定义一个block
context[@"log"] = ^() {
NSLog(@"+++++++Begin Log+++++++");
NSArray *args = [JSContext currentArguments];
for (JSValue *jsVal in args) {
NSLog(@"%@", jsVal);
}
JSValue *this = [JSContext currentThis];
NSLog(@"this: %@",this);
NSLog(@"-------End Log-------");
};
// 调用js执行log方法
[context evaluateScript:@"log('ider', [7, 21],
{ hello:'world', js:100 });"];
当web端调用log方法,传入相关参数,就能调用OC端的block。实现交互。
通过注入模型的方式交互
-
OC端要做的事情
首先,我们自定义一个协议,而且这个协议必须要遵守JSExport协议
协议暴露的方法,是供JS调用的方法。还可以实现回调。
@protocol JavaScriptObjectiveCDelegate <JSExport>
// JS调用此方法来调用OC的share
- (void)share:(NSDictionary *)params ;
// JS调用此方法来调用OC的相机
- (void)callCamera ;
// 在JS中调用时,多个参数需要使用驼峰方式
// 这里是多个个参数的。
- (void)showAlert:(NSString *)title msg:(NSString *)msg;
// 通过JSON传过来
- (void)callWithDict:(NSDictionary *)params;
// JS调用Oc,然后在OC中通过调用JS方法来传值给JS。
- (void)jsCallObjcAndObjcCallJsWithDict:(NSDictionary *)params;
@end
接下来,我们还需要定义一个模型:
// 此模型用于注入JS的模型,这样就可以通过模型来调用方法。
@interface HYBJsObjCModel : NSObject <JavaScriptObjectiveCDelegate>
@property (nonatomic, weak) JSContext *jsContext;
@property (nonatomic, weak) UIWebView *webView;
@end
模型的实现:
@implementation HYBJsObjCModel
- (void)share:(NSDictionary *)params {
NSLog(@"Js调用了OC的share方法,参数为:%@", params);
}
- (void)callWithDict:(NSDictionary *)params {
NSLog(@"Js调用了OC的方法,参数为:%@", params);
}
// JS调用了callCamera
- (void)callCamera {
NSLog(@"JS调用了OC的方法,调起系统相册");
// JS调用后OC后,可以传一个回调方法的参数,进行回调JS
JSValue *jsFunc = self.jsContext[@"jsFunc"];
[jsFunc callWithArguments:nil];
}
- (void)jsCallObjcAndObjcCallJsWithDict:(NSDictionary *)params {
NSLog(@"jsCallObjcAndObjcCallJsWithDict was called, params is %@", params);
// 调用JS的方法
JSValue *jsParamFunc = self.jsContext[@"jsParamFunc"];
[jsParamFunc callWithArguments:@[@{@"age": @10, @"name": @"lili", @"height": @158}]];
}
// 指定参数的用法
// 在JS中调用时,函数名应该为showAlertMsg(arg1, arg2)
- (void)showAlert:(NSString *)title msg:(NSString *)msg {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertView *a = [[UIAlertView alloc] initWithTitle:title message:msg delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil, nil];
[a show];
});
}
@end
JavaScriptCore使用注意
JavaScript调用本地方法是在子线程中执行的,这里要根据实际情况考虑线程之间的切换。
模型实现完了,在哪里注入呢。在controller的webView加载完成后
我们是通过webView的valueForKeyPath获取的,其路径为documentView.webView.mainFrame.javaScriptContext
。
这样就可以获取到JS的context,然后为这个context注入我们的模型对象。
#pragma mark - UIWebViewDelegate
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// 通过模型调用方法,这种方式更好些。
HYBJsObjCModel *model = [[HYBJsObjCModel alloc] init];
// 模型
self.jsContext[@"OCModel"] = model;
model.jsContext = self.jsContext;
model.webView = self.webView;
// 增加异常的处理
self.jsContext.exceptionHandler = ^(JSContext *context,
JSValue *exceptionValue) {
context.exception = exceptionValue;
NSLog(@"异常信息:%@", exceptionValue);
};
}
另外关于模型,也可根据需求直接将模型作为controller,去实现相关的方法实现,省去模型
这一层。 如下:
#pragma mark - UIWebViewDelegate
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
self.jsContext[@"OCModel"] = self;
self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
context.exception = exceptionValue;
NSLog(@"异常信息:%@", exceptionValue);
};
}
-
WEB端
代码内容
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
</head>
<body>
<div style="margin-top: 100px">
<h1>Objective-C和JavaScript交互的那些事</h1>
<input type="button" value="CallCamera" onclick="OCModel.callCamera()">
</div>
<div>
<input type="button" value="Share" onclick="callShare()">
</div>
<script>
var callShare = function() {
OCModel.share({'title': '标题', 'desc': '内容', 'shareUrl': 'http://www.jianshu.com/p/f896d73c670a');
}
</script>
</body>
</html>
注意下 如果调用的方法是多个参数的,必须使用驼峰写法并去掉冒号
- (void)showAlert:(NSString *)title msg:(NSString *)msg;
需要这样调用
OCModel. showAlertMsg('title','msg');
以上就是简单的利用JavaScriptCore framework进行JS交互的用法,
感谢
iOS与JS交互实战篇(ObjC)
Objective-C与JavaScript交互的那些事
提供的资料参考,仅仅是做个总结
2. WebViewJavascriptBridge
这个第三方库起先是在UIWebView与JS的深度交互大神文中知悉。其还是使用拦截WebView请求方法,但是做了完整的封装后,使用起来还是很简单的。
1) 导入
#import "WKWebViewJavascriptBridge.h"
2) 初始化
self.bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
// 开启日志,方便调试
[WebViewJavascriptBridge enableLogging];
3) Web端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 = 'wvjbscheme://__BRIDGE_LOADED__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
4)call setupWebViewJavascriptBridge
setupWebViewJavascriptBridge(function(bridge) {
/* Initialize your app here */
bridge.registerHandler('JS Echo', function(data, responseCallback) {
console.log("JS Echo called with:", data)
responseCallback(data)
})
bridge.callHandler('ObjC Echo', function responseCallback(responseData) {
console.log("JS received response:", responseData)
})
})
-
ObjC API
OC端初始化时 默认消息处理器
实例化WebViewJavascriptBridge并定义native端的默认消息处理器。
JS调用bridge.send()即可触发默认处理。
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView handler:^(id data, WVJBResponse *response) {
NSLog(@"ObjC received message from JS: %@", data);
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"ObjC got message from Javascript:" message:data delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
}];
OC端调用[self.bridge send];
即可触发JS端的默认处理。
[self.bridge send:@"Give me a response, will you?" responseCallback:^(id responseData) {
NSLog(@"ObjC got its response! %@", responseData);
}];
OC端registerHandler接收JS调用
在JS中调用了bridge.callHandler('getScreenHeight')
就会触发OC注册的对应的handler,responseCallback中回调JS传递参数
[self.bridge registerHandler:@"getScreenHeight" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"ObjC Echo called with: %@", data);
responseCallback([NSNumber numberWithInt:[UIScreen
mainScreen].bounds.size.height]);
}];
或者 JS传递data给OC,OC打印
[self.bridge registerHandler:@"log" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"Log: %@", data);
}];
OC端callHandler调用JS
调用JS showAlert
,传递data
[self.bridge callHandler:@"showAlert" data:@"Hi from ObjC to JS!"];
调用JS getCurrentPageUrl
,在block中获取参数
[self.bridge callHandler:@"getCurrentPageUrl" data:nil responseCallback:^(id responseData) {
NSLog(@"Current UIWebView page URL is: %@", responseData);
}];
还可设置代理监听
[bridge setWebViewDelegate:(UIWebViewDelegate*)webViewDelegate];
-
Javascript API
JS registerHandler接收OC调用
注册handle,OC可以通过[bridge callHandler:"handlerName" data:@"Foo"]
和 [bridge callHandler:"handlerName" data:@"Foo" responseCallback:^(id responseData) { ... }]
进行调用JS
OC传递data进行调用
bridge.registerHandler("showAlert", function(data) { alert(data) })
参数结果回传给OC
bridge.registerHandler("getCurrentPageUrl", function(data, responseCallback) {
responseCallback(document.location.toString())
})
bridge.callHandler("handlerName", data)
JS 调用OC
JS调用bridge.callHandler("handlerName", data)
和bridge.callHandler("handlerName", data, function responseCallback(responseData) { ... })
调用OC端打印
bridge.callHandler("Log", "Foo")
调用OC端获取高度,在block中使用
bridge.callHandler("getScreenHeight", null, function(response) {
alert('Screen height:' + response)
})
3.WKWebView - iOS8 or Later
iOS8,苹果新推出了WebKit,用WKWebView代替UIWebView和WebView。相关的使用和特性可以细读。
WKWebView
iOS 8 WebKit框架概览(下)
WKWebView特性及使用
-
WKWebView新特性
性能、稳定性、功能大幅度提升
允许JavaScript的Nitro库加载并使用(UIWebView中限制)
支持了更多的HTML5特性
高达60fps的滚动刷新率以及内置手势
GPU硬件加速
KVO
重构UIWebView成14类与3个协议,查看官方文档
需要注意的是WKWebView貌似不支持NSURLProtocol和NSURLCache。不能做缓存的话,就蛋疼了。
关于WKWebView的代理方法 这篇有比较完整的介绍
http://www.jianshu.com/p/1d7a8525ad16
下面是相关的交互方法
-
app调js方法
WKWebView调用js方法和UIWebView类似,一个是evaluateJavaScript
,一个是stringByEvaluatingJavaScriptFromString
。获取返回值的方式不同,WKWebView用的是回叫函数获取返回值
//直接调用js
webView.evaluateJavaScript("hi()", completionHandler: nil)
//调用js带参数
webView.evaluateJavaScript("hello('liuyanwei')", completionHandler: nil)
// 调用js获取返回值
webView.evaluateJavaScript("getName()") { (any,error) -> Void in
NSLog("%@", any as! String)
}
-
js调app方法
UIwebView没有js调app的方法,而在WKWebView中有了改进。具体步骤分为app注册handler,app处理handler委托,js调用三个步骤
- 注册handler需要在webView初始化之前,如示例,注册了一个webViewApp的handler
config = WKWebViewConfiguration()
//注册js方法
config.userContentController.addScriptMessageHandler(self, name: "webViewApp")
// 初始化
webView = WKWebView(frame: self.webWrap.frame, configuration: config)
- 处理handler委托。ViewController实现WKScriptMessageHandler委托的
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage)
代理方法。在里面处理事件。
//实现WKScriptMessageHandler委托
class ViewController:WKScriptMessageHandler
//实现js调用ios的handle委托
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
//接受传过来的消息从而决定app调用的方法
let dict = message.body as! Dictionary<String,String>
let method:String = dict["method"]!
let param1:String = dict["param1"]!
if method=="hello"{
hello(param1)
}
}
-
js调用 。通过
window.webkit.messageHandlers.webViewApp
找到之前注册的handler对象,然后调用postMessage方法把数据传到app,app通过上一步的方法解析方法名和参数。webViewApp
是之前注册的namevar message = { 'method' : 'hello', 'param1' : 'liuyanwei', }; window.webkit.messageHandlers.webViewApp.postMessage(message);
如果需要app对js的调用有所响应,可以通过回叫函数的方式回应js。可以在调用app的时候增加一个js回叫函数名
,app在处理完之后可以呼叫回叫函数并把需要的参数通过回叫函数的方式进行传递
-
使用用户脚本来注入 JavaScript
WKUserScript 允许在正文加载之前或之后注入到页面中。这个强大的功能允许在页面中以安全且唯一的方式操作网页内容。
一个简单的例子如下,用户改变背景的用户脚本被插入到网页中:
let source = "document.body.style.background = \"#777\";"
let userScript = WKUserScript(source: source, injectionTime: .AtDocumentEnd, forMainFrameOnly: true)
let userContentController = WKUserContentController()
userContentController.addUserScript(userScript)
let configuration = WKWebViewConfiguration()
configuration.userContentController = userContentController
self.webView = WKWebView(frame: self.view.bounds, configuration: configuration)
WKUserScript 对象可以以 JavaScript 源码形式初始化,初始化时还可以传入是在加载之前还是结束时注入,以及脚本影响的是这个布局还是仅主要布局。于是用户脚本被加入到 WKUserContentController 中,并且以 WKWebViewConfiguration 属性传入到 WKWebView 的初始化过程中。
4. 拦截协议
最简单也是最容易想到的一种
UIWebView的代理方法,web view发出请求后拦截,查看是否为约定的协议,采取处理。
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSString *url = request.URL.absoluteString;
if ([url rangeOfString:@"camera://"].location != NSNotFound) {
// url的协议头是camera
NSLog(@"callCamera");
return NO;
}
return YES;
}
WKWebView
-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
NSString *url = navigationAction.request.URL.absoluteString;
NSLog(@"%@",url);
if (navigationAction.navigationType == WKNavigationTypeLinkActivated && [url rangeOfString:@"camera://"].location != NSNotFound)
{
// url的协议头是camera
NSLog(@"callCamera");
decisionHandler(WKNavigationActionPolicyCancel);
// dosomthing。。。
}
else
{
decisionHandler(WKNavigationActionPolicyAllow);
}
}
5. NSURLProtocol拦截
这种方式也是最近才看到,原本利用自定义NSURLProtocol来做缓存处理。相关的文章可以看:
NSURLProtocol和NSRunLoop的那些坑
iOS中的 NSURLProtocol
在自定义的Protocol的- (void)startLoading
方法中,可以拦截到请求。一般会在这里做缓存的判断与读取处理。在此处,也可以判断约定的协议,然后发送通知,客户端就可以接收到通知,执行相应的方法。
- (void)startLoading
{
NSString * url = [[[self request] URL] absoluteString];
if([url hasPrefix:@"LocalActions/"])
{
NSString * actname = [url stringByReplacingOccurrencesOfString:@"LocalActions/" withString:@"LocalAction_"];
// 发送通知 客户端就可执行方法
[[NSNotificationCenter defaultCenter] postNotificationName:actname object:nil];
}
}
需要注意的是WKWebView貌似不支持NSURLProtocol和NSURLCache。不能做缓存的话,就蛋疼了。
网友评论
一般请求是放在oc端还是js端啊?
我现在只能把onload方法里面的内容重新封装一个方法onReady,然后,在webview webViewDidFinishLoad的时候通过stringByEvaluatingJavaScriptFromString 去调用onReady方法。
我还遇到一个问题,就是在方法里面,如果js里面不调用oc,然后向后台请求数据,请求的数据返回有问题,但是如果我在js里面调用oc方法(我就加了一个NSLog方法)。然后我在JS调用Ajax方法之前,调用一下log方法,请求数据就正确了。
我只是加了个log而已,其他什么都没改,感觉好奇怪!
<div style="margin-top: 100px">
<h1>Objective-C和JavaScript交互的那些事</h1>
<input type="button" value="CallCamera" onclick="OCModel.callCamera()">
</div>
<div>
<input type="button" value="Share" onclick="callShare()">
</div>
<script>
var callShare = function() {
var shareInfo = JSON.stringify({"title": "标题", "desc": "内容", "shareUrl": "http://www.jianshu.com/p/f896d73c670a",
"shareIco":"http://upload-images.jianshu.io/upload_images/1192353-fd26211d54aea8a9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"});
OCModel.share(shareInfo);
}
var picCallback = function(photos) {
showAlertMsg('haha','haha');
}
var shareCallback = function(){
alert('成功');
}
</script>
@property (strong, nonatomic) JSContext *jsContext;
-(void)callWithDict:(NSDictionary *)params{
NSLog(@"JS调用了OC的方法,参数是:%@",params);
}
-(void)callSystemCamera{
NSLog(@"JS调用了OC的方法,调起了系统相册");
//JS调用后OC,可以传一个回调方法的参数,进行回调JS
JSValue *jsFunc = self.jsContext[@"jsFunc"];
[jsFunc callWithArguments:nil];
}
-(void)jsCallObjectAndObjcCallJsWithDict:(NSDictionary *)params{
NSLog(@"jsCallObjectAndObjcCallJsWithDict");
//调用JS的方法
JSValue *jsParamFunc = self.jsContext[@"jsParamFunC"];
[jsParamFunc callWithArguments:@[@{@"age":@10,@"name":@"lili",@"height":@170}]];
}
//指定参数的用法,在JS中调用时,函数名应该为showAlert
// 指定参数的用法
// 在JS中调用时,函数名应该为showAlertMsg(arg1, arg2)
- (void)showAlert:(NSString *)title msg:(NSString *)msg {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertView *a = [[UIAlertView alloc] initWithTitle:title message:msg delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
[a show];
});
}
-(void)callCamera{
NSLog(@"callCamera");
}
-(void)callShare{
NSLog(@"callShare");
}
/*
JavaScriptCore使用注意
JavaScript调用本地方法是在子线程中执行的,这里要根据实际情况考虑线程之间的切换。
模型实现完了,在哪里注入呢。在controller的webView加载完成后
我们是通过webView的valueForKeyPath获取的,其路径为documentView.webView.mainFrame.javaScriptContext。
这样就可以获取到JS的context,然后为这个context注入我们的模型对象。
*/
#pragma mark - UIWebViewDelegate
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
NSLog(@"self.jsContext:%@",self.jsContext);
self.jsContext[@"OCModel"] = self;
self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
context.exception = exceptionValue;
NSLog(@"异常信息:%@", exceptionValue);
};
}
这一句可以实现 把app端的数据传递给js端吗?有没有具体的代码