JHHK

欢迎来到我的个人网站
行者常至 为者常成

WKWebView(2)

目录

关键类介绍

一、WKWebViewConfiguration

WKWebViewConfiguration 是用于配置 WKWebView 实例的类,允许你在创建 WKWebView 对象时设置一些选项和属性,以满足特定的需求。
通过配置对象,你可以影响 WebView 的行为、添加脚本、设置用户内容控制规则等。

以下是 WKWebViewConfiguration 的一些主要属性和用法:

userContentController 属性:
这个属性允许你配置 WKUserContentController 对象,用于处理与 JavaScript 通信,注入自定义脚本等

WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
// 添加脚本消息处理器<WKScriptMessageHandler>
[configuration.userContentController addScriptMessageHandler:self name:@"myHandler"];

applicationNameForUserAgent 属性:
该属性允许你设置 WebView 的 User-Agent 中包含的应用程序名称。

WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
configuration.applicationNameForUserAgent = @"MyApp";

二、WKWebsiteDataStore介绍

1、 Local Storage 与 Session Storage 的区别
Storage是浏览器的本地存储

特性 Local Storage Session Storage
数据生命周期 持久存储,除非显式清除 临时存储,页面会话结束(标签页/窗口关闭)即删除
数据共享范围 同一域名下所有标签页/窗口共享 仅限当前标签页或窗口
用途 保存长期有效的数据(如用户设置、Token、购物车) 保存短期数据(如临时表单数据、页面状态)
清除方式 必须通过代码或手动清除 自动清除(关闭会话即删除)
存储大小 通常为 5MB,但具体取决于浏览器 通常为 5MB,但具体取决于浏览器

2、WebSQL 与 IndexedDB 的对比
WebSQL 与 IndexedDB 是浏览器的数据库存储

特性 WebSQL IndexedDB
API 类型 基于 SQL 查询语言 基于 NoSQL 键值对存储
支持情况 被废弃,不推荐使用 现代浏览器推荐的客户端存储解决方案
性能 简单查询性能高 支持更复杂的查询和事务
兼容性 仅部分浏览器支持(如 Safari 和 Chrome) 所有现代浏览器支持
未来方向 已停止支持,可能被逐渐移除 持续改进和增强

3、Local Storage 可以存储数据,为什么还需要用cookie?
Storage用于本地存储数据,cookies用于存储少量需要服务器端读取的数据(如会话标识)。

特性 Local Storage Cookies
存储大小 通常为 5MB 通常为 4KB
数据生命周期 持久存储,除非手动清除 可设置到期时间(会话或持久)
数据传输 仅客户端可用,不随请求发送到服务器 每次 HTTP 请求都会自动发送到服务器
用途 用于存储大量、长期的客户端数据 用于存储少量需要服务器端读取的数据(如会话标识)
安全性 仅限客户端访问,容易受到 XSS 攻击 易受 XSS 和 CSRF 攻击,需加 HttpOnly 和 Secure 标志
访问方式 通过 JavaScript API 操作,易用性更强 通过 JavaScript API 或 HTTP 标头设置,操作稍复杂

4、WKWebsiteDataStore
WKWebsiteDataStore 是 WebKit 框架中的一个类,用于管理与网站相关的数据。
这个类的主要目的是提供一种方式来管理缓存、Cookies、本地存储等网站数据,以及处理网站数据的相关配置。

以下是 WKWebsiteDataStore 的一些主要功能和用途:

数据存储隔离: WKWebsiteDataStore 允许你为每个 WKWebView 实例创建一个独立的数据存储,从而实现不同 WebView 之间的数据隔离。这对于实现多个 WebView 之间的独立状态或隔离用户数据非常有用。

缓存管理: 通过 WKWebsiteDataStore,你可以清除和管理 WebView 缓存。这包括清除缓存中的资源文件、页面数据等。

Cookies 管理: 可以通过 WKWebsiteDataStore 来管理 Cookies,包括添加、删除和查询 Cookies。这对于处理用户身份验证和跟踪用户会话非常有用。

