UIWebView和WKWebView与JS的交互详解

7,295 阅读14分钟

参考文章WKWebView官方文档

简介

相信很多公司的app都有网页的嵌入吧,原生APP和JS交互的方式有UIWebView、WKWebView、Cordava、Weex、Flutter、Reactive Native等,我们目前比较常用的是WKWebView,但是本篇文章我准备讲解一下UIWebView和WKWebView和JS的交互。

UIWebView继承自UIView,是iOS内置的浏览器控件,可以浏览网页、打开文档等。能够加载html、htm、pdf、docx、txt等格式的文件。 iOS8,苹果新推出了WebKit,用WKWebView代替UIWebView和WebView。相关的使用和特性可以细读。性能、稳定性、功能大幅度提升 允许JavaScript的Nitro库加载并使用(UIWebView中限制)、支持了更多的HTML5特性、高达60fps的滚动刷新率以及内置手势、GPU硬件加速、KVO、重构UIWebView成14类与3个协议。

WKWebView是现代WebKit API在iOS8和OS X Yosemite应用中的核心部分。它代替了UIKit的UIWebView和APPKit中的WebView,提供了统一的跨双平台API,目前主要使用WKWebView。

好笑

一、UIWebView

1、UIWebView的基本用法

初始化一个UIWebView,并调用UIWebView网页加载展示的方法,并实现UIWebView的代理方法,基本上就可以实现网页加载的功能了。

1.1 UIWebView的加载方法


1、//使用 NSURLRequest 的方式加载网页
- (void)loadRequest:(NSURLRequest *)request;
2、/* 
    功能:加载HTML字符串
    string为要加载的本地HTML字符串
    baseURL用来确定htmlString的基准地址,相当于HTML的<base>标签的作用,定义页面中所有链接的默认地址
*/
- (void)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
3、- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType
textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL;

1.2.UIWebView的代理方法

//是否允许加载网页,也可获取js要打开的url,通过截取此url可与js交互
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request 
                                                 navigationType:(UIWebViewNavigationType)navigationType;
//开始加载网页
- (void)webViewDidStartLoad:(UIWebView *)webView;
//网页加载完成
- (void)webViewDidFinishLoad:(UIWebView *)webView;
//网页加载错误
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error;

2、UIWebView OC调用JS

2.1 stringByEvaluatingJavaScriptFromString:

是一个比较常用的方法,使用起来比较简单直接,直接调用- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;就可以了,示例如下:

self.title = [self.webView stringByEvaluatingJavaScriptFromString:@"document.title"];

这个方法虽然简单,但是有很多缺点:

  • 1.该方法不能判断调用了一个js方法之后,是否发生了错误。当错误发生时,返回值为nil,而当调用一个方法本身没有返回值时,返回值也为nil,所以无法判断是否调用成功了。
  • 2、返回值类型为nullable NSString *,当调用的js方法有返回值时,就都以字符串返回,不够灵活。当返回值是一个js的Array时,还需要解析字符串,就会比较麻烦。
  • 3、stringByEvaluatingJavaScriptFromString只能在主线程执行。

对于以上的缺点,可以通过使用JavaScriptCore(iOS 7.0 +)来解决。

2.2 JavaScriptCore(iOS 7.0 +)

JSPatch 最近被苹果禁止在appStore中的应用中使用, JSPatch中最核心的是JavaScriptCore,因为JavaScriptCore的JS到OC的映射,可以将js方法替换成为oc方法,所以其动态性(配合runtime的不安全性)也就成为了JSPatch被Apple禁掉的最主要原因。这里讲下UIWebView通过JavaScriptCore来实现OC调用JS。其实WebKit都有一个内嵌的js环境,一般我们在页面加载完成之后,获取js上下文,然后通过JSContext的evaluateScript:方法来获取返回值。因为该方法得到的是一个JSValue对象,所以支持JavaScript的Array、对象等数据类型。

用法如下:


JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
JSValue *value = [context evaluateScript:@"document.title"];
self.title = value.toString;
 

假设我们执行了一个不存在的方法的话,会出现什么样的情况呢?比如getSize方法

[self.context evaluateScript:@"document.getSize"];

结果可以知道程序报错,我们可以通过@property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception);,设置exceptionHandler来获取异常。这个方法也很好的解决了程序出现异常之后捕获不到异常信息的情况,用法如下:


