

前文我们介绍了 Android 的 WebView 交互方式，iOS 从原理上来说和 Android 还是非常类似的。在 iOS 中 WebView 需要分为UIWebView 和 iOS8 中新增的 WKWebView 两种类型。其中 WKWebView 相较于 UIWebView 优势在于能够直接使用系统 Safari 渲染引擎去渲染页面，支持更多的 HTML5 特性，渲染性能也会更好点。由于对 iOS 开发了解不太多，以下的代码大多是网络整理，没有 swift 的实现，如果有任何错误还请及时联系。

## 客户端调用 JS

两个 WebView 类型提供了不同的调用方式，但是基本上可以归类成以下两种：

### evaluateScript

在 UIWebView 中，iOS7+ 提供了 JavascriptCore 让我们能够直接在 WebView 中获取到 JSContext，也就是当前执行环境的 JS 上下文。在这里我们就可以获取到对应的 JS 方法并执行，是非常高效的执行方式。同时这种方式的好处是能够拿到 JS 执行的结果，并转换成对应的 JS 类型。定义好 `jsContext` 之后就可以调用 `evaluateScript` 方法来执行 JS 了。

```oc
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    JSContext *jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    //设置JS执行报错捕获
    [self.jsContext setExceptionHandler:^(JSContext *context, JSValue *exception){
        NSLog(@"%@", exception);
    }];

    JSValue *value = [self.jsContext evaluateScript:@"document.title"];
    self.navigationItem.title = value.toString;
}
```

|Objective-C 数据类型  |   对应 JavaScript 数据类型|
|--------------------|---------------------|
|         nil         |     undefined|
|        NSNull       |        null|
|       NSString      |       string|
|       NSNumber      |   number, boolean|
|     NSDictionary    |   Object object|
|       NSArray       |    Array object|
|        NSDate       |     Date object|
|       NSBlock   |   Function object |
|          id     |   Wrapper object |
|        Class     | Constructor object |

<!-- m o r e -->
不过在 WKWebView 中没办法获取到 `JSContext`，不过也还是提供了 `evaluateScript` 方法，调用方式比起 JavascriptCore 更加简单。同时将错误捕获放置到了执行的异步回调中，对个性化错误处理比较方便。

```oc
[self.webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable title, NSError * _Nullable error) {
        NSLog(@"Hello, %@", title);
}];
```

### stringByEvaluatingJavaScriptFromString

除了 `evaluateScript`，两个 WebView 还提供了另外一种调用方式，那就是 `stringByEvaluatingJavaScriptFromString`。同样是执行一段 JS 字符串，它的优势是两者都兼容，缺点是返回值类型无法转换，只能是字符串，而且无法捕获错误。

```oc
self.navigationItem.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
```

## JS 调用客户端

JS 调用 iOS 客户端的方法其实和 Android 的非常的类似，JavascriptCore 对应的是 `addJavascriptInterface()`，而劫持执行的方法都是通用的。

### JavascriptCore

不得不说 JavascriptCore 十分强大，获取到 JSContext 上下文之后既可以读取 JS 方法，同时也可以对其写入方法以供 JS 调用。

```oc
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    self.jsContext[@"hello"] = ^() {
        NSLog(@"Hello World");
    };
}
```

这样加载的页面中就可以直接执行 `hello()` 方法来执行客户端方法了。

### WKScriptMessageHandler

虽然在 WKWebView 中不支持获取 JavascriptCore，但是其提供了一套 Message Handler 协议的方式来进行客户端与 JS 的通信，和 JavascriptCore 有一些区别。

```oc
//定义 Message Handler 处理方法
- (void)userContentController:(WKUserContentController *)userContentController
      didReceiveScriptMessage:(WKScriptMessage *)message {
       if ([message.name isEqualToString:@"hello"]) {
           NSLog(@"Hello World");
       }
}


WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
config.userContentController = [[WKUserContentController alloc] init];

//声明 hello message handler 协议
[config.userContentController addScriptMessageHandler:self name:@"hello"];
self.webview = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
self.webview.UIDelegate = self;
[self.view addSubview:self.myWebView]
```