本地存储管理: WKWebsiteDataStore 允许你管理 WebView 的本地存储,包括本地数据库、localStorage 等。

HTTP 缓存策略: 提供了一些配置选项,用于设置 WebView 的 HTTP 缓存策略。

下面是一个简单的示例,展示了如何创建一个自定义的 WKWebsiteDataStore:

-(void)storeUsage {
    // 创建一个默认配置的 WKWebViewConfiguration
    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];

    // 创建一个自定义的 WKWebsiteDataStore,Non-Persistent Data Store(非持久数据存储,程序退出数据清除)
    WKWebsiteDataStore *dataStore = [WKWebsiteDataStore nonPersistentDataStore];
    
    // 获取默认store,持久存储,这种类型的数据存储会将数据永久保存在磁盘上,即使应用程序退出或设备重启,数据依然存在
    // 通常情况下,如果你需要在应用程序的多个 WKWebView 实例之间共享某些数据,可以使用持久数据存储。
    // 如果你只需要一个短暂的、与应用程序生命周期相对独立的数据存储,可以使用非持久数据存储。
    // 选择使用哪种数据存储取决于你的应用程序的需求和设计。
    // WKWebsiteDataStore *persistentDataStore = [WKWebsiteDataStore defaultDataStore];


    // 将自定义的 WKWebsiteDataStore 设置给 WKWebViewConfiguration
    configuration.websiteDataStore = dataStore;

    // 使用带有配置的 WKWebView 实例
    WKWebView *wkWebView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration];
}

清除缓存

- (void)__cleanWKWebViewCache{
    NSSet *websiteDataTypes = [NSSet setWithArray:@[WKWebsiteDataTypeDiskCache]];
    // NSSet *websiteDataTypes = [WKWebsiteDataStore allWebsiteDataTypes];
    
    NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0];
    
    [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes
                                               modifiedSince:dateFrom
                                           completionHandler:^{
    }];
}

提取cookie

// 提取 Cookie 并下载文件
+ (void)downloadFileFromURL:(NSURL *)url withWebViewCookies:(WKWebView *)webView finish:(void(^)(NSURL *fileURL))finish {
    
    // 提取 Cookie
    WKHTTPCookieStore *cookieStore = webView.configuration.websiteDataStore.httpCookieStore;
    
    // 获取所有 cookies
    __weak typeof(self) weakSelf = self;
    [cookieStore getAllCookies:^(NSArray<NSHTTPCookie *> *cookieArray) {
        
        // 创建下载请求并附加 Cookie
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
        
        // 将 cookies 附加到请求头
        NSDictionary *cookieHeaders = [NSHTTPCookie requestHeaderFieldsWithCookies:cookieArray];
        [request setAllHTTPHeaderFields:cookieHeaders];
        
        // 使用 NSURLSession 下载文件
        NSURLSession *session = [NSURLSession sharedSession];
        NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
            if (!error && finish) {
                NSString *fileName = [weakSelf queryItemValueWithUrl:url.absoluteString name:@"filename"];
                NSString *documentsPath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
                NSURL *fileURL = [NSURL fileURLWithPath:documentsPath];
                NSFileManager *fileManager = [NSFileManager defaultManager];
                [fileManager moveItemAtURL:location toURL:fileURL error:nil];
                dispatch_async(dispatch_get_main_queue(), ^{
                    finish(fileURL);
                });
            }
        }];
        [downloadTask resume];
    }];
}

native 和 js 通讯

一、native调用js方法

1、js代码

function ocCallJSFunction(data){
    alert(data.name);
    return 'good';
}

2、native代码

-(void)__ocCallJSFunction:(UIButton*)btn{
    NSString *jsfunc = @"ocCallJSFunction({'name':'tom'})";
    [self.wkWebView evaluateJavaScript:jsfunc
                     completionHandler:^(id _Nullable obj, NSError * _Nullable error) {
        
        // obj 是方法的返回值
        NSLog(@"evaluateJavaScript,obj=%@,error=%@",obj,error);
    }];
}

二、js调用native方法

1、js代码

