iOS之OC与JS交互(WKWebView)

前言

最近项目开发中用到了OC与JS交互方面的知识,以前也用过UIWebView JS与OC交互方面的,使用的苹果在iOS7开放的javascriptCore框架,使用起来挺方便快捷,javascriptCore源码是开放的,有兴趣的可以去了解一下。

自从iOS8,苹果就UIWebView性能不好,推出了WKWebView,以及github上评分很高的WebViewJavascriptBridge里面最新版本也最WKWebView做了兼容。

实现的方式

所以我总结的方式,分UIWebview、WKWebView、以及通用版本的第三方WebViewJavascriptBridge,进行实现。

  • UIWebView中JS与OC交互
  • WKWebView中JS与OC交互(只能iOS8及之后的版本)
  • WebViewJavascriptBridge对UIWebView与WKWebView做了统一处理。

WKWebView出现背景和优点

UIWebView自iOS2就有,WKWebView从iOS8才有,毫无疑问WKWebView将逐步取代笨重的UIWebView。

通过简单的测试即可发现UIWebView占用过多内存,且内存峰值更是夸张。WKWebView网页加载速度也有提升,但是并不像内存那样提升那么多

下面列举一些其它的优势:

  • 更多的支持HTML5的特性

  • 官方宣称的高达60fps的滚动刷新率以及内置手势

  • Safari相同的JavaScript引擎

  • 将UIWebViewDelegate与UIWebView拆分成了14类与3个协议(官方文档说明)

  • 另外用的比较多的,增加加载进度属性:estimatedProgress

了解WKWebView要涉及一些类

首页使用WKWebView要引进:

#import 
  • WKWebView使用
//初始化
self.webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) configuration:config];

// UI代理
self.webView.UIDelegate = self;

// 导航代理        self.webView.navigationDelegate = self;

// 是否允许手势左滑返回上一级, 类似导航控制的左滑返回   self.webView.allowsBackForwardNavigationGestures = YES;

//可返回的页面列表, 存储已打开过的网页 
WKBackForwardList * backForwardList = [self.webView backForwardList];

 //页面后退
 [self.webView goBack];
 
 //页面前进
 [self.webView goForward];
 
 //刷新当前页面
 [self.webView reload];
 
 //加载本地的html
 NSString *path = [[[NSBundle mainBundle] bundlePath]  stringByAppendingPathComponent:@"test.html"];
 
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]];
[self.webView loadRequest:request];
    
  • WKWebViewConfiguration:为添加WKWebView配置信息
//创建网页配置对象
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
        
// 创建设置对象
WKPreferences *preference = [[WKPreferences alloc]init];

//最小字体大小 当将javaScriptEnabled属性设置为NO时,可以看到明显的效果
preference.minimumFontSize = 0;

//设置是否支持javaScript 默认是支持的
preference.javaScriptEnabled = YES;

// 在iOS上默认为NO,表示是否允许不经过用户交互由javaScript自动打开窗口        
preference.javaScriptCanOpenWindowsAutomatically = YES;
config.preferences = preference;
        
// 是使用h5的视频播放器在线播放, 还是使用原生播放器全屏播放
config.allowsInlineMediaPlayback = YES;
        
//设置视频是否需要用户手动播放  设置为NO则会允许自动播放
config.requiresUserActionForMediaPlayback = YES;
       
//设置是否允许画中画技术 在特定设备上有效
config.allowsPictureInPictureMediaPlayback = YES;

//设置请求的User-Agent信息中应用程序名称 iOS9后可用
config.applicationNameForUserAgent = @"ChinaDailyForiPad";

//自定义的WKScriptMessageHandler 是为了解决内存不释放的问题
WeakWebViewScriptMessageDelegate *weakScriptMessageDelegate = [[WeakWebViewScriptMessageDelegate alloc] initWithDelegate:self];

//这个类主要用来做native与JavaScript的交互管理
WKUserContentController * wkUController = [[WKUserContentController alloc] init];

//注册一个name为jsToOcNoPrams的js方法
[wkUController addScriptMessageHandler:weakScriptMessageDelegate name:@"js调用OC的方法"];

[wkUController addScriptMessageHandler:weakScriptMessageDelegate name:@"js调用OC的方法"]; 

config.userContentController = wkUController;
       
  • WKUserScript:用于进行JavaScript注入
//以下代码适配文本大小,由UIWebView换为WKWebView后,会发现字体小了很多,这应该是WKWebView与html的兼容问题,解决办法是修改原网页,要么我们手动注入JS

NSString *jSString = @"var meta = document.createElement('meta'); 
meta.setAttribute('name', 'viewport'); 
meta.setAttribute('content', 
'width=device-width'); 
document.getElementsByTagName('head')
[0].appendChild(meta);";

