Overview

由于iOS13 将UIWebView废弃了, 所有使用UIWebView的APP都会直接被拒绝上架。(😌一个时代的终结啊,哈利路亚)。顺势而上的是我们的WKWebview,以内存占用低,刷新率高并支持多种手势而引来开发者的一众欢呼。下图展示了WKWebview的大致信息,下文中会逐一展开解释。

接入

使用WKWebview的大致流程如下:

  1. 创建一个WKWebview
  2. 完成基本设置,包括frame,configuration等
  3. 设置相关的代理
  4. 加载要加载的URL
  5. 在相应的代理回调中处理相关逻辑

初始化

//initialize
#import <WebKit/WebKit.h>

//MARK: lifecycle
- (void)viewDidLoad {
    [super viewDidLoad];
    WKWebView *myWebview = [[WKWebView alloc] initWithFrame:UIScreen.mainScreen.bounds];
    [self.view addSubview:myWebview];

    [myWebview.configuration setValue:@YES forKey:@"_allowUniversalAccessFromFileURLs"];// 允许加载本地文件,加载本地H5时会用到
    myWebview.scrollView.bounces = NO;//在网页处于最顶端时,禁止下拉拖动
    myWebview.allowsLinkPreview = NO; //禁止a标签链接长按预览

    myWebview.UIDelegate = self;
    myWebview.navigationDelegate = self;

    NSURL *myUrl = [[NSURL alloc] initWithString:@"https://www.zhihu.com"];
    NSURLRequest *myRequest = [[NSURLRequest alloc] initWithURL:myUrl];
    [myWebview loadRequest:myRequest];
}

WKNavigation Delegate

//MARK: WKNavigation Delegate Method

//决定是否可以跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
    FUNCLog();
    decisionHandler(WKNavigationActionPolicyAllow);
}

- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
    FUNCLog();
    decisionHandler(WKNavigationResponsePolicyAllow);
}

- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation{
    FUNCLog();
}

- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation{
    FUNCLog();
}

- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
    FUNCLog();
}

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    FUNCLog();
}

- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
    FUNCLog();
}

每次加载一个页面都会调用这几个方法. 成功加载一个页面的调用顺序为: decidePolicyForNavigationAction -> didStartProvisionalNavigation -> decidePolicyForNavigationResponse -> didCommitNavigation -> didFinishNavigation 即: 请求导航 -> 开始导航 -> 知道了返回内容,是否允许加载 -> 开始接受内容 -> 加载结束 WKUIDelegate 这两个方法主要用来处理JS中的alert弹窗和confirm处理。

//MARK: WKUIDelegate Method

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
    FUNCLog();
}

- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler {
    FUNCLog();
}

JS交互

WKWebview中Native和JS的交互有多种方式,可以在decidePolicyForNavigationAction中拦截scheme来实现交互,也可以通过WKScriptMessageHandler协议来实现,这里特指后一种方式。

1. 使用官方提供的方法

  • JS调Native ① Native和JS 约定好调用的方法名 ② iOS使用WKUserContentControlleraddScriptMessageHandler:name:方法监听之前约定好的那个方法名 ③ JS通过window.webkit.messageHandlers.{约定的名字}.postMessage()的方式发送消息 ④ iOS在-userContentController:didReceiveScriptMessage这个回调方法中接收收到的数据

    注:addScriptMessageHandler: name:方法可能会引起循环引用问题。一般来说,在合适的时机removeScriptMessageHandler可以解决此问题。比如:在-viewWillAppear:方法中执行add操作,在-viewWillDisappear:方法中执行remove操作。

    OC端

   //! 导入WebKit框架头文件
   #import <WebKit/WebKit.h>
   //! WKWebViewWKScriptMessageHandlerController遵守WKScriptMessageHandler协议
   @interface EXPJSCallNative () <WKScriptMessageHandler>
  //! 为userContentController添加ScriptMessageHandler,并指明name
  WKUserContentController *userContentController = [[WKUserContentController alloc] init];
  [userContentController addScriptMessageHandler:self name:@"jsToOc"];

  //! 使用添加了ScriptMessageHandler的userContentController配置configuration
  WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
  configuration.userContentController = userContentController;

  //! 使用configuration对象初始化webView
  _webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
  #pragma mark - WKScriptMessageHandler
  //! WKWebView收到ScriptMessage时回调此方法
  - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {

      if ([message.name caseInsensitiveCompare:@"jsToOc"] == NSOrderedSame) {
          [WKWebViewWKScriptMessageHandlerController showAlertWithTitle:message.name message:message.body cancelHandler:nil];
      }
  }

前端

//! 登录按钮
<button onclick = "login()" style = "font-size: 18px;">登录</button>
//! 登录
function login() {
  var token = "js_tokenString";
  loginSucceed(token);
}
//! 登录成功
function loginSucceed(token) {
  var action = "loginSucceed";
  window.webkit.messageHandlers.jsToOc.postMessage(action, token);
}
  • Native调JS
    • 准备好要执行的JS字符串
    • iOS使用-evaluateJavaScript:completionHandler:方法执行要注入的JS
    • JS执行相关方法后可在completionHandler的回调中处理相关逻辑