//在调用不存在的方法getSize前,先设置异常回调
[self.context setExceptionHandler:^(JSContext *context, JSValue *exception){
        NSLog(@"程序异常为:%@", exception);
}];
//执行getSize方法
JSValue *value = [self.context evaluateScript:@"document.getSize"];

3、UIWebView JS调用OC

3.1 URL拦截方式

假如有一个使用短信验证码登录的功能,html或者js中的方法名为mobileCode,点击使用短信验证码登录按钮的时候会捕捉下面的链接,解析出所需的参数,从而实现JS 调用OC。

 <a href="mobileCode://smsLogin?username=13678946758&code=122786">使用短信验证码登录</a>

OC代码中,当打开了一个链接,webView会通过代理方法捕捉到链接,并且返回NO,从而可以实现我们的OC方法。捕捉不到的话就返回YES,继续跳转到html页面。

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    NSURL *URL = request.URL;    
    if ([URL.scheme isEqualToString:@"mobileCode"]) {
        if ([URL.host isEqualToString:@"smsLogin"]) {
            NSLog(@"使用短信验证码登录,参数为 %@", URL.query);
            return NO;
        }
    }
    return YES;
}

3.2 JavaScriptCore (iOS 7.0+)

首先我们在js文件中定义了一个share方法

function share (title, content, imageUrl, url) {
                //使用WKWebView测试
                window.webkit.messageHandlers.share.postMessage({title: title, content: content, imageUrl: imageUrl, url: url});
                //OC实现代码
            }

html中实现一个a标签调用share方法

<a href="javascript:void(0);" class="sharebtn" onclick="share('领取话费','分享链接给你的微信号又或者qq好友,即可领取1元话费' 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1566203866173&di=7a3035ce1c25fb6f1003ca2eeca7f2cd&imgtype=0&src=http%3A%2F%2Fimg1.juimg.com%2F180405%2F355858-1P40511025273.jpg', location.href)">分享领话费</a>

我们在OC中的实现如下

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
//    self.title = [self.title stringByAppendingString:[webView stringByEvaluatingJavaScriptFromString:@"document.title"]];
    //获取该UIWebView的javascript上下文
    JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    
    //这也是一种获取标题的方法。
    JSValue *value = [context evaluateScript:@"document.title"];
    //更新标题
    self.title = value.toString;
    
    [self convertJSToOCMethod];
}


#pragma mark - 将JS的函数转换成OC的方法

- (void)convertJSToOCMethod
{
    //获取该UIWebview的javascript上下文
    //self持有context
    //@property (nonatomic, strong) context *context;
    self.context = [self.webView valueForKeyPath:@"context"];
    
    //context oc调用js
    //JSValue *value = [self.context evaluateScript:@"document.title"];
    
    //js调用oc
    //其中share就是js的方法名称,赋给是一个block,block中是oc代码
    //此方法最终将打印出所有接收到的参数,js参数是不固定的
    self.context[@"share"] = ^() {
        //获取到share方法里的所有参数array
        NSArray *array = [JSContext currentArguments];
        //array中的元素JSValue对象转换为OC对象
        NSMutableArray *messages = [NSMutableArray array];
        for (JSValue *value in array) {
            [messages addObject:[value toObject]];
        }
        NSLog(@"点击分享按钮js传回的参数如下:\n%@", messages);
    };
    

点击html中的分享领话费按钮会在控制台打印出传递参数

4、UIWebView的Cookie管理

Cookie,有时也用其复数形式 Cookies,指某些网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据(通常经过加密)。 UIWebView的cookie一般是由[NSHTTPCookieStorage sharedHTTPCookieStorage]这个单例来管理的,UIWebView会自动同步单例中的Cookie。特殊情况要通过添加Cookie区分的时候可以通过以下几种方式来实现

  • 添加header实现
 NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.LynkCo.com"]];
    [request addValue:@"cookitnmae=78965420;" forHTTPHeaderField:@"Set-Cookie"];
    [self.webView loadRequest:request];
  • 通过操作NSHTTPCookieStorage添加一个自定义的Cookie
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:@{
    NSHTTPCookieName: @"cookieNmae", 
    NSHTTPCookieDomain: @".LynkCo.com",
    NSHTTPCookiePath: @"/"
    NSHTTPCookieValue: @"78965420", 
}];
//Cookie存在则覆盖,不存在添加
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];   
  • 读取所有Cookie,Cookie转换成HTTPHeaderFields,并添加到request的header中
NSArray *cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies;
//Cookies数组转换为requestHeaderFields
NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
//设置请求头
request.allHTTPHeaderFields = requestHeaderFields;

