webview的混合开发

前言

Web 页面中的 JS 与 iOS Native 如何交互是每个 iOS 猿必须掌握的技能。而说到 Native 与 JS 交互,就不得不提一嘴 Hybrid。

JavaScriptCore 这个库是 Apple 在 iOS 7 之后加入到标准库的,它对 iOS Native 与 JS 做交互调用产生了划时代的影响。

JavaScriptCore 大体是由 4 个类以及 1 个协议组成的:


webview的混合开发_第1张图片
javaSacriptCore.jpg
  • JSContext 是 JS 执行上下文,你可以把它理解为 JS 运行的环境。
  • JSValue 是对 JavaScript 值的引用,任何 JS 中的值都可以被包装为一个 JSValue。
  • JSManagedValue 是对 JSValue 的包装,加入了“conditional retain”。
  • JSVirtualMachine 表示 JavaScript 执行的独立环境。
  • JSExport 协议:实现将 Objective-C 类及其实例方法,类方法和属性导出为 JavaScript 代码的协议。

iOS Native 与 JS 交互

JS 调用 Native

  1. 假 Request 方法

原理:其实这种方式就是利用了 webview 的代理方法,在 webview 开始请求的时候截获请求,判断请求是否为约定好的假请求。如果是假请求则表示是 JS 想要按照约定调用我们的 Native 方法,按照约定去执行我们的 Native 代码就好。

HTML代码如下:

    
       

这里是第一种方式



然后在项目的控制器中实现UIWebView的代理方法:

#pragma mark - UIWebViewDelegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
 NSURL * url = [request URL];
if ([[url scheme] isEqualToString:@"firstclick"]) {
     NSArray *params =[url.query componentsSeparatedByString:@"&"];
     
     NSMutableDictionary *tempDic = [NSMutableDictionary dictionary];
     for (NSString *paramStr in params) {
         NSArray *dicArray = [paramStr componentsSeparatedByString:@"="];
         if (dicArray.count > 1) {
             NSString *decodeValue = [dicArray[1] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
             [tempDic setObject:decodeValue forKey:dicArray[0]];
         }
     }
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"方式一" message:@"这是OC原生的弹出窗" delegate:self cancelButtonTitle:@"收到" otherButtonTitles:nil];
    [alertView show];
    NSLog(@"tempDic:%@",tempDic);
     return NO;
 }
 
 return YES;
}

然后在项目的控制器中实现WKWebView的代理方法:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
   NSURL *url = navigationAction.request.URL;
   // 与约定好的函数名作比较
   if ([[url scheme] isEqualToString:@"your_func_name"]) {
       // just do it
       decisionHandler(WKNavigationActionPolicyCancel);
       return;
   }
   
   decisionHandler(WKNavigationActionPolicyAllow);
}

注意:1. JS中的firstClick,在拦截到的url scheme全都被转化为小写

2.html中需要设置编码,否则中文参数可能会出现编码问题。

3.JS用打开一个iFrame的方式替代直接用document.location的方式,以避免多次请求,被替换覆盖的问题。

关于这种方式调用OC方法唐巧大神文章:关于UIWebView的总结

  1. JavaScriptCore 方法

在iOS 7之后,apple添加了一个新的库JavaScriptCore,用来做JS交互,因此JS与原生OC交互也变得简单了许多。 首先导入JavaScriptCore库, 然后在OC中获取JS的上下文

可以在UIWebView加载url完成后,在其代理方法中添加要调用的share方法:

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
 JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
 //定义好JS要调用的方法, share就是调用的share方法名
 context[@"share"] = ^() {
     NSLog(@"+++++++Begin Log+++++++");
     NSArray *args = [JSContext currentArguments];
   
     dispatch_async(dispatch_get_main_queue(), ^{
         UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"方式二" message:@"这是OC原生的弹出窗" delegate:self cancelButtonTitle:@"收到" otherButtonTitles:nil];
         [alertView show];
     });
     
     for (JSValue *jsVal in args) {
         NSLog(@"%@", jsVal.toString);
     }
     
     NSLog(@"-------End Log-------");
 };
}

注意: 可能最新版本的iOS系统做了改动,现在(iOS9,Xcode 7.3,去年使用Xcode 6 和iOS 8没有线程问题)中测试,block中是在子线程,因此执行UI操作,控制台有警告,需要回到主线程再操作UI。

对应的html部分代码如下:

  
 

这里是第二种方式



iOS Native 调用 JS

  1. webview 直接注入 JS 并执行
NSString *jsStr = [NSString stringWithFormat:@"showAlert('%@')", @"alert msg"];
[_webView stringByEvaluatingJavaScriptFromString:jsStr];