2. 使用第三方库 - WebViewJavascriptBridge

该库的接入可使用cocoapods或者手动接入,具体情况参考源代码库。

引入头文件

#import "WebViewJavascriptBridge.h

申明一个bridge变量

@property WebViewJavascriptBridge* bridge;

Native call JS 或者 JS call Native

   //JS调Native,注册一个待调用函数
   [self.bridge registerHandler:@"ObjC Echo" handler:^(id data, WVJBResponseCallback responseCallback) {
   	NSLog(@"ObjC Echo called with: %@", data);
   	responseCallback(data);
   }];
   //Native调JS, callHandler中直接加JS代码
   [self.bridge callHandler:@"JS Echo" data:nil responseCallback:^(id responseData) {
   	NSLog(@"ObjC received response: %@", responseData);
   }];

前端处理 传统JS方法可以直接拷贝下面代码进要使用的JS文件中,Vue可以专门存一个Bridge.js文件,再进行引入。

      function setupWebViewJavascriptBridge(callback) {
      	if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
      	if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
      	window.WVJBCallbacks = [callback];
      	var WVJBIframe = document.createElement('iframe');
      	WVJBIframe.style.display = 'none';
      	WVJBIframe.src = 'https://__bridge_loaded__';
      	document.documentElement.appendChild(WVJBIframe);
      	setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
      }

前端调用

   	setupWebViewJavascriptBridge(function(bridge) {
         	//Native call JS
         	bridge.registerHandler('JS Echo', function(data, responseCallback) {
         		console.log("JS Echo called with:", data)
         		responseCallback(data)
         	})
         	//JS call Native
         	bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) {
         		console.log("JS received response:", responseData)
         	})
   	})

相关API

WKWebview API

基本信息

Name Info
configuration : WKWebViewConfiguration 用于初始化web视图的配置副本
title : NSString 网页的标题
URL : NSURL 活动链接
scrollView : UIScrollView 与WebView相关联的滚动视图
- initWithFrame:configuration: 用指定的frame和configuration初始化视图

委托

Name Info
UIDelegate UIDelegate代理
navigationDelegate WKNavigationDelegate代理

加载

Name Info
estimatedProgress : double 值为0-1,表示当前页面加载的进度
hasOnlySecureContent : BOOL 表示页面上的所有资源是否通过安全加密的连接加载
loading : BOOL 表示当前页面是否正在加载
- loadRequest: 加载一个URL请求
- loadHTMLString:baseURL: 加载本地html, 如果不涉及外部资源baseURL可以填nil, 否则填相关的url
- loadFileURL: allowingReadAccessToURL: 加载本地文件资源
- reload 重新加载当前页面
- reloadFromOrigin 重新加载当前页面, 如果可能, 使用缓存验证条件执行端到端重新验证
- stopLoading 停止加载当前页面所有资源

缩放

Name Info
allowsMagnification : BOOL 表示放大手势是否会改变网页视图的放大倍数
magnification : CGFloat 页面内容当前的缩放因子,默认是1.0
- setMagnification:centeredAtPoint: 按指定的因子缩放页面内容,并将结果居中在指定的点上

导航

Name Info
allowsBackForwardNavigationGestures : BOOL 表示水平滑动手势是否会触发后退列表导航,默认为NO
backForwardList : WKBackForwardList 网页视图的后退列表, 即之前访问过的web页面的列表
canGoBack : BOOL 指示后退列表中是否有可被导航到的后退项, 即是否可以可以回退
canGoForward : BOOL 指示后退列表中是否有可被导航到的前进项, 即是否可以前进
allowsLinkPreview : BOOL 用于确定是否长按/连续按下可以显示链接目标的预览
- goBack 导航到后退列表中的后退项中
- goForward 导航到后退列表中的前进项中
- goToBackForwardListItem 导航到后退列表中的某一个网页项, 并将其设置为当前项

js调用相关

Name Info
- evaluateJavaScript:completionHandler 苹果官方OC调用JS的方法

WKWebViewConfiguration API

使用WKWebViewConfiguration类,你可以确定网页呈现的速度、媒体播放的处理方式等等。 WKWebViewConfiguration仅在首次初始化WebView视图的时候使用,当WebView视图被创建以后,你就无法再使用此类来更改WebView的配置信息了

基础配置

Name Info
applicationNameForUserAgent : NSString 在用户代理字符串中使用的应用程序的名称
preferences : WKPreferences web视图要使用的首选项对象
processPool : WKProcessPool 视图的web内容进程所在的进程池
userContentController : WKUserContentController 和Web视图相关联的内容控制器
websiteDataStore : WKWebsiteDataStore 由网页视图使用的存储的网站数据

页面控制

Name Info
ignoresViewportScaleLimits : BOOL 是否永远允许网页缩放
suppressesIncrementalRendering : BOOL 指示网络视图是否在【内容渲染完全加载到内存之前】禁止内容呈现,默认是NO。