WKWebView

1、WKWebView的基本用法

1.1 WKWebView 的初始化

整体的初始化示例

- (void)createWebView
{
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]
    WKUserContentController *controller = [[WKUserContentController alloc] init];
    config.userContentController = controller;
    // 根据需要去设置对应的属性
    WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
    webView.navigationDelegate = self;
    [self.view addSubview:webView];    

    NSURL *url = [NSURL URLWithString:self.strURL];
    [self loadWebViewWithURL:url];    // JS调用OC 添加处理脚本
    [self.webView.configuration.userContentController addScriptMessageHandler:self name:@"Share"];
}

常用创建方法

- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;

介绍以下WKWebViewConfiguration中的两个比较重要的属性

  • 属性 WKWebsiteDataStore *websiteDataStore

WKWebView的一些缓存存储在websiteDataStore中,修改缓存可以通过WKWebsiteDataStore.h中提供的方法,事实上我们用的比较少,一般情况下清除缓存可以通过删除沙盒目录中的Cache文件。

  • 属性 WKUserContentController *userContentController

js和oc的交互以及动态注入js会用到这个属性。

1.2 WKWebView的一些类以及类的属性和方法

  • WKWebView 常用属性:
// 导航代理
@property (nullable, nonatomic, weak) id <WKNavigationDelegate> navigationDelegate;
// UI代理
@property (nullable, nonatomic, weak) id <WKUIDelegate> UIDelegate;

// 页面标题, 一般使用KVO动态获取
@property (nullable, nonatomic, readonly, copy) NSString *title;
// 页面加载进度, 一般使用KVO动态获取
@property (nonatomic, readonly) double estimatedProgress;

// 可返回的页面列表, 已打开过的网页, 有点类似于navigationController的viewControllers属性
@property (nonatomic, readonly, strong) WKBackForwardList *backForwardList;

// 页面url
@property (nullable, nonatomic, readonly, copy) NSURL *URL;
// 页面是否在加载中
@property (nonatomic, readonly, getter=isLoading) BOOL loading;
// 是否可返回
@property (nonatomic, readonly) BOOL canGoBack;
// 是否可向前
@property (nonatomic, readonly) BOOL canGoForward;
// WKWebView继承自UIView, 所以如果想设置scrollView的一些属性, 需要对此属性进行配置
@property (nonatomic, readonly, strong) UIScrollView *scrollView;
// 是否允许手势左滑返回上一级, 类似导航控制的左滑返回
@property (nonatomic) BOOL allowsBackForwardNavigationGestures;

//自定义UserAgent, 会覆盖默认的值 ,iOS 9之后有效
@property (nullable, nonatomic, copy) NSString *customUserAgent

  • WKWebView 常用方法

// 带配置信息的初始化方法
// configuration 配置信息
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration
// 加载请求
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
// 加载HTML
- (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
// 返回上一级
- (nullable WKNavigation *)goBack;
// 前进下一级, 需要曾经打开过, 才能前进
- (nullable WKNavigation *)goForward;
// 刷新页面
- (nullable WKNavigation *)reload;
// 根据缓存有效期来刷新页面
- (nullable WKNavigation *)reloadFromOrigin;
// 停止加载页面
- (void)stopLoading;
// 执行JavaScript代码
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;

1.3 WKWebView 动态注入JS

示例代码如下

/* 第一步:通过给userContentController添加WKUserScript,可以实现动态注入js。比如我先注入一个脚本,给每个页面添加一个Cookie */
//添加自定义的cookie
WKUserScript *newCookieScript = [[WKUserScript alloc] initWithSource:@"                document.cookie = 'LynkcoCookie=Lynkco;'" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
//添加脚本
[controller addUserScript:newCookieScript];   

/* 第二步骤:注入一个脚本,每当页面加载,就会alert当前页面cookie,在OC中的实现 */
//创建脚本
WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:@"alert(document.cookie);" injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
//添加脚本
[controller addUserScript:script];

注入的js 资源可以是js字符串,也可以是js文件,比如我们要注入一个js文件ImageClickEvent

/**
 页面中的所有img标签添加点击事件
 */
- (void)imgAddClickEvent
{
    //防止频繁IO操作,造成性能影响
    static NSString *jsSource;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        jsSource = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"ImageClickEvent" ofType:@"js"] encoding:NSUTF8StringEncoding error:nil];
    });
    //添加自定义的脚本
    WKUserScript *js = [[WKUserScript alloc] initWithSource:jsSource injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
    [self.webView.configuration.userContentController addUserScript:js];
    //注册回调
    [self.webView.configuration.userContentController addScriptMessageHandler:self name:@"imageDidClick"];
}