Note: 这个方法会返回运行 JS 的结果(nullable NSString *),它是一个同步方法,会阻塞当前线程!尽管此方法不被弃用,但最佳做法是使用 WKWebView类的 evaluateJavaScript:completionHandler:method。

WKWebView注入并执行 JS 的方法不会阻塞当前线程

NSString *jsStr = [NSString stringWithFormat:@"setLocation('%@')", @"北京市东城区南锣鼓巷纳福胡同xx号"];
[_webview evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
   NSLog(@"%@----%@", result, error);
}];

Note: 方法不会阻塞线程,而且它的回调代码块总是在主线程中运行。

  1. JavaScriptCore 方法

在native端代码

  // 首先引入 JavaScriptCore 库
  #import 
  
  // 先获取 JS 上下文
  self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
  // 如果涉及 UI 操作,切回主线程调用 JS 代码中的 YourFuncName,通过数组@[parameter] 入参
  dispatch_async(dispatch_get_main_queue(), ^{
      JSValue *jsValue =  self.jsContext[@"YourFuncName"];
      [jsValue callWithArguments:@[parameter]];
  });

JS 代码:

 function YourFuncName(arguments){
   var result = arguments;
   // do what u want to do
} 

WKWebView 与 JS 交互的特有方法

WKUIDelegate 方法

对于 WKWebView 上文提到过,除了 WKNavigationDelegate,它还有一个 WKUIDelegate,这个 WKUIDelegate 是做什么用的呢?

WKUIDelegate 协议包含一些函数用来监听 web JS 想要显示 alert 或 confirm 时触发。我们如果在 WKWebView 中加载一个 web 并且想要 web JS 的 alert 或 confirm 正常弹出,就需要实现对应的代理方法。

我们这里就拿 alert 举例,相信各位读者可以自己举一反三。下面是在 WKUIDelegate 监听 web 要显示 alert 的代理方法中用 Native UIAlertController 替代 JS 中的 alert 显示的栗子 :

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
   // 用 Native 的 UIAlertController 弹窗显示 JS 将要提示的信息
   UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提醒" message:message preferredStyle:UIAlertControllerStyleAlert];
   [alert addAction:[UIAlertAction actionWithTitle:@"知道了" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
       // 函数内必须调用 completionHandler
       completionHandler();
   }]];
   
   [self presentViewController:alert animated:YES completion:nil];
}

MessageHandler 方法

MessageHandler 是继 Native 截获 JS 假请求后另一种 JS 调用 Native 的方法,该方法利用了 WKWebView 的新特性实现。对比截获假 Request 的方法来说,MessageHandler 传参数更加简单方便。

该方法用来添加一个脚本处理器,可以在处理器内对 JS 脚本调用的方法做出处理,从而达到 JS 调用 Native 的目的。

在 WKWebView 的初始化函数中有一个入参 configuration,它的类型是 WKWebViewConfiguration。WKWebViewConfiguration 中包含一个属性 userContentController,这个 userContentController 就是 WKUserContentController 类型的实例,我们可以用这个 userContentController 来添加不同名称的脚本处理器。

MessageHandler 的坑

  • 我们的代码会在 viewWillAppear 和 viewWillDisappear 成对儿的添加和删除 MessageHandler:

       - (void)viewWillAppear:(BOOL)animated {
          [super viewWillAppear:animated];
          [self.webview.configuration.userContentController addScriptMessageHandler:self name:@"YourFuncName"];
      }
      
      - (void)viewWillDisappear:(BOOL)animated {
          [super viewWillDisappear:animated];
          [self.webview.configuration.userContentController removeScriptMessageHandlerForName:@"YourFuncName"];
      }
      // WKScriptMessageHandler 协议方法,在接收到脚本信息时触发
      - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
          // message 有两个属性:name 和 body
          // message.name 可以用于区别要做的处理
          if ([message.name isEqualToString:@"YourFuncName"]) {
              // message.body 相当于 JS 传递过来的参数
              NSLog(@"JS call native success %@", message.body);
          }
      }
    

    JS 的代码:

     //  换 YourFuncName, 换你要的入参即可
    window.webkit.messageHandlers..postMessage()
    

UIWebView

UIWebView使用非常简单,可以分为三步,也是最简单的用法,显示网页占用过多内存,且内存峰值更是夸张。

  1. 举例:简单的使用
- (void)simpleExampleTest {
  // 1.创建webview,并设置大小,"20"为状态栏高度
  UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 20, self.view.frame.size.width, self.view.frame.size.height - 20)];
  // 2.创建请求
  NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.cnblogs.com/mddblog/"]];
  // 3.加载网页
  [webView loadRequest:request];
  
  // 最后将webView添加到界面
  [self.view addSubview:webView];
}
  1. 一些实用函数
