05月14, 2019

iOS 中与 WebView 交互

在 iOS 中 webView 分为 UIWebView 和 WKWebView 两种。其中 WKWebView 是在 iOS8.0 版本新增,而 UIWebView 是 iOS2.0 开始就存在了。 UIWebView 存在占用过多内存且不容易控制释放、加载速度等问题。WKWebView 相较于 UIWebView 优势在于能够直接使用系统 Safari 渲染引擎去渲染页面,支持更多 HTML5 特性,渲染性能也会更好点,内存占用变少等。接下来分别介绍一下WKWebView 和 UIWebView 和原生交互逻辑。

客户端调用 JS

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:

WKWebView 中是获取不到 JSContext。提供了 evaluateScript 方法,调用方式比起 JavaScriptCore 更加简单。同时将错误捕获放置到了执行的异步回调中,对个性化错误处理比较方便。

[self.webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable title, NSError * _Nullable error) {
        NSLog(@"Hello, %@", title);
}];

通用的方式:

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

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

JS 调用客户端:

JS 调用 iOS 客户端,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 hasPrefix:@"sdk:hello"]) {
        NSLog(@"hello world");
        return NO;
    }
    return YES;

/**
 约定协议:
 例如上边`[requestString hasPrefix:@"sdk:hello"]`通过判断前缀来实现,客户端本地实现相应的功能。
 通常判断出是约定协议,从 url 中拆解出“具体功能”、“回调方法”、“参数”等信息。
 接着处理本地逻辑,调用回调方法,将处理结果回传给H5.
*/

方法劫持

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 调用客户端的方法,以及客户端调用前端的方法。JavaScriptCoreMessage Handler 方法都提供了回去执行结果的方法,而 URL 劫持则需要在 JS 调用的时候需要传入一个回调方法名,然后客户端直接执行回调方法。这样就完成了一个完成的信息交流的过程。

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

参考链接:

参考链接

本文链接:http://www.iuutech.com/post/ios_1557796671.html

-- EOF --

Comments