iOS平台Webview和原生代码交互
最近遇到很多原生和前端进行交互的业务逻辑,前前后后做了一些调研。现在把常用的方法总结一下。
前端和原生交互大致分为这几种方式:拦截URL、WKWebView、JavaScriptCore,下面来分别说一下这几种方式的实现以及优缺点。
1.拦截URL
1.1 通过Webview的委托方法拦截
UIWebview1
2
3
4
5
6-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSString *url = request.URL.absoluteString;
//判断url特征,进行对应的逻辑处理
return YES;
}
WKWebview1
2
3
4
5
6-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
NSString *url = navigationAction.request.URL.absoluteString;
//判断url特征,进行对应的逻辑处理
decisionHandler(WKNavigationActionPolicyAllow);
}
- 优势:实现简单方便
- 缺点:如果是用GET请求的话在原生和webview之间传参会有长度限制。而POST请求的话需要只能在UIWebview上才能获取到请求体,在WKWebview里面是无法获取到POST请求体的。
1.2 使用WebViewJavascriptBridge
这个是iOS上使用的比较多的js交互框架,也是基于拦截URL的方式来实现的。不过在自己公司项目中用的不多,所以没有做深入的了解。
1.3 通过NSURLProtocol拦截
这个是利用了NSURLProtocol会拦截webview的网络请求而实现的,本质上和通过Webview的委托方法拦截差不多。不过据说WKWebView貌似不支持NSURLProtocol,没有做实验,具体是否可行还需要测试。
2.WKWebView
iOS8,苹果新推出了WebKit,用WKWebView代替UIWebView和WebView。相关的使用和特性可以细读WKWebView和A Look at the WebKit、WKWebView那些坑(林泽水原创发表在bugly公众号上的一篇文章,基本上把它的缺陷都点出来了,入坑前必读)。
优点:功能比UIWebview强大很多,调用js的时候可以返回对象以及错误状态;而且js也可以直接反向回调
缺点:WKWebview的坑很多,参见上一段落提到的“WKWebview那些坑”
App调用js
WKWebView调用js方法和UIWebView类似,一个是evaluateJavaScript
,一个是stringByEvaluatingJavaScriptFromString
。不过获取返回值的方式不同,WKWebView用的是闭包回调;UIWebview返回的是一个string。js调用App
UIWebView中js是没办法直接回调App的(只能用url的方式间接回调),而在WKWebView中有了改进。具体步骤分为App注册handler,js调用,App处理handler委托三个步骤注册handler
1
2
3
4
5config = WKWebViewConfiguration()
//注册js方法
config.userContentController.addScriptMessageHandler(self, name: "WebApp")
// 初始化
webView = WKWebView(frame: self.webWrap.frame, configuration: config)js调用
通过window.webkit.messageHandlers.WebApp找到之前注册的handler对象,然后调用postMessage方法把数据传到WebApp通过上一步的方法解析方法名和参数。WebApp是之前注册的name。1
2
3
4
5var message = {
'method' : 'hello',
'param1' : 'liuyanwei',
};
window.webkit.messageHandlers.WebApp.postMessage(message);处理handler委托
之前初始化Webview时指定的MessageHandler要实现WKScriptMessageHandler的协议方法userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage)
然后在这里面处理事件。1
2
3
4func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
//根据传过来数据从而决定app调用的方法
let dict = message.body as! Dictionary<String,Any>
}
3.JavaScriptCore
JavaScriptCore中类及协议:
JSContext:给JavaScript提供运行的上下文环境,通过-evaluateScript:方法就可以执行js代码
JSValue:封装了js与OC中的对应的类型,以及调用js的API等
JSManagedValue:管理数据和方法的类
JSVirtualMachine:处理线程相关,使用较少
JSExport:这是一个协议,如果采用协议的方法交互,自己定义的协议必须遵守此协议,在协议中声明的API都会在js中暴露出来,才能调用
OC调用js
在JavaScriptCore中提供的调用js的方法-(JSValue *)evaluateScript:(NSString *)script
方法就可以执行一段JavaScript脚本,并且如果其中有方法、变量等信息都会被存储在其中以便在需要的时候使用。
JSValue提供了-(JSValue *)callWithArguments:(NSArray *)arguments
方法来将参数传进去调用方法1
2
3
4
5
6
7
8
9
10
11
12
13
14// 一个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: 28js调用OC
使用JavaScriptCore在JS端调用原生代码的时候主要有两种方式:block和方法注入。Block方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14JSContext *context = [[JSContext alloc] init];
// 定义一个block
context[@"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 });"];OC方法注入
这里需要原生自定义一个协议,并且它还需要遵守JSExport协议。协议里的方法,就是暴露给js端的方法。1
2
3@protocol JavaScriptObjectiveCDelegate <JSExport>
-(void)callWithDict:(NSDictionary *)params;
@end
然后自定义一个对象,让这个对象来实现上面所说的协议1
2
3
4
5
6
7
8
9
10
11// 此模型用于注入JS的模型,这样就可以通过模型来调用方法。
@interface HYBJsObjCModel : NSObject <JavaScriptObjectiveCDelegate>
@property (nonatomic, weak) JSContext *jsContext;
@property (nonatomic, weak) UIWebView *webView;
@end
@implementation HYBJsObjCModel
- (void)callWithDict:(NSDictionary *)params {
NSLog(@"Js调用了OC的方法,参数为:%@", params);
}
@end
对象实现完了,在哪里注入呢。在controller的webView加载完成后我们是通过webView的valueForKeyPath获取的,其路径为documentView.webView.mainFrame.javaScriptContext。这样就可以获取到js的context,然后为这个context注入我们的模型对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16- (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);
};
}
对应的在js端调用的代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head lang="en">
<meta charset="UTF-8">
</head>
<body>
<div style="margin-top: 100px">
<h1>OC方法注入</h1>
<input type="button" value="Call OC method" onclick="OCModel.callWithDict()">
</div>
</body>
</html>
JavaScript调用本地方法是在子线程中执行的,这里要根据实际情况考虑线程之间的切换。