前文我们介绍了 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://toutiao.io/posts/26yhry 欢迎点赞支持!
欢迎订阅《不错的好文》https://toutiao.io/subjects/5551