1.4 WKWebView 加载

加载的方法通常有以下几种

  • -(nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
  • -(nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL API_AVAILABLE(macosx(10.11), ios(9.0));
  • -(nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
  • -(nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL API_AVAILABLE(macosx(10.11), ios(9.0)); 但是通常情况下加载本地的html文件不用loadHTMLString:baseURL:方法,要使用loadRequest:方法。

[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"test" ofType:@"html"]]]];

1.5 WKWebView 代理方法

  • @protocol WKNavigationDelegate; //类似于UIWebView的加载成功、失败、是否允许跳转等

  • @protocol WKUIDelegate; //主要是一些alert、打开新窗口之类的

以下是WKNavigationDelgate的一些协议方法

//下面这2个方法共同对应了UIWebView的 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
//先:针对一次action来决定是否允许跳转,action中可以获取request,允许与否都需要调用decisionHandler,比如decisionHandler(WKNavigationActionPolicyCancel);
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
//后:根据response来决定,是否允许跳转,允许与否都需要调用decisionHandler,如decisionHandler(WKNavigationResponsePolicyAllow);
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;

//开始加载,对应UIWebView的- (void)webViewDidStartLoad:(UIWebView *)webView;
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;

//加载成功,对应UIWebView的- (void)webViewDidFinishLoad:(UIWebView *)webView;
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;

//加载失败,对应UIWebView的- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error;
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;

2、WKWebView OC调用JS

2.1 通过JavaScriptCore调用

该方法很好的解决了UIWebView使用stringByEvaluatingJavaScriptFromString:方法的两个缺点(1. 返回值只能是NSString。2. 报错无法捕获)。比如说要获取webView的title除了self.webView.title,还可以通过以下方法

-(void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id result, NSError * _Nullable error))completionHandler; 用法示例

[self.webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable title, NSError * _Nullable error) {
        NSLog(@"调用evaluateJavaScript异步获取title:%@", title);
}];

3、WKWebView JS调用OC

3.1 URL拦截方式

和UIWebView的拦截方式一致,具体可参照本文3.1,在此就不做赘述。

3.2 addScriptMessageHandler 方法

在OC中添加一个scriptMessageHandler,则会在all frames中添加一个js的function: window.webkit.messageHandlers..postMessage() ,涉及到的方法:

  • -(void)addScriptMessageHandler:(id )scriptMessageHandler name:(NSString *)name;
  • -(void)removeScriptMessageHandlerForName:(NSString *)name;

3.3 使用示例,调用过程

第一步:在OC中注册一个handler回调

[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"choosePhoneContact"];

第二步:js中调用方法

window.webkit.messageHandlers.choosePhoneContact.postMessage(param);

第三步:oc中实现WKScriptMessageHandler回调

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"choosePhoneContact"]) {
        [self selectContactCompletion:^(NSString *name, NSString *phone) {
            NSLog(@"选择完成");
            //读取js function的字符串
            NSString *jsFunctionString = message.body[@"completion"];
            //拼接调用该方法的js字符串
            NSString *callbackJs = [NSString stringWithFormat:@"(%@)({name: '%@', mobile: '%@'});", jsFunctionString, name, phone];
            //执行回调
            [self.webView evaluateJavaScript:callbackJs completionHandler:^(id _Nullable result, NSError * _Nullable error) {
                
            }];
        }];
    }
}

第四步:调用removeScriptMessageHandler移除回调

- (void)dealloc {
    [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"choosePhoneContact"];
}

4、WKWebView Cookie管理

加载Cookie的时候的几个注意事项

  • WKWebView加载网页得到的Cookie会同步到NSHTTPCookieStorage中。
  • WKWebView加载请求时,不会同步NSHTTPCookieStorage中已有的Cookie。
  • 通过共用一个WKProcessPool并不能解决2中Cookie同步问题,且可能会造成Cookie丢失。

4.1 wkwebVire首次加载Cookie不成功的问题

在请求头中添加cookie,这样的话只要保证[NSHTTPCookieStorage sharedHTTPCookieStorage]中存在你的cookie,第一次请求就不会有问题了。

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.LynkCo.com"]];
NSArray *cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies;
//Cookies数组转换为requestHeaderFields
NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
//设置请求头
request.allHTTPHeaderFields = requestHeaderFields;
[self.webView loadRequest:request];

