WKWebView 基础篇


WKWebView 是 iOS 8.0 以后用于替代 UIWebView 的浏览器组件。和 UIWebView 相比,具有渲染性能更好、支持更多的 HTML5 特性、控制更加细致等诸多优点。一直没沉下心来学习,我们一起好好看看里面都有什么吧🤓

WKWebView

主要的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@interface WKWebView : UIView

//重要属性
@property (nonatomic, readonly, copy) WKWebViewConfiguration *configuration;
@property (nullable, nonatomic, weak) id <WKNavigationDelegate> navigationDelegate;
@property (nullable, nonatomic, weak) id <WKUIDelegate> UIDelegate;
@property (nonatomic, readonly, strong) WKBackForwardList *backForwardList;

@property (nonatomic, readonly, nullable) SecTrustRef serverTrust;

@property (nullable, nonatomic, copy) NSString *customUserAgent;
@property (nonatomic) BOOL allowsLinkPreview;
@property (nonatomic, readonly, strong) UIScrollView *scrollView;
...

//加载方法
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
- (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL;
...

- (nullable WKNavigation *)goBack;
- (nullable WKNavigation *)goForward;

- (nullable WKNavigation *)reload;
- (nullable WKNavigation *)reloadFromOrigin;

//类方法
+ (BOOL)handlesURLScheme:(NSString *)urlScheme;

//与JS交互接口
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
//iOS 14新增的API
- (void)evaluateJavaScript:(NSString *)javaScriptString inFrame:(nullable WKFrameInfo *)frame inContentWorld:(WKContentWorld *)contentWorld completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
- (void)callAsyncJavaScript:(NSString *)functionBody arguments:(nullable NSDictionary<NSString *, id> *)arguments inFrame:(nullable WKFrameInfo *)frame inContentWorld:(WKContentWorld *)contentWorld completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;

@end

WKWebView 初始化

一个简单用于展示的 WebView 可以是这样的:

1
2
3
4
5
6
7
8
9
10
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
WKWebView *webView = ({
webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
webView.scrollView.bounces = NO;
webView.backgroundColor = [UIColor whiteColor];
webView;
});

[self.view addSubview:webView];
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://baidu.com"]]];

这样一个单纯用于展示网页、没有任何配置项的 WebView 就算完成了。相较于 UIWebView,我们看到多了一个 configuration 配置类型,那我们就来 Read the fucking source code……

WKWebViewConfiguration

官方文档是这样描述这个类型的:

A WKWebViewConfiguration object is a collection of properties with which to initialize a web view.

一个用于初始化 web view 属性的集合。

我们可以用它做什么呢?

  • 设置用于网站的初始cookie
  • 处理自定义的 URL schemes
  • 设置如何处理媒体内容
  • 管理网页中选中的信息
  • 自定义注入网页的脚本
  • 自定义内容的展示规则
  • ……

我们可以通过创建一个 WKWebViewConfiguration 对象来设置网页的属性,并且在 WebView 初始化的时候传递给它。注意的是只能在初始化的时候配置 configuration 中的属性,后面是没办法动态再去修改这些配置的。

我们通过这个类看看 WebKit 里面有哪些主要的内容:

WKProcessPool

1
2
3
4
5
6
7
/*! @abstract The process pool from which to obtain the view's web content
process.
@discussion When a web view is initialized, a new web content process
will be created for it from the specified pool, or an existing process in
that pool will be used.
*/
@property (nonatomic, strong) WKProcessPool *processPool;
1
2
@interface WKProcessPool : NSObject <NSSecureCoding>
@end

官方文档解释为一个可以在单个进程中运行多个 web 视图的不可见 token?令牌?。进程池。可以看到 WKProcessPool 类没有暴漏任何接口,这意味着我们只能创建和读取该对象,通过对象地址判断是否在相同进程。

WKWebView 为了安全和稳定性考虑,会为每一个 WKWebView 实例分配独立的进程(而不是直接使用APP的进程空间),系统会有一个设定的进程个数上线。相同 WKProcessPool 对象的 WKWebView 共享相同的进程空间。这也是与 UIWebView 最大不同的一点:NSHTTPCookieStorage 中的 cookie ,UIWebView 是可以自动携带使用的,但 WKWebView 无法获取 Storage中 的 cookie。

诶,那是不是放在同一个进程池中的 web view 就可以共享 cookie 了呢?带着这个问题,稍后我们会提到 cookie 有关的处理。

WKPreferences

1
2
3
/*! @abstract The preference settings to be used by the web view.
*/
@property (nonatomic, strong) WKPreferences *preferences;

针对 web 视图的偏好设置,如果是针对 web 内容的设置还是使用 WKWebViewConfiguration ,感觉这个类还在完善、扩充中,内容很少。比较值得注意的是与 JavaScript 有关的两个属性。

1
2
3
4
5
6
7
8
//字体
@property (nonatomic) CGFloat minimumFontSize;
//是否允许在没有用户交互的情况下,JavaScript可以打开windows
@property (nonatomic) BOOL javaScriptCanOpenWindowsAutomatically;
//是否启用javaScript,14.0 以后就废弃了,有对应替换的 API
@property (nonatomic) BOOL javaScriptEnabled;//ios(8.0, 14.0)
//是否提醒 如网络钓鱼或恶意软件 等可疑的欺诈内容
@property (nonatomic) BOOL fraudulentWebsiteWarningEnabled;//ios(13.0)

WKUserContentController

1
2
3
/*! @abstract The user content controller to associate with the web view.
*/
@property (nonatomic, strong) WKUserContentController *userContentController;

这个类提供了一个 JavaScript 向 web view 发送消息的途径,可以增删用户脚本。

JavaScript 与原生做交互,比较多的一个场景是需要调用原生的某些能力。在 UIWebView 中简单的方式是拦截请求,根据特定的 scheme 或者参数来区分,那在 WKWebView 中则是通过 WKUserContentController 添加消息处理器。例如打开相机功能:

  • 添加脚本处理器
1
2
3
4
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
[userContentController addScriptMessageHandler:self name:@"OpenCamera"];
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
configuration.userContentController = userContentController;
  • 实现 WKScriptMessageHandler 代理的方法
1
2
3
4
5
6
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name caseInsensitiveCompare:@"OpenCamera"] == NSOrderedSame) {
//Call your open camera action.
}
}
  • JS 中调用
