前端 WebView 指南之 iOS 交互篇

前文我们介绍了 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 了。

- (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 |

不过在 WKWebView 中没办法获取到 `JSContext`,不过也还是提供了 `evaluateScript` 方法,调用方式比起 JavascriptCore 更加简单。同时将错误捕获放置到了执行的异步回调中,对个性化错误处理比较方便。
[self.webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable title, NSError * _Nullable error) {
        NSLog(@"Hello, %@", title);
}];

stringByEvaluatingJavaScriptFromString

除了 evaluateScript,两个 WebView 还提供了另外一种调用方式,那就是 stringByEvaluatingJavaScriptFromString。同样是执行一段 JS 字符串,它的优势是两者都兼容,缺点是返回值类型无法转换,只能是字符串,而且无法捕获错误。

self.navigationItem.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];

JS 调用客户端

JS 调用 iOS 客户端的方法其实和 Android 的非常的类似,JavascriptCore 对应的是 addJavascriptInterface(),而劫持执行的方法都是通用的。

JavascriptCore

不得不说 JavascriptCore 十分强大,获取到 JSContext 上下文之后既可以读取 JS 方法,同时也可以对其写入方法以供 JS 调用。

- (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 有一些区别。

//定义 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 对象,我们可以如下直接调用客户端方法了。

window.webkit.messageHandlers.hello.postMessage();

URL劫持

同 Android 一样,我们也可以使用客户端劫持 URL 跳转的方式来进行 JS 与客户端的通信。URL劫持主要是使用 shouldStartLoadWithRequest() 进行 WebView URL 劫持。在该回调中我们能够获取到前端提供的 URL 地址。我们通过构造约定协议的 URL 地址提供给客户端识别,识别成功后执行对应的方法即可。

- (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 |

将这三个方法列在一块是因为这几个方法的本质上都是差不多,定义好对应的回调方法即可。客户端具体的配置如下:

- (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 调用的时候需要传入一个回调方法名,然后客户端直接执行回调方法。这样就完成了一个完成的信息交流的过程。

window.hello = function(text) {
  console.log(text);
};
location.href = '$hello:{"callback": "hello"}';
//以 stringByEvaluatingJavaScriptFromString 为例
[webView stringByEvaluatingJavaScriptFromString:@"hello('hello world')"];

有人将通信机制进行了封装,形成一套完善的 WebviewJSBridge 方案,提供了客户端调前端,前端调用客户端的系统解决方案。例如 marcuswestin/WebViewJavascriptBridge 项目,其实它在底层是使用了 URL 劫持的方法与 JS 进行交互。使用 URL 劫持的方式主要是适用范围广,同时还能兼容 Android 端。

参考资料:

前端 WebView 指南之 iOS 交互篇》上有一条评论

  1. 感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/26yhry 欢迎点赞支持!
    欢迎订阅《不错的好文》https://toutiao.io/subjects/5551

发表评论

电子邮件地址不会被公开。 必填项已用 * 标注