- (void)loadRequest:(NSURLRequest *)request;
- (void)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL;

UIWebView不仅可以加载HTML页面,还支持pdf、word、txt、各种图片等等的显示。下面以加载mac桌面上的png图片:/Users/coohua/Desktop/bigIcon.png为例

// 1.获取url
NSURL *url = [NSURL fileURLWithPath:@"/Users/coohua/Desktop/bigIcon.png"];
// 2.创建请求
NSURLRequest *request=[NSURLRequest requestWithURL:url];
// 3.加载请求
[self.webView loadRequest:request];
  1. 网页导航刷新有关函数
// 刷新
- (void)reload;
// 停止加载
- (void)stopLoading;
// 后退函数
- (void)goBack;
// 前进函数
- (void)goForward;
// 是否可以后退
@property (nonatomic, readonly, getter=canGoBack) BOOL canGoBack;
// 是否可以向前
@property (nonatomic, readonly, getter=canGoForward) BOOL canGoForward;
// 是否正在加载
@property (nonatomic, readonly, getter=isLoading) BOOL loading;
  1. 代理协议使用:UIWebViewDelegate
/// 是否允许加载网页,也可获取js要打开的url,通过截取此url可与js交互
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
  
  NSString *urlString = [[request URL] absoluteString];
  urlString = [urlString stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
  
  NSArray *urlComps = [urlString componentsSeparatedByString:@"://"];
  NSLog(@"urlString=%@---urlComps=%@",urlString,urlComps);
  return YES;
}

  /// 开始加载网页
  - (void)webViewDidStartLoad:(UIWebView *)webView {
      NSURLRequest *request = webView.request;
      NSLog(@"webViewDidStartLoad-url=%@--%@",[request URL],[request HTTPBody]);
  }
  /// 网页加载完成
  - (void)webViewDidFinishLoad:(UIWebView *)webView {
      NSURLRequest *request = webView.request;
      NSURL *url = [request URL];
      if ([url.path isEqualToString:@"/normal.html"]) {
          NSLog(@"isEqualToString");
      }
      NSLog(@"webViewDidFinishLoad-url=%@--%@",[request URL],[request HTTPBody]);
      NSLog(@"%@",[self.webView stringByEvaluatingJavaScriptFromString:@"document.title"]);
  }
  /// 网页加载错误
  - (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
      NSURLRequest *request = webView.request;
      NSLog(@"didFailLoadWithError-url=%@--%@",[request URL],[request HTTPBody]);
      
  }

WKWebView使用

  1. 简单使用
  - (void)simpleExampleTest {
  // 1.创建webview,并设置大小,"20"为状态栏高度
  WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 20, self.view.frame.size.width, self.view.frame.size.height - 20)];
  // 2.创建请求
  NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.cnblogs.com/mddblog/"]];
  // 3.加载网页
  [webView loadRequest:request];
  
  // 最后将webView添加到界面
  [self.view addSubview:webView];
}
  1. 一些实用函数

加载网页函数,相比UIWebview,WKWebView也支持各种文件格式,并新增了loadFileURL函数,顾名思义加载本地文件。

    /// 模拟器调试加载mac本地文件
  - (void)loadLocalFile {
      // 1.创建webview,并设置大小,"20"为状态栏高度
      WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 20, self.view.frame.size.width, self.view.frame.size.height - 20)];
      // 2.创建url  userName:电脑用户名
      NSURL *url = [NSURL fileURLWithPath:@"/Users/userName/Desktop/bigIcon.png"];
      // 3.加载文件
      [webView loadFileURL:url allowingReadAccessToURL:url];
      // 最后将webView添加到界面
      [self.view addSubview:webView];
  }
  
  /// 其它三个加载函数
  - (WKNavigation *)loadRequest:(NSURLRequest *)request;
  - (WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
  - (WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL;

网页导航刷新相关函数

和UIWebview几乎一样,不同的是有返回值,WKNavigation(已更新),另外增加了函数reloadFromOrigin和goToBackForwardListItem。

reloadFromOrigin会比较网络数据是否有变化,没有变化则使用缓存,否则从新请求。

goToBackForwardListItem:比向前向后更强大,可以跳转到某个指定历史页面

@property (nonatomic, readonly) BOOL canGoBack;
@property (nonatomic, readonly) BOOL canGoForward;
- (WKNavigation *)goBack;
- (WKNavigation *)goForward;
- (WKNavigation *)reload;
- (WKNavigation *)reloadFromOrigin; // 增加的函数
- (WKNavigation *)goToBackForwardListItem:(WKBackForwardListItem *)item; // 增加的函数
- (void)stopLoading;

参考文章

UIWebView、WKWebView使用详解

深入剖析 iOS 与 JS 交互

你可能感兴趣的:(webview的混合开发)