1
window.webkit.messageHandlers.OpenCamera.postMessage(/*需要传的参数,两端约定好*/);

我们看到接收 JavaScript 的消息是通过一个 WKScriptMessage 类型,我们再来简单看下这个类

WKScriptMessage

A WKScriptMessage object contains information about a message sent from a webpage.

他的注释就很简单了:一个包含网页发来的消息内容的对象。

1
2
3
4
5
6
7
8
9
10
// 消息体、参数。允许的类型 NSNumber, NSString, NSDate, NSArray, NSDictionary, and NSNull.
@property (nonatomic, readonly, copy) id body;
// 发送消息的 web view。
@property (nullable, nonatomic, readonly, weak) WKWebView *webView;
// 前端中发送消息的 frame。
@property (nonatomic, readonly, copy) WKFrameInfo *frameInfo;
// 用于接收前端消息的处理器的名字。
@property (nonatomic, readonly, copy) NSString *name;
// The content world from which the message was sent. ?我也还没用过不清楚是干嘛的。
@property (nonatomic, readonly) WKContentWorld *world API_AVAILABLE(macos(11.0), ios(14.0));

WKUserScript

A script that the web view injects into a webpage.

当需要将自定义脚本代码注入 Web 页面时,可以创建一个 WKUserScript 对象。使用此对象指定要注入的 JavaScript 代码,以及与注入该代码的时间和方式相关的参数。通过前面提到的 WKUserContentController 调用 addUserScript: 完成注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface WKUserScript : NSObject <NSCopying>

//JS 代码
@property (nonatomic, readonly, copy) NSString *source;
//JS 注入的时机
@property (nonatomic, readonly) WKUserScriptInjectionTime injectionTime;
//是否只用于主视图
@property (nonatomic, readonly, getter=isForMainFrameOnly) BOOL forMainFrameOnly;