媒体播放首选项

Name Info
allowsInlineMediaPlayback : BOOL 指示HTML5视频是否内嵌播放, 或使用native全屏控制器
allowsAirPlayForMediaPlayback : BOOL 是否允许使用AirPlay
allowsPictureInPictureMediaPlayback : BOOL 是否支持使用画中画
mediaTypesRequiringUserActionForPlayback : WKAudiovisualMediaTypes 确定哪些类型需要用户手势才能播放
WKAudiovisualMediaTypes 枚举类型, 需要用户手势开始播放的媒体类型

其他

Name Info
selectionGranularity : WKSelectionGranularity 用户可以在网页视图中交互地选择内容的粒度级别
userInterfaceDirectionPolicy : WKUserInterfaceDirectionPolicy 用户界面元素的方向
dataDetectorTypes : WKDataDetectorTypes 所需的数据监测类型

踩过的坑

ATS(App Transport Security)问题

关于这个问题,喵神已经在他的的博客中已经讲的很清楚了,我这里不再赘述。讲几个因为这个问题而引发的几种问题吧。

  1. 加载本地静态H5/访问不安全的站点(如http页、或直接以IP : PORT形式存在)而引起的白屏
  2. H5中有不安全站点的ajax请求

解决 针对第一种情况,只需在项目的info.plist中添加App Transport Security Settings -> Allow Arbitrary Loads(YES)即可,而对于第二种情况还需要添加Exception Domains,如下图: source code

	<key>NSAppTransportSecurity</key>
	<dict>
		<key>NSAllowsArbitraryLoads</key>
		<true/>
		<key>NSAllowsArbitraryLoadsInWebContent</key>
		<true/>
		<key>NSAllowsLocalNetworking</key>
		<true/>
		<key>NSExceptionDomains</key>
		<dict>
			<key>172.20.156.68</key>
			<dict/>
      				<key>NSIncludesSubdomains</key>
      				<true/>
      				<key>NSExceptionAllowsInsecureHTTPLoads</key>
		        	<true/>
			<key>222.186.209.130</key>
			<dict>
            			<key>NSExceptionAllowsInsecureHTTPLoads</key>
            			<true/>
            			<key>NSIncludesSubdomains</key>
            			<true/>
			</dict>
		</dict>
	</dict>

cookie丢失问题

WKWebView在打开时不会自动去NSHTTPCookieStorage获取cookie信息,于是在一些请求中便不会带上cookie而导致302-重定向问题WWDC 2017推荐使用WKHTTPCookieStore来管理cookie值。 WKHTTPCookieStore通过其三个方法就可以很方便的管理cookie值:

//在decidePolicyForNavigationResponse回调中获取当前websiteDataStore中的cookie值,保存到本地
//这样下次请求中便会带上相应的cookie值
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
{
    if(@available(iOS 11, *)){
            //WKHTTPCookieStore的使用
            WKHTTPCookieStore *cookieStore = myWebView.configuration.websiteDataStore.httpCookieStore;
            //获取 cookies
            [cookieStore getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull cookies) {
                for (NSHTTPCookie *cookie in cookies) {
                        //NSHTTPCookie cookie
                    NSLog(@"cookieStore-cookies_ :%@    %@", cookie.name, cookie.value);
                   [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
                }
            }];
        }
    decisionHandler(WKNavigationResponsePolicyAllow);
}

页面中的下载问题

WKWebView中点击网页中的下载事件会变成直接预览该网页,目前的解决方案是在didStartProvisionalNavigation进行拦截,如果url地址后缀是文件类型,则进行拦截,并发起HTTP请求,将文件下载到本地。

    NSURL *fileURL = navigationAction.request.URL;
    NSString *internalFileExtension = [fileURL.absoluteString.pathExtension lowercaseString];
    if ([internalFileExtension isEqualToString:@"png"] ||
        [internalFileExtension isEqualToString:@"txt"] ||
        [internalFileExtension isEqualToString:@"jpg"] ||
        [internalFileExtension isEqualToString:@"pdf"] ||
        [internalFileExtension isEqualToString:@"doc"] ||
        [internalFileExtension isEqualToString:@"docx"]||
        [internalFileExtension isEqualToString:@"ppt"] ||
        [internalFileExtension isEqualToString:@"pptx"]||
        [internalFileExtension isEqualToString:@"md"])
    {
         HCDownloadViewController *dlvc = [[HCDownloadViewController alloc] init];
         UINavigationController *vc = [[UINavigationController alloc] initWithRootViewController:dlvc];
         vc.transitioningDelegate  = self;
         dlvc.delegate = self;
        //Fire download
         NSLog(@"fileUrl = %@",fileURL);
         [dlvc downloadURL:fileURL userInfo:nil];
         [self presentViewController:vc animated:YES completion:nil];
        decisionHandler(WKNavigationActionPolicyCancel);
    }else{
        decisionHandler(WKNavigationActionPolicyAllow);
    }

参考链接

联系方式: 邮箱 zhijian.eric@gmail.com 欢迎交流骚扰