//实现自定义的OCLog方法
function ocLog(text){
    //传递的信息
    try {
        // 调用oc注册到js环境中的方法ocLog,text为传递的参数
        window.webkit.messageHandlers.ocLog.postMessage(text)
    } catch(error) {
        console.log(error)
    }
}

//原生端注册getMessage
function jsCallNative(param){
    //传递的信息
    var jsonStr = '{"id":"666", "message":"我是传递的数据"}';
    try {
        // 调用oc注册到js环境中的方法getMessage,jsonStr为传递的参数
        window.webkit.messageHandlers.getMessage.postMessage(jsonStr)
    } catch(error) {
        console.log(error)
    }
}

2、native代码

WKWebViewConfiguration *config = [self __getWKWebViewConfiguration];
self.wkWebView = [[WKWebView alloc] initWithFrame:frame configuration:config];

// 注册 messageHandler
-(WKWebViewConfiguration*)__getWKWebViewConfiguration{
    WKWebViewConfiguration * config = [[WKWebViewConfiguration alloc] init];
    [config.userContentController addScriptMessageHandler:(id<WKScriptMessageHandler>)[XYProxy proxyWithTarget:self] name:@"getMessage"];
    [config.userContentController addScriptMessageHandler:(id<WKScriptMessageHandler>)[XYProxy proxyWithTarget:self] name:@"ocLog"];
    return config;
}


#pragma mark WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    //2023-12-18 15:54:47.815436+0800 XYApp[92178:35576261] xy:message.name = ocLog, message.body = ocLogTest, message.framInfo = <WKFrameInfo: 0x7fac8c910380; webView = 0x7fac8d074e00; isMainFrame = YES; request = <NSMutableURLRequest: 0x6000002a8680> { URL: file:///Users/lixiaoyi/Library/Developer/CoreSimulator/Devices/9DA297BF-FD4C-4A1B-A483-B35D97BF1F1F/data/Containers/Bundle/Application/3F199C2E-D536-47D7-B402-C309FE2EAA18/XYApp.app/Frameworks/XYTestModule.framework/XYTestModule.bundle/test.html }>
    NSLog(@"xy:message.name = %@, message.body = %@, message.framInfo = %@", message.name, message.body, message.frameInfo);
    
    
    if ([message.name isEqualToString:@"ocLog"]) {
        NSLog(@"xy:ocLog");
    }else if([message.name isEqualToString:@"getMessage"]){
        NSLog(@"xy:getMessage");
        //通过下面的方式将返回值传递给js
        [self.wkWebView evaluateJavaScript:completionHandler:];
    }

    NSString *methods = [NSString stringWithFormat:@"%@:", message.name];
    SEL selector = NSSelectorFromString(methods);
    if ([self respondsToSelector:selector]) {
        [self performSelector:selector onThread:[NSThread mainThread] withObject:message.body waitUntilDone:NO];
    } else {
        NSLog(@"xy:方法未实现:%@", methods);
    }
}

其它调用方式

//js调用原生事件 方法二:
//通过隐藏iframe的方式,加载一个url,Native的WebView拦截url,并执行相应方法
function jsCallNativeTwo(){
    var url = "event://jsCallNativeTwo";
    var iFrame;
    iFrame = document.createElement("iframe");
    iFrame.setAttribute("src", url);
    iFrame.setAttribute("style", "display:none;");
    iFrame.setAttribute("height", "0px");
    iFrame.setAttribute("width", "0px");
    iFrame.setAttribute("frameborder", "0");
    document.body.appendChild(iFrame);
    // 发起请求后这个 iFrame 就没用了,所以把它从 dom 上移除掉
    iFrame.parentNode.removeChild(iFrame);
    iFrame = null;
}

三、js注入

// js注入,注入一个alert方法,页面加载完毕弹出一个对话框。
// WKUserScriptInjectionTimeAtDocumentStart:页面刚渲染前执行;WKUserScriptInjectionTimeAtDocumentEND:页面渲染后执行。
// forMainFrameOnly:NO(全局窗口),yes(只限主窗口)
WKWebViewConfiguration * config = [[WKWebViewConfiguration alloc] init];
NSString *jscode = @"alert(\"WKUserScript注入js\");";
WKUserScript *userScript = [[WKUserScript alloc] initWithSource:jscode
                                                injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
                                            forMainFrameOnly:YES];