//用于进行JavaScript注入
WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jSString 
injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];

[config.userContentController addUserScript:wkUScript];

  • WKUserContentController:这个类主要用来做native与JavaScript的交互管理
 //这个类主要用来做native与JavaScript的交互管理
WKUserContentController * wkUController = [[WKUserContentController alloc] init];

//注册一个name为jsToOcNoPrams的js方法,设置处理接收JS方法的代理
[wkUController addScriptMessageHandler:self  name:@"jsCallOCNoPrams"];
[wkUController addScriptMessageHandler:self  name:@"jsCallOCNoPrams"];
config.userContentController = wkUController;

//用完记得移除
//移除注册的js方法,避免内存泄露
[[self.webView configuration].userContentController 
removeScriptMessageHandlerForName:@"jsToOcNoPrams"];

[[self.webView configuration].userContentController 
removeScriptMessageHandlerForName:@"jsToOcWithPrams"];

  • WKScriptMessageHandler:这个协议类专门用来处理监听JavaScript方法从而调用原生OC方法,和WKUserContentController搭配使用。
注意:遵守WKScriptMessageHandler协议,代理是由WKUserContentControl设置

//通过接收JS传出消息的name进行捕捉的回调方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {

    /*
     message.body: 触发js方法传的值
     message.name: 触发js的方法名
    */
    NSLog(@"传的值L: --%@ \n 方法名: --- %@",message.body, message.name);
}

  • WKNavigationDelegate :主要处理一些跳转、加载处理操作
// 页面开始加载时调用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
}

// 页面加载失败时调用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error {
   
} 

// 当内容开始返回时调用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
}

// 页面加载完成之后调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
   
}

//提交发生错误时调用
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
    
}  

// 接收到服务器跳转请求即服务重定向时之后调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {
}

// 根据WebView对于即将跳转的HTTP请求头信息和相关信息来决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    
}
    
// 根据客户端受到的服务器响应头以及response相关信息来决定是否可以跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {

    NSString * urlStr = navigationResponse.response.URL.absoluteString;
    NSLog(@"当前跳转地址:%@",urlStr);
    //允许跳转    
    decisionHandler(WKNavigationResponsePolicyAllow);
    
    //不允许跳转
    decisionHandler(WKNavigationResponsePolicyCancel);
} 

//进程被终止时调用
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {

}
  • WKUIDelegate :主要处理JS脚本,确认框,警告框等
/**
     *  web界面中有弹出警告框时调用
     *
     *  @param webView           实现该代理的webview
     *  @param message           警告框中的内容
     *  @param completionHandler 警告框消失调用
     */
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"HTML的弹出框" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:([UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
}
    // 确认框
    //JavaScript调用confirm方法后回调的方法 confirm是js中的确定框,需要在block中把用户选择的情况传递进去
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:([UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(NO);
    }])];
    [alertController addAction:([UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(YES);
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
}
    // 输入框
    //JavaScript调用prompt方法后回调的方法 prompt是js中的输入框 需要在block中把用户输入的信息传入
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler{
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        textField.text = defaultText;
    }];
    [alertController addAction:([UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(alertController.textFields[0].text?:@"");
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
}
    // 页面是弹出窗口 _blank 处理
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
    if (!navigationAction.targetFrame.isMainFrame) {
        [webView loadRequest:navigationAction.request];
    }
    return nil;
}

  • 网页内容加载进度条和title的实现,使用KVO
//添加监测网页加载进度的观察者
    [self.webView addObserver:self
                   forKeyPath:@"estimatedProgress"
                      options:0
                      context:nil];
   //添加监测网页标题title的观察者
    [self.webView addObserver:self
                   forKeyPath:@"title"
                      options:NSKeyValueObservingOptionNew
                      context:nil];

   //kvo 监听进度 必须实现此方法
-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void *)context{
    if ([keyPath isEqualToString:NSStringFromSelector(@selector(estimatedProgress))]
        && object == self.webView) {
        
       NSLog(@"网页加载进度 = %f",_webView.estimatedProgress);
       self.progressView.progress = self.webView.estimatedProgress;
       
       if (self.webView.estimatedProgress >= 1.0f) {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                self.progressView.progress = 0;
            });
       } 
    }else if([keyPath isEqualToString:@"title"]
             && object == self.webView){
        self.navigationItem.title = self.webView.title;
    }else{
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

 //移除观察者,不然会引起崩溃
- (void)dealloc {
   
[self.webView removeObserver:self forKeyPath:@"estimatedProgress"];
[self.webView removeObserver:self forKeyPath:@"title"];

}
                  

WKWebView的JS和OC的交互

首先要遵守 WKScriptMessageHandler协议, WKNavigationDelegate,WKUIDelegate 代理

  • WKScriptMessageHandler协议 专门用来处理监听JavaScript方法从而调用原生OC方法
  • WKNavigationDelegate 主要处理一些跳转、加载处理操作
  • WKUIDelegate 回拦截alert、confirm、prompt三种js弹框
JS方法 WKUIDelegate方法
alert(message) -webView: runJavaScriptAlertPanelWithMessage: initiatedByFrame:completionHandler:
confirm(message) -webView:runJavaScriptConfirmPanelWithMessage:initiatedByFrame:completionHandler:
prompt(prompt, defaultText) webView:runJavaScriptTextInputPanelWithPrompt:defaultText:initiatedByFrame:completionHandler:

注意:WKUIDelegate中的三个方法都有completionHandlerblock参数,在iOS实现对应的功能后必须调用此block完成回调,否则会崩溃

其次初始化WKWebView设置这两个WKUIDelegate、WKNavigationDelegate


//webview添加配置
[self configWKWebView];

 //加载本地的html
NSString *path = [[[NSBundle mainBundle] bundlePath]  stringByAppendingPathComponent:@"test.html"];

NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]];
[self.webView loadRequest:request];