4.2 ajax请求cookie丢失的问题

只需要通过添加WKUserScript就可以了,只要保证sharedHTTPCookieStorage中你的Cookie存在,后续Ajax请求就不会有问题。

/*!
 *  更新webView的cookie
 */
- (void)updateWebViewCookie
{
    WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource:[self cookieString] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
    //添加Cookie
    [self.configuration.userContentController addUserScript:cookieScript];
}

- (NSString *)cookieString
{
    NSMutableString *script = [NSMutableString string];
    [script appendString:@"var cookieNames = document.cookie.split('; ').map(function(cookie) { return cookie.split('=')[0] } );\n"];
    for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
        // Skip cookies that will break our script
        if ([cookie.value rangeOfString:@"'"].location != NSNotFound) {
            continue;
        }
        // Create a line that appends this cookie to the web view's document's cookies
        [script appendFormat:@"if (cookieNames.indexOf('%@') == -1) { document.cookie='%@'; };\n", cookie.name, cookie.da_javascriptString];
    }
    return script;
}

4.3 跳转页面cookie丢失问题

//核心方法:
/**
 修复打开链接Cookie丢失问题

 @param request 请求
 @return 一个fixedRequest
 */
- (NSURLRequest *)fixRequest:(NSURLRequest *)request
{
    NSMutableURLRequest *fixedRequest;
    if ([request isKindOfClass:[NSMutableURLRequest class]]) {
        fixedRequest = (NSMutableURLRequest *)request;
    } else {
        fixedRequest = request.mutableCopy;
    }
    //防止Cookie丢失
    NSDictionary *dict = [NSHTTPCookie requestHeaderFieldsWithCookies:[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies];
    if (dict.count) {
        NSMutableDictionary *mDict = request.allHTTPHeaderFields.mutableCopy;
        [mDict setValuesForKeysWithDictionary:dict];
        fixedRequest.allHTTPHeaderFields = mDict;
    }
    return fixedRequest;
}

#pragma mark - WKNavigationDelegate 

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {

#warning important 这里很重要
    //解决Cookie丢失问题
    NSURLRequest *originalRequest = navigationAction.request;
    [self fixRequest:originalRequest];
    //如果originalRequest就是NSMutableURLRequest, originalRequest中已添加必要的Cookie,可以跳转
    //允许跳转
    decisionHandler(WKNavigationActionPolicyAllow);
    //可能有小伙伴,会说如果originalRequest是NSURLRequest,不可变,那不就添加不了Cookie了,是的,我们不能因为这个问题,不允许跳转,也不能在不允许跳转之后用loadRequest加载fixedRequest,否则会出现死循环,具体的,小伙伴们可以用本地的html测试下。
    
    NSLog(@"%@", NSStringFromSelector(_cmd));
}

#pragma mark - WKUIDelegate

- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {

#warning important 这里也很重要
    //这里不打开新窗口
    [self.webView loadRequest:[self fixRequest:navigationAction.request]];
    return nil;
}

总结

在iOS开发中,H5的嵌入可以通过UIWebView或者WKWebView。这两个都是继承UIView,来加载web数据的类。UIWebView是在iOS2的时候开始使用的。特点是加载速度慢,占用内存多,优化艰难。目前UIWebView在iOS12.0之后已被废弃,WKWebView是在iOS8苹果新推出的,加载速度快,占用内存较少,是一个不错的选择。

但是目前WKWebView依然存在很多坑,比如

  • 当WKWebView加载的网页占用内存过大时,会出现白屏现象
  • 如果不实现WKUIDelegate中的代理方法,js alert方法就不出现弹窗。所以还要实现这个方法

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

  • Cookie丢失,比如从一个登录状态的页面跳转到另一个页面cookie就会丢失。
  • evaluateJavaScript:completionHandler:只能通过异步回调的方式来实现
  • 自定义contentInset页面刷新时会出现页面跳动的问题,比如self.webView.scrollView.contentInset = UIEdgeInsetsMake(64, 0, 44, 0);可以通过kvc给私有变量_obscuredInsets设置值,

[self.webView setValue:[NSValue valueWithUIEdgeInsets:self.webView.scrollView.contentInset] forKey:@"_obscuredInsets"]。

  • 不支持拦截NSURLProtocol。如果要拦截可以使用UIWebView

DEMO地址