[config.userContentController addUserScript:userScript];

场景总结

一、User-Agent的获取和设置

1、获取 User-Agent

1.1 通过 webView.customUserAgent 是不可以直接获取当前的 User-Agent 的,除非你之前已经手动设置了它。

具体说明:
如果未设置 customUserAgent: webView.customUserAgent 返回 nil。
需要通过 evaluateJavaScript(“navigator.userAgent”) 来获取默认的 User-Agent。

如果已设置 customUserAgent:
webView.customUserAgent 返回你设置的自定义 User-Agent。
你仍然可以通过 evaluateJavaScript(“navigator.userAgent”) 来验证当前的 User-Agent 是否被正确设置。

// 设置 customUserAgent
webView.customUserAgent = "Your Custom User Agent"

// 检查 customUserAgent
if let customUA = webView.customUserAgent {
    print("Custom User-Agent: \(customUA)")
}

1.2 通过evaluateJavaScript(“navigator.userAgent”)获取

let webView = WKWebView()

// 获取默认的 User-Agent
webView.evaluateJavaScript("navigator.userAgent") { (result, error) in
    if let userAgent = result as? String {
        print("Default User-Agent: \(userAgent)")
    }
}

1.3 获取单次请求的User-Agent

// 拦截请求并获取请求头中的 User-Agent
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    if let request = navigationAction.request as? URLRequest,
       let userAgent = request.value(forHTTPHeaderField: "User-Agent") {
        print("User-Agent for this request: \(userAgent)")
    } else {
        // 如果请求头中没有 User-Agent,自定义处理逻辑
        print("User-Agent not found in this request.")
    }
    
    // 允许加载请求
    decisionHandler(.allow)
}

2、设置 User-Agent

2.1 通过 webView.customUserAgent 设置

let webView = WKWebView()
webView.customUserAgent = "Your Custom User Agent"

2.2 通过请求单独设置 User-Agent
如果你只想修改某些请求的 User-Agent,可以在创建 URL 请求时手动设置 User-Agent 头

var request = URLRequest(url: URL(string: "https://example.com")!)
request.setValue("Your Custom User Agent", forHTTPHeaderField: "User-Agent")
webView.load(request)

customUserAgent 属性会全局替换掉 WKWebView 的 User-Agent,并且适用于所有请求。
手动设置请求头中的 User-Agent 只会影响特定的请求,其他请求仍然会使用默认的 User-Agent。

二、进度条加载

通过监听estimatedProgress属性,拿到加载的进度

[weView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];

加载新的资源(比如图片)或者重定向(什么时候返回重定向是后台人员控制,有可能当前页面已经加载了50%)时,会出现进度条回退的现象,需要进行处理

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"estimatedProgress"]) {
        self.progresslayer.opacity = 1;
        //不要让进度条倒着走...有时候goback会出现这种情况
        if ([change[@"new"] floatValue] < [change[@"old"] floatValue]) { return;}
        
        self.progresslayer.frame = CGRectMake(0, 0, self.view.bounds.size.width * [change[@"new"] floatValue], 3);
        if ([change[@"new"] floatValue] == 1) {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                self.progresslayer.opacity = 0;
            });
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                self.progresslayer.frame = CGRectMake(0, 0, 0, 3);
            });
        }
    }else{
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

三、点击链接没有反应

如果H5页面中点击url是在新的标签页中打开(target=_blank),在wkwebview中会出现无法跳转的现象,如下代码

// 在新的标签也中打开时,会调用这个方法
<a href="https://www.baidu.com" target="_blank">跳转到百度,新页面打开</a>

解决方法

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

    WKFrameInfo *frameInfo = navigationAction.targetFrame;
    if (![frameInfo isMainFrame]) {
        [webView loadRequest:navigationAction.request];
    }
    return nil;
}