注册完 Message Handler 之后，JS 中会存在 `window.webkit.messageHandlers` 对象，我们可以如下直接调用客户端方法了。

```js
window.webkit.messageHandlers.hello.postMessage();
```

### URL劫持

同 Android 一样，我们也可以使用客户端劫持 URL 跳转的方式来进行 JS 与客户端的通信。URL劫持主要是使用 `shouldStartLoadWithRequest()` 进行 WebView URL 劫持。在该回调中我们能够获取到前端提供的 URL 地址。我们通过构造约定协议的 URL 地址提供给客户端识别，识别成功后执行对应的方法即可。

```oc
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
 
    NSString *requestString = [[[request URL]  absoluteString] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding ];
    
    if ([requestString isEqualToString:@"sdk:hello"]) {
        NSLog(@"hello world");
        return NO;
    }
    return YES;
```

### 方法劫持

在 WKWebView 中，JS 的 `alert()` 等弹窗行为方法是无法直接触发的，它们会触发客户端的方法，客户端需要手动实现这些方法。在这些方法中客户端可以获取到 JS 传入的参数，然后做相应的处理。目前前端主要有以下三种方法会触发对应的回调方法，对应关系如下：

| JS方法  | 触发的客户端方法                      |
|---------|---------------------------------------|
| alert   | runJavaScriptAlertPanelWithMessage    |
| prompt  | runJavaScriptTextInputPanelWithPrompt |
| confirm | runJavaScriptConfirmPanelWithMessage |

将这三个方法列在一块是因为这几个方法的本质上都是差不多，定义好对应的回调方法即可。客户端具体的配置如下：

```oc
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
  
    if ([message isEqualToString:@"sdk:hello"]) {
        NSLog(@"hello world");
        return NO;
    }

    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"alert" message:@"JS调用alert" preferredStyle:UIAlertControllerStyleAlert];
    [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
    completionHandler();
  }]];
  
  [self presentViewController:alert animated:YES completion:NULL];
}
```

另外两种方法都差不多的写法，这里就不一一列举了。在实际的使用过程中我们只要约定好一种调用协议即可。

## 总结

本文讲述了 JS 调用客户端的方法，以及客户端调用前端的方法。JavascriptCore 和 Message Handler 方法都提供了回去执行结果的方法，而 URL 劫持则需要在 JS 调用的时候需要传入一个回调方法名，然后客户端直接执行回调方法。这样就完成了一个完成的信息交流的过程。

```js
window.hello = function(text) {
  console.log(text);
};
location.href = '$hello:{"callback": "hello"}';
```

```
//以 stringByEvaluatingJavaScriptFromString 为例
[webView stringByEvaluatingJavaScriptFromString:@"hello('hello world')"];
```

有人将通信机制进行了封装，形成一套完善的 WebviewJSBridge 方案，提供了客户端调前端，前端调用客户端的系统解决方案。例如 [marcuswestin/WebViewJavascriptBridge][1] 项目，其实它在底层是使用了 URL 劫持的方法与 JS 进行交互。使用 URL 劫持的方式主要是适用范围广，同时还能兼容 Android 端。

**参考资料：**

- [《js(javascript)与ios(Objective-C)相互通信交互》][2]
- [《UIWebView和WKWebView的使用及js交互》][3]
- [《iOS中UIWebView与WKWebView、JavaScript与OC交互、Cookie管理看我就够》][4]
- [《iOS中JavaScript 与OC交互》][5]
- [《iOS开发-JavaScriptCore、UIWebView及WKWebView交互的那些事》][6]
- [《WKWebView使用简介》][7]
- [《IOS WKWebview 和JS交互》][8]


  [1]: https://github.com/marcuswestin/WebViewJavascriptBridge
  [2]: http://www.skyfox.org/javascript-ios-navive-message.html
  [3]: http://liuyanwei.jumppo.com/2015/10/17/ios-webView.html
  [4]: https://www.jianshu.com/p/ac45d99cf912
  [5]: https://www.jianshu.com/p/59242a92d4f2
  [6]: https://www.jianshu.com/p/d8f7c5e237e8
  [7]: https://www.jianshu.com/p/c16153ab46e9
  [8]: https://www.jianshu.com/p/c59dd41c3631
