https://www.jianshu.com/p/403853b63537
雖然WKWebView是在Apple的WWDC 2014隨iOS 8和OS X 10.10出來的,是為了解決UIWebView加載速度慢、占用內存大的問題。但是由於之前還要適配iOS7,又不想做兩套加載頁面(主要是因為懶),所以就沒有使用。現在項目都適配iOS 8以上了,所以就開始使用WKWebView了,但是發現在使用的時候有好多坑,希望這篇文章能帶大家繞過坑,更好的使用WKWebView。
這篇文章主要介紹了以下問題,方便小伙伴們查閱:
WKWebView的基本介紹和使用
WKWebView和JavaScript的交互
解決WKWebView加載POST請求無法發送參數問題
WKWebView的基本介紹和使用
WKWebView的幾個代理方法
WKWebView是蘋果在iOS 8中引入的新組件,目的是給出一個新的高性能的WebView解決方案,擺脫過去 UIWebView的老、舊、笨重,特別是內存占用量巨大的問題,它使用Nitro JavaScript引擎,這意味着所有第三方瀏覽器運行JavaScript將會跟safari一樣快。
看到我這篇文章的小伙伴,對iOS的開發應該有一定的了解,肯定用過UIWebView,現在就用UIWebView和WKWebView的代理方法做一個對比。
加載狀態的回調(用來跟蹤頁面加載的過程(頁面開始加載、加載完成、加載失敗的方法),還可以決定是否跳轉):
准備加載頁面
UIWebViewDelegate: - webView:shouldStartLoadWithRequest:navigationType
WKNavigationDelegate: - webView:didStartProvisionalNavigation:
2. **內容開始加載**`(view的過渡動畫可在此方法中加載)`
UIWebViewDelegate: - webViewDidStartLoad:
WKNavigationDelegate: - webView:didCommitNavigation:
3. **頁面加載完成**`(view的過渡動畫的移除可在此方法中進行)`
UIWebViewDelegate: - webViewDidFinishLoad:
WKNavigationDelegate: - webView:didFinishNavigation:
4. **頁面加載失敗**
UIWebViewDelegate: - webView:didFailLoadWithError:
WKNavigationDelegate: - webView:didFailNavigation:withError:
WKNavigationDelegate: - webView:didFailProvisionalNavigation:withError:
此外,WKWebKit還有三個頁面跳轉的代理方法:
頁面跳轉的代理
接收到服務器跳轉請求的代理
WKNavigationDelegate: - webView:didReceiveServerRedirectForProvisionalNavigation:
2. **在收到響應后,決定是否跳轉的代理**
WKNavigationDelegate: - webView:decidePolicyForNavigationResponse:decisionHandler:
3. **在發送請求之前,決定是否跳轉的代理**
WKNavigationDelegate: - webView:decidePolicyForNavigationAction:decisionHandler:
WKWebView增加的屬性
WKWebViewConfiguration *configuration:初始化WKWebView的時候的配置,后面會用到
WKBackForwardList *backForwardList:相當於訪問歷史的一個列表
double estimatedProgress:進度,有這個之后就不用自己寫假的進度條了
WKWebView的使用
OC代碼:
// 創建WKWebView
WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
// 設置訪問的URL
NSURL *url = [NSURL URLWithString:@"http://www.jianshu.com"];
// 根據URL創建請求
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// WKWebView加載請求
[webView loadRequest:request];
// 將WKWebView添加到視圖
[self.view addSubview:webView];
Swift代碼:
// 創建WKWebView
let webView = WKWebView(frame: UIScreen.mainScreen().bounds)
// 設置訪問的URL
let url = NSURL(string: "http://www.jianshu.com")
// 根據URL創建請求
let requst = NSURLRequest(URL: url!)
// WKWebView加載請求
webView.loadRequest(requst)
// 將WKWebView添加到視圖
view.addSubview(webView)
可以看到很簡單,和UIWebView並沒有多少差別,然而性能就刷刷刷的提上去了,是不是很爽呢?如果你只是簡單的集成個Web頁到App,這些已經夠了。不過很多時候並沒有那么簡單,還需要處理各種東西,那么接着往后看。
WKWebView和JavaScript的交互
在WebKit框架中,有WKWebView可以替換UIKit的UIWebView和AppKit的WebView,而且提供了在兩個平台可以一致使用的接口。WebKit框架使得開發者可以在原生App中使用Nitro來提高網頁的性能和表現,Nitro就是Safari的JavaScript引擎,WKWebView不支持JavaScriptCore的方式但提供message handler的方式為JavaScript與Native通信。(這個引自天狐博客,更多的與UIWebView或者WKWebView的交互方法可以在這里看到。下面部分代碼(例如JS)也是竊取這個作者的,尊重原著,所以把原博客地址放這里,與JS交互寫的比我好多了。)
Native調用JavaScript方法
原生調用JavaScript的代碼需要在頁面加載完成之后,就是在 - webView:didFinishNavigation:代理方法里面
OC代碼:
[webView evaluateJavaScript:@"showAlert('奏是一個彈框')" completionHandler:^(id item, NSError * _Nullable error) {
// Block中處理是否通過了或者執行JS錯誤的代碼
}];
Swift代碼:
webView.evaluateJavaScript("showAlert('奏是一個彈框')") { (item, error) in
// 閉包中處理是否通過了或者執行JS錯誤的代碼
}
大家可以看到這段JS代碼是最簡單的彈出一個Alert的代碼,后面WKWebView加載POST請求參數問題中還會有一個加載POST請求的JS代碼,先不要管它了,請各位看官繼續往后翻,看看JavaScript怎么調用Native的方法。
JavaScript調用Native方法
JavaScript的配置
JavaScript調用Native的方法就需要前端和Native的小伙伴們配合了,需要前端的小伙伴在JS的方法中調用:
window.webkit.messageHandlers.NativeMethod.postMessage("就是一個消息啊");
這行代碼。請注意,這個NativeMethod是和App中要統一的,配置方法將在下面的Native中書寫。
Native App的代碼配置
下面該Native的代碼的配置了,細心的小伙伴可能已經發現了,創建WKWebView的時候,除了有- initWithFrame:方法外,還有一個高端的方法:- initWithFrame:configuration:方法。那句名言是誰說的來着:普通玩家選擇推薦配置,高端玩家選擇自定義配置,就當是我說的吧(那個拿鞋的把鞋穿上吧,我承認不是我說的😂)。這個方法就是用來自定義配置的,具體怎么自定義呢,童鞋們接着往下看吧。
OC代碼:
// 創建配置
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
// 創建UserContentController(提供JavaScript向webView發送消息的方法)
WKUserContentController* userContent = [[WKUserContentController alloc] init];
// 添加消息處理,注意:self指代的對象需要遵守WKScriptMessageHandler協議,結束時需要移除
[userContent addScriptMessageHandler:self name:@"NativeMethod"];
// 將UserConttentController設置到配置文件
config.userContentController = userContent;
// 高端的自定義配置創建WKWebView
WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds configuration:config];
// 設置訪問的URL
NSURL *url = [NSURL URLWithString:@"http://www.jianshu.com"];
// 根據URL創建請求
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// WKWebView加載請求
[webView loadRequest:request];
// 將WKWebView添加到視圖
[self.view addSubview:webView];
Swift代碼:
// 創建配置
let config = WKWebViewConfiguration()
// 創建UserContentController(提供JavaScript向webView發送消息的方法)
let userContent = WKUserContentController()
// 添加消息處理,注意:self指代的對象需要遵守WKScriptMessageHandler協議,結束時需要移除
userContent.addScriptMessageHandler(self, name: "NativeMethod")
// 將UserConttentController設置到配置文件
config.userContentController = userContent
// 高端的自定義配置創建WKWebView
let webView = WKWebView(frame: UIScreen.mainScreen().bounds, configuration: config)
// 設置訪問的URL
let url = NSURL(string: "http://www.jianshu.com")
// 根據URL創建請求
let requst = NSURLRequest(URL: url!)
// 設置代理
webView.navigationDelegate = self
// WKWebView加載請求
webView.loadRequest(requst)
// 將WebView添加到當前view
view.addSubview(webView)
可以看到,添加消息處理的handler的name,就是JavaScript中調用時候的NativeMethod,這兩個要保持一致。請把URL換成你自己的。
請注意第6行的代碼配置當前ViewController為MessageHandler,需要服從WKScriptMessageHandler協議,如果出現警告⚠️,請檢查是否服從了這個協議。
注意!注意!注意:上面將當前ViewController設置為MessageHandler之后需要在當前ViewController銷毀前將其移除,否則會造成內存泄漏。
移除的代碼如下:
OC代碼:
[webView.configuration.userContentController removeScriptMessageHandlerForName:@"NativeMethod"];
Swift代碼:
webView.configuration.userContentController.removeScriptMessageHandlerForName("NativeMethod")
請注意這個Name和上面創建WKWebView的配置中注冊的名字是一樣的,要保持對應。
好了,現在萬事俱備,只欠東風了。東風是什么呢,就是該在哪兒處理。可以看到WKScriptMessageHandler的協議里面只有一個方法,就是:
- userContentController:didReceiveScriptMessage:
相信聰明的你已經猜到了。是的,就是在這個代理方法里面操作:如果JavaScript執行已經寫好的:window.webkit.messageHandlers.NativeMethod.postMessage("就是一個消息啊");這行代碼,這個代理方法就會走,並且會有個WKScriptMessage的對象,這個WKScriptMessage對象有個name屬性,拿到之后你會發現,就是我們注冊的NativeMethod這個字符串,這時候你就可以手動調用Native的方法了。如果有多個方法需要調用的話怎么辦,看到JavaScript中postMessage()方法有一個參數了沒有,可以根據這里的參數來區分調用原生App的哪個方法。
代碼很簡單,就不寫了。什么?你說你還需要寫?好吧,那我還是貼出來吧:
OC代碼:
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
// 判斷是否是調用原生的
if ([@"NativeMethod" isEqualToString:message.name]) {
// 判斷message的內容,然后做相應的操作
if ([@"close" isEqualToString:message.body]) {
}
}
}
Swift代碼:
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
// 判斷是否是調用原生的
if "NativeMethod" == message.name {
// 判斷message的內容,然后做相應的操作
if "close" == message.body as! String {
}
}
}
上面的方法就可以獲取到JavaScript發送的Message了,JavaScript可以這樣調用:window.webkit.messageHandlers.NativeMethod.postMessage("close");,這時候上面的代理方法的兩個if判斷都能通過,不同的操作可增加里面的if語句的分支判斷message的內容來進行不同的Native代碼的調用,也就是JavaScript的postMessage方法的參數的不同來區分不同的操作。
好了,現在WKWebView和JavaScript的簡單交互你也會了。用WKWebView的時候貌似也還算開心。但是不要高興的太早,下面就要有坑了。
解決WKWebView加載POST請求無法發送參數問題
也許你用UIWebView加載過POST請求的頁面,感覺並沒有什么難點或者需要注意的地方,那真的是圖樣圖森破了,因為我也這樣天真過。直到我踩了很多坑之后,我才發現夢想與現實之間的差別,不過沒關系,我又要說另一句名言了:沒有挖不到的牆角...,咳咳咳,說錯了,請重新來BGM,跟我一起說:沒有解決不了的Bug,只有不努力的碼農!(各位架構師、高級開發工程師請手下留情,我說的碼農是我😂)
來來來,先來一發POST請求加載WebView。你會說,這還不easy?下面就來一個,走起:
OC代碼:
// 創建WKWebView
WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
// 設置訪問的URL
NSURL *url = [NSURL URLWithString:@"http://www.example.com"];
// 根據URL創建請求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 設置請求方法為POST
[request setHTTPMethod:@"POST"];
// WKWebView加載請求
[webView loadRequest:request];
// 將WKWebView添加到視圖
[self.view addSubview:webView];
Swift代碼:
// 創建WKWebView
let webView = WKWebView(frame: UIScreen.mainScreen().bounds)
// 設置訪問的URL
let url = NSURL(string: "http://www.example.com")
// 根據URL創建請求
let requst = NSMutableURLRequest(URL: url!)
// 設置請求方法為POST
requst.HTTPMethod = "POST"
// WKWebView加載請求
webView.loadRequest(requst)
// 將WKWebView添加到視圖
view.addSubview(webView)
這樣確實加載POST請求的網頁成功了(注意請把鏈接換成自己的),你一定露出了得意的笑容。但是騷年,不要高興的太早,這只是一個簡單的POST請求,還沒有添加參數呢。於是乎,你又說:那更簡單,在第9行插入如下代碼即可(比方說這個接口是登錄):
OC代碼:
// 設置請求參數
[request setHTTPBody:[@"username=aaa&password=123" dataUsingEncoding:NSUTF8StringEncoding]];
Swift代碼:
// 設置請求參數
requst.HTTPBody = "username=aaa&password=123".dataUsingEncoding(NSUTF8StringEncoding)
這種方法在UIWebView里面是沒有問題的,所以你認為在這里也應該是沒有問題的。從理論上講應該是這樣的,但是我要恭喜你了,這是WKWebView的Bug,讓你給碰到了。這里寫的POST請求沒有問題,但是就是不會把這兩個參數傳上去的,不信你可以試試(截止我發表這篇博客的日期,iOS 9.3並沒有修復此問題)。
好了,不廢話了(其實已經說了很多廢話了),下面看解決辦法(如果你需要適配iOS 8請直接使用方法2):
使用NSURLSession發送一個請求,然后把請求下來的數據當作本地HTML加載
使用JavaScript解決WKWebView無法發送POST參數問題
1. 使用NSURLSession解決WKWebView無法POST參數的問題(性能和結果都可能有問題,不推薦使用)
當發現POST無法傳遞參數的時候,我首先想到的是換個方法來,就是用一般的請求方式:NSURLSession發送請求,然后把接收到的數據轉化成字符串,然后再用WKWebView加載。大家可能已經看出來了,需要把整個網頁放到內存中或着放到本地然后再加載,所以肯定消耗內存呀。下面貼代碼吧:
OC代碼:
// 創建WKWebView
WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
// 將WKWebView添加到當前View
[self.view addSubview:webView];
// 設置訪問的URL
NSURL *url = [NSURL URLWithString:@"http://www.example.com"];
// 根據URL創建請求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 設置請求方法為POST
[request setHTTPMethod:@"POST"];
// 設置請求參數
[request setHTTPBody:[@"username=aaa&password=123" dataUsingEncoding:NSUTF8StringEncoding]];
// 實例化網絡會話
NSURLSession *session = [NSURLSession sharedSession];
// 創建請求Task
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// 將請求到的網頁數據用loadHTMLString 的方法加載
NSString *htmlStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[webView loadHTMLString:htmlStr baseURL:nil];
}];
// 開啟網絡任務
[task resume];
Swift代碼:
// 創建WKWebView
let webView = WKWebView(frame: UIScreen.mainScreen().bounds)
// 設置訪問的URL
let url = NSURL(string: "http://www.example.com")
// 根據URL創建請求
let requst = NSMutableURLRequest(URL: url!)
// 設置請求方法為POST
requst.HTTPMethod = "POST"
// 設置請求參數
requst.HTTPBody = "username=aaa&password=123".dataUsingEncoding(NSUTF8StringEncoding)
// 將WKWebView添加到視圖
view.addSubview(webView)
// 實例化網絡會話
let session = NSURLSession.sharedSession()
// 創建請求Task
let task = session.dataTaskWithRequest(requst) { (data, response, error) in
webView.loadHTMLString(String(data: data!, encoding: NSUTF8StringEncoding)!, baseURL: nil)
}
task.resume()
當你用iOS 9以上的設備的時候,貌似完全沒有一點問題,只是需要請求下來再放而已。但是注意前提條件:iOS 9,當你用iOS 8的時候,發現你的網頁的樣式和JavaScript事件全部沒有了。是不是有一種呵呵的沖動,那你就盡情呵呵吧。如果你要適配iOS 8,那么這個方法也不符合你的氣質。
其實這個東西和加載本地網頁無法加載CSS樣式和JS一樣,如果你也加載本地HTML文件出現問題,請查看Jay神的WKWebView使用遇到的坑。盡給別人打廣告了,呵呵,聲明一下啊:我跟這些人木有關系,只是為了方便大家查閱而已,誰讓我那么的大公無私呢😂。
好了,好了,來看一個更好的解決辦法吧:
2. 使用JavaScript解決WKWebView無法發送POST參數問題
開始之前我先說一下實現思路,方便大家理解,如果出錯了也能知道錯誤的地方:
將一個包含JavaScript的POST請求的HTML代碼放到工程目錄中
加載這個包含JavaScript的POST請求的代碼到WKWebView
加載完成之后,用Native調用JavaScript的POST方法並傳入參數來完成請求
創建包含JavaScript的POST請求的HTML代碼
相關代碼:
<html>
<head>
<script>
//調用格式: post('URL', {"key": "value"});
function post(path, params) {
var method = "post";
var form = document.createElement("form");
form.setAttribute("method", method);
form.setAttribute("action", path);
for(var key in params) {
if(params.hasOwnProperty(key)) {
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", key);
hiddenField.setAttribute("value", params[key]);
form.appendChild(hiddenField);
}
}
document.body.appendChild(form);
form.submit();
}
</script>
</head>
<body>
</body>
</html>
```
將這段代碼拷貝下來,然后粘貼到文本編輯器中,名字可以隨意起,比方說保存為:JSPOST.html,然后拷貝到工程目錄中,記得選擇對應的Target和勾選Copy items if needed(默認應該是勾選的)。這時候,就可以用這段JavaScript代碼來發送帶參數的POST請求了。
將對應的JavaScript代碼通過加載本地網頁的形式加載到WKWebView
OC代碼:
// JS發送POST的Flag,為真的時候會調用JS的POST方法(僅當第一次的時候加載本地JS)
self.needLoadJSPOST = YES;
// 創建WKWebView
self.webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
//設置代理
self.webView.navigationDelegate = self;
// 獲取JS所在的路徑
NSString *path = [[NSBundle mainBundle] pathForResource:@"JSPOST" ofType:@"html"];
// 獲得html內容
NSString *html = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
// 加載js
[self.webView loadHTMLString:html baseURL:[[NSBundle mainBundle] bundleURL]];
// 將WKWebView添加到當前View
[self.view addSubview:self.webView];
Swift代碼:
// JS發送POST的Flag,為真的時候會調用JS的POST方法(僅當第一次的時候加載本地JS)
needLoadJSPOST = true
// 創建WKWebView
webView = WKWebView(frame: UIScreen.mainScreen().bounds)
//設置代理
webView.navigationDelegate = self
// 獲取JS路徑
let path = NSBundle.mainBundle().pathForResource("JSPOST", ofType: "html")
// 獲得html內容
do {
let html = try String(contentsOfFile: path!, encoding: NSUTF8StringEncoding)
// 加載js
webView.loadHTMLString(html, baseURL: NSBundle.mainBundle().bundleURL)
} catch { }
// 將WKWebView添加到當前View
view.addSubview(webView)
這段代碼就相當於把工程中的JavaScript腳本加載到WKWebView中了,后面就是看怎么用了。(請注意換成您的文件名)
Native調用JavaScript腳本並傳入參數來完成POST請求
還記得 WKWebView和JavaScript的交互這一節嘛?現在該Native調用JavaScript了,如果忘記了,請往前翻溫故一下:- webView:didFinishNavigation:代理表明頁面已經加載完成,我們在這里操作,下面上代碼:
OC代碼:
// 加載完成的代理方法
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
// 判斷是否需要加載(僅在第一次加載)
if (self.needLoadJSPOST) {
// 調用使用JS發送POST請求的方法
[self postRequestWithJS];
// 將Flag置為NO(后面就不需要加載了)
self.needLoadJSPOST = NO;
}
}
// 調用JS發送POST請求
- (void)postRequestWithJS {
// 發送POST的參數
NSString *postData = @"\"username\":\"aaa\",\"password\":\"123\"";
// 請求的頁面地址
NSString *urlStr = @"http://www.postexample.com";
// 拼裝成調用JavaScript的字符串
NSString *jscript = [NSString stringWithFormat:@"post('%@', {%@});", urlStr, postData];
// NSLog(@"Javascript: %@", jscript);
// 調用JS代碼
[self.webView evaluateJavaScript:jscript completionHandler:^(id object, NSError * _Nullable error) {
}];
}
Swift代碼:
// 加載完成的代理方法
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
// 判斷是否需要加載(僅在第一次加載)
if needLoadJSPOST {
// 調用使用JS發送POST請求的方法
postRequestWithJS()
// 將Flag置為NO(后面就不需要加載了)
needLoadJSPOST = false
}
}
// 調用JS發送POST請求
func postRequestWithJS() {
// 發送POST的參數
let postData = "\"username\":\"aaa\",\"password\":\"123\""
// 請求的頁面地址
let urlStr = "http://www.postexample.com"
// 拼裝成調用JavaScript的字符串
let jscript = "post('\(urlStr)', {\(postData)});"
// 調用JS代碼
webView.evaluateJavaScript(jscript) { (object, error) in
}
}
好了,到目前為止你的請求就發出去了。相信后面的版本會解決這個問題,但是現在你要用的話也得有辦法,誰讓已經入了Apple的坑呢,誰讓UIWebView太不給力了呢.
寫在最后:
當時選擇WKWebView就是為了提高性能,但是沒有想到遇到這么多坑,從看iOS 9才解決了iOS 8無法加載本地樣式的問題,有時候蘋果解決問題的速度還有略慢的,到現在POST請求參數都發不出去也真是不應該。不過沒辦法,先解決了,說不定iOS 10 出來之后解決了呢。(我雖然有iOS 10的設備,但是我還沒有測試,感興趣的小伙伴們可以試試)。大家如果有什么問題,歡迎留言提問。謝謝支持!
作者:winann
鏈接:https://www.jianshu.com/p/403853b63537
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。