Overview
由于iOS13 将UIWebView废弃了, 所有使用UIWebView的APP都会直接被拒绝上架。(😌一个时代的终结啊,哈利路亚)。顺势而上的是我们的WKWebview,以内存占用低,刷新率高并支持多种手势而引来开发者的一众欢呼。下图展示了WKWebview的大致信息,下文中会逐一展开解释。
接入
使用WKWebview的大致流程如下:
- 创建一个WKWebview
- 完成基本设置,包括frame,configuration等
- 设置相关的代理
- 加载要加载的URL
- 在相应的代理回调中处理相关逻辑
初始化
//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使用
WKUserContentController
的addScriptMessageHandler: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)问题
关于这个问题,喵神已经在他的的博客中已经讲的很清楚了,我这里不再赘述。讲几个因为这个问题而引发的几种问题吧。
- 加载本地静态H5/访问不安全的站点(如http页、或直接以
IP : PORT
形式存在)而引起的白屏 - 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值:
- - getAllCookies: 获取所有存储的cookie值
- - setCookie:completionHandler: 设置一个cookie值
- - deleteCookie:completionHandler: 删除某个cookie Example
//在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);
}
参考链接
- QiShare 团队WKWebview总结
- Marcus Westin的WebViewJavascriptBridge库
- 喵神-关于iOS10中ATS的问题
- 拉维-WKWebview
- APPLE DEVELOP DOCUMENT - WKWebview
- APPLE 2017 WWDC:Customized Loading in WKWebView
- APPPLE DEVELOP DOCUMENT - WKHTTPCookieStore
- 南华Coder-WKWebView的Cookie问题小记
联系方式: 邮箱 zhijian.eric@gmail.com 欢迎交流骚扰
...