给WKWebView添加配置

//给wkwebview添加配置
- (void)configWKWebView {
    
    WKWebViewConfiguration *config = [WKWebViewConfiguration new];
    //JS调用OC方法
    // WKScriptMessageHandler:这个协议类专门用来处理监听JavaScript方法从而调用原生OC方法
    [config.userContentController addScriptMessageHandler:self name:@"getUserIdFromOC"];
    
    
    self.webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height) 
    configuration:config];
     self.webView.UIDelegate = self;
    self.webView.navigationDelegate = self;
    [self.view addSubview:self.webView];
    
}

OC调用JS

当webview加载完成时候,再使用OC调用JS

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    
    //OC调用JS
    [self getParamsFromOC];
}

- (void)getParamsFromOC {
    
    //js的方法名和参数,把OC的userId传到js里
    NSString *js = @"getParamsFromOC('张三','18岁')";
    
    [self.webView evaluateJavaScript:js completionHandler:^(id _Nullable response, NSError * _Nullable error) {
        
        //当js里面的方法有返回值时候,response就会有值,没有为null
        NSLog(@"response: %@ error: %@", response, error);
    }];
}

我把JS里面主要的代码写出来, 在

function getParamsFromOC(responseData1,responseData2) {
                    
    alert(responseData1+responseData2);
    //有返回值
    return {'userId':'123456'};
}

getParamsFromOC这个方法名就是OC调用JS的方法名字,传了两个参数值张三、18岁
return {'userId':'123456'};有返回值的情况下,在evaluateJavaScript:js completionHandler: 打印response数据:

response: {
    userId = 123456;
}

如果没有return,默认是return null;,所以打印为null

JS调用OC

下面这个方法就是调用JS的getUserIdFromOC方法

//相当于注册js的方法
[config.userContentController 
addScriptMessageHandler:self name:@"getUserIdFromOC"];

在H5页面的body里添加一个button,点击事件



在Html中 在中添加方法


function getUserIdFromOC(responseData) {
                        
    //WKWebView调用oc方法,规定的写法
    window.webkit.messageHandlers.
    getUserIdFromOC.postMessage(responseData);
}
 

PS:window.webkit.messageHandlers.
方法名.postMessage()这个是WKWebView的统一写法。这样的话,JS就调用了OC的方法

JS调用了OC的方法,会触发下面WKScriptMessageHandler协议方法

//WKScriptMessageHandler 协议
- (void)userContentController:(nonnull WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message {
    
    /*
     message.body: 触发js方法传的值
     message.name: 触发js的方法名
     */
    NSLog(@"传的值L: --%@ \n 方法名: --- %@",message.body, message.name);
    //如果是JS的这个getUserIdFromOC方法
    if ([message.name isEqualToString:@"getUserIdFromOC"]) {
        
        NSLog(@"调用%@成功,传的值:%@",message.name, message.body);
        //对这个方法进行处理操作
    }
}

点击heml的按钮,触发OC的方法,打印结果如下:

调用getUserIdFromOC成功,传的值:{
    userId = 123456;
}

所以在WKWebView中OC与JS交互的整个流程已经完毕。

结尾

WKWebView是iOS8之后才出现的,使用了
所以要适配iOS8之前的,针对系统判断来进行用UIWebView还是WKWebView进行OC与JS交互。现在苹果已经XCode已经不针对iOS8一下的进行适配,所以大家尽量使用WKWebView来做处理。

你可能感兴趣的:(iOS之OC与JS交互(WKWebView))