四、注入js实现图片长按保存

三方h5页面中图片没有长按保存功能,我们可以通过注入js实现图片长按保存的功能

- (void)addLongPressSaveImgForXiaoC:(WKWebViewConfiguration*)configuration {

      NSString *scriptSource = @"\
          var touchStartTime; \
          \
          document.body.addEventListener('touchstart', function(event) { \
            console.log('Body touchstart'); \
            event.preventDefault(); \
            touchStartTime = Date.now();\
          }, false); \
          \
          document.body.addEventListener('touchend', function(event) { \
              console.log('Body touchend'); \
              var touchEndTime = Date.now();\
              if (touchEndTime - touchStartTime > 1000) { \
                  if (event.target.tagName === 'IMG') { \
                    window.webkit.messageHandlers.imageHandler.postMessage(event.target.src); \
                  } \
              } \
          }, false); \
          \
      ";
    
    WKUserScript *userScript = [[WKUserScript alloc] initWithSource:scriptSource injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
    [configuration.userContentController addUserScript:userScript];
    [configuration.userContentController addScriptMessageHandler:self name:@"imageHandler"];
}

五、拦截url进行自定义处理

在H5中访问邮件,邮件附件无法预览,需要下载后分享到其它APP,比如文件管理器、微信

// 拦截URL请求
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if ([self.view isKindOfClass:UIViewController.class]) {
        UIViewController *vc = self.view;
        FZWKWebMailViewController *openVc = [[FZWKWebMailViewController alloc] init];
        openVc.url = navigationAction.request.URL;
        openVc.webview = webView;
        [vc.navigationController pushViewController:openVc animated:NO];
    }
    // 阻止 WKWebView 加载此 URL
    decisionHandler(WKNavigationActionPolicyCancel);
}

六、上传文件失败

在iOS13系统,下面的tabbar有6个及以上item时,在h5页面通过 <input type="file">标签上传文件时APP发生了崩溃
崩溃内容如下:

Thread 1: "Your application has presented a UIDocumentMenuViewController (<UIDocumentMenuViewController: 
0x121b65b90>\nsuperclass:\t\t\t\tUIViewController\ntitle:\t\t\t\t\t(null)\nview:\t\t\t\t\t<UIView: 0x107af4010; frame = (0 0; 414 
896); layer = <CALayer: 0x28236f780>>). In its current trait environment, the modalPresentationStyle of a 
UIDocumentMenuViewController with this style is UIModalPresentationPopover. You must provide location information for this popover 
through the view controller's popoverPresentationController. You must provide either a sourceView and sourceRect or a barButtonItem. 
 If this information is not known when you present the view controller, you may provide it in the 
 UIPopoverPresentationControllerDelegate method -prepareForPopoverPresentation."

iPhone默认最多5个tabbarItem,当超过5个时,在iOS13系统,选择文件的弹窗弹出时没有了瞄点,不知道在哪里弹出,就会崩溃。

解决思路
1、H5端通过系统版本号判断,当13及以下的系统,调用jsbridge,比如约定为chooseFile
2、App端实现自定义弹窗,进行图库、拍照、文件选择控制器的实现。
3、拿到图片或文件数据后,转为base64字符串传给H5端。

七、音频播放无法停止

H5页面播放音频后,如果不手动点击停止,退出当前或者关闭H5页面时,声音仍然在播放

解决思路一
H5的音频播放和原生封装的音频播放器播放的音频播放,都是调用的系统底层的音频服务
我们可以在页面关闭或者销毁时在原生将系统的音频服务停止掉来达到停止音频的目的。
代码如下:

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self interruptAudioSession];  // 打断音频
}

- (void)interruptAudioSession {
    AVAudioSession *session = [AVAudioSession sharedInstance];
    [session setCategory:AVAudioSessionCategorySoloAmbient error:nil];
    [session setActive:YES error:nil];
}

解决思路二
返回按钮在原生端,当点击返回按钮时会调用原生的



行者常至,为者常成!





R
Valine - A simple comment system based on Leancloud.