iOS平台Webview和原生代码交互

iOS平台Webview和原生代码交互

最近遇到很多原生和前端进行交互的业务逻辑,前前后后做了一些调研。现在把常用的方法总结一下。
前端和原生交互大致分为这几种方式:拦截URL、WKWebView、JavaScriptCore,下面来分别说一下这几种方式的实现以及优缺点。

1.拦截URL

1.1 通过Webview的委托方法拦截

UIWebview

1
2
3
4
5
6
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSString *url = request.URL.absoluteString;
//判断url特征,进行对应的逻辑处理
return YES;
}

WKWebview
1
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。相关的使用和特性可以细读WKWebViewA Look at the WebKitWKWebView那些坑(林泽水原创发表在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委托三个步骤

    1. 注册handler

      1
      2
      3
      4
      5
      config = WKWebViewConfiguration()
      //注册js方法
      config.userContentController.addScriptMessageHandler(self, name: "WebApp")
      // 初始化
      webView = WKWebView(frame: self.webWrap.frame, configuration: config)
    2. js调用
      通过window.webkit.messageHandlers.WebApp找到之前注册的handler对象,然后调用postMessage方法把数据传到WebApp通过上一步的方法解析方法名和参数。WebApp是之前注册的name。

      1
      2
      3
      4
      5
      var message = {
      'method' : 'hello',
      'param1' : 'liuyanwei',
      };
      window.webkit.messageHandlers.WebApp.postMessage(message);
    3. 处理handler委托
      之前初始化Webview时指定的MessageHandler要实现WKScriptMessageHandler的协议方法userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage)然后在这里面处理事件。

      1
      2
      3
      4
      func 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: 28
  • js调用OC
    使用JavaScriptCore在JS端调用原生代码的时候主要有两种方式:block和方法注入。

    1. Block方式

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
          JSContext *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 });"];
    2. 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
<!DOCTYPE html>
<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调用本地方法是在子线程中执行的,这里要根据实际情况考虑线程之间的切换。

相关参考: