01月16, 2018

前端 WebView 指南之 iOS 交互篇

原文:https://imnerd.org/ios-webview-and-js.html

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

参考资料:

本文链接:https://75team.com/post/ios-webview-and-js

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。