//初始化
- (instancetype)initWithSource:(NSString *)source injectionTime:(WKUserScriptInjectionTime)injectionTime forMainFrameOnly:(BOOL)forMainFrameOnly;
//iOS14 新增的一个方法,区分JS的“作用域”
- (instancetype)initWithSource:(NSString *)source injectionTime:(WKUserScriptInjectionTime)injectionTime forMainFrameOnly:(BOOL)forMainFrameOnly inContentWorld:(WKContentWorld *)contentWorld WK_API_AVAILABLE(macos(11.0), ios(14.0));

@end

WKContentWorld

An object that defines a scope of execution for JavaScript code, and which you use to prevent conflicts between different scripts.

WKContentWorld 是 iOS 14 的新增内容,可以理解为不同的命名空间、不同的运行环境。显而易见的,在逻辑上,原生的 JS 环境和 web JS 运行环境存在命名冲突的可能。WKContentWorld 有两个类属性 defaultClientWorldpageWorld,分别代表原生和 web 容器的 JS 运行空间。开发者也可以通过:

1
+ (WKContentWorld *)worldWithName:(NSString *)name;

工厂方法创建一个独立的 JS 运行环境。

WKWebsiteDataStore

这个类貌似包含了一个 web view 的所有数据,我看完这个类的介绍,第一感觉是,哇,我可以窥探一切了。然而,除了 cookie,一毛钱都拿不到……不讲了,有兴趣自己试吧~~

1
2
3
4
5
6
7
8
9
10
11
12
(
WKWebsiteDataTypeDiskCache,
WKWebsiteDataTypeOfflineWebApplicationCache,
WKWebsiteDataTypeMemoryCache,
WKWebsiteDataTypeLocalStorage,
WKWebsiteDataTypeFetchCache,
WKWebsiteDataTypeCookies,
WKWebsiteDataTypeSessionStorage,
WKWebsiteDataTypeIndexedDBDatabases,
WKWebsiteDataTypeWebSQLDatabases,
WKWebsiteDataTypeServiceWorkerRegistrations
)

WKHTTPCookieStore

A WKHTTPCookieStore object allows managing the HTTP cookies associated with a particular WKWebsiteDataStore.

用来管理与特定 WKWebsiteDataStore 相关联的 HTTP cookie。

API 看上去很简单…但获得 cookie 是异步操作,与 NSHTTPCookieStorage 的同步操作不同,处理起来可能要注意下。

1
2
3
4
5
- (void)getAllCookies:(void (^)(NSArray<NSHTTPCookie *> *))completionHandler;
- (void)setCookie:(NSHTTPCookie *)cookie completionHandler:(nullable void (^)(void))completionHandler;
- (void)deleteCookie:(NSHTTPCookie *)cookie completionHandler:(nullable void (^)(void))completionHandler;
- (void)addObserver:(id<WKHTTPCookieStoreObserver>)observer;
- (void)removeObserver:(id<WKHTTPCookieStoreObserver>)observer;

对应观察者的协议方法:

在 cookie 发生变化时,可以异步通知,但经测试是有一点延迟的,有兴趣可以测一测?

1
2
3
4
@protocol WKHTTPCookieStoreObserver <NSObject>
@optional
- (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore;
@end

WKBackForwardList

访问过的 web 页面历史记录。

WKNavigation

WKNavigation 对象可以用来了解网页的加载进度。通过 loadRequest、goBack 等方法加载页面时,将返回一个 WKNavigation 对象。通过 WKNavigationDelegate 代理的以下几个方法,可以知道页面的加载情况。WKNavigationDelegate 稍后我们再说。

WKNavigationAction

包含网页导航信息,需要据此显示对应的操作界面。

WKFrameInfo

标识当前网页内容信息的对象。


关于 WKWebView 的几篇文章:

WKWebView 基础篇
WKWebView 协议篇
WKWebView 实战篇
WKWebView Cookie 试错
WKWebView - WKScriptMessageHandler 循环引用

Demo

WebView 的 Demo