自從公司的ezbuy
App最低支援版本提升到iOS8
以後, 使用更多的iOS8
以後才特有的新特性就被提上了議程, 比如WebKit
.
作為公司最沒有節操, 最沒有底線的程式設計師之一, 這項任務不可避免的就落到了我的身上.
既然要使用Webkit
, 那麼首先我們就得明白為什麼要使用它, 它相對於UIWebView
來說, 有什麼優勢, 同時, 還得知道它的劣勢,以及這些劣勢是否會對公司現有業務造成影響.
首先我們來說說它的優勢:
- 效能更高 高達60fps的滾動重新整理以及內建手勢
- 記憶體佔用更低 記憶體佔用只有UIWebView的1/4左右
- 允許JavaScript的Nitro庫的載入並使用
- 支援更多的HtML5特性
- 原生支援載入進度
- 支援自定義UserAgent(
iOS9
以上)
再來說說它的劣勢:
- 不支援快取
- 不能攔截修改Request
說完了優勢劣勢, 那下面就來說說它的基本用法.
一、載入網頁
載入網頁的方法和UIWebView
相同, 程式碼如下:
let webView = WKWebView(frame: self.view.bounds,configuration: config)
webView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
view.addSubview(webView)
複製程式碼
二、WKWebView的代理方法
WKNavigationDelegate
用來追蹤載入過程(頁面開始載入、載入完成、載入失敗)的方法:
// 頁面開始載入時呼叫
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!)
// 當內容開始返回時呼叫
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!)
// 頁面載入完成之後呼叫
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)
// 頁面載入失敗時呼叫
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error)
複製程式碼
用來跳轉頁面的方法:
// 接收到伺服器跳轉請求之後呼叫
func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!)
// 在收到響應後,決定是否跳轉
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void)
// 在傳送請求之前,決定是否跳轉
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)
複製程式碼
WKUIDelegate
// 建立一個新的webView
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView?
// webView中的確認彈窗
func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void)
// webView中的輸入框
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void)
// webView中的警告彈窗
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void)
//TODO: iOS10中新新增的幾個代理方法待補充
複製程式碼
WKScriptMessageHandler
這個協議包含一個必須實現的方法, 它可以直接將接收到的JS指令碼轉為Swift或者OC物件.
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)
複製程式碼
這部分其實除了iOS10新加的幾個代理方法, 其他的並沒有什麼特別的. 只不過把原本UIWebView
裡面相應的代理方法挪過來而已.
三、WKWebView的Cookie
由於我們的APP內使用了大量的商品列表/活動等H5頁面, H5需要知道是哪一個使用者在訪問這個頁面, 那麼用Cookie
是最好也是最合適的解決方案了, 在UIWebView的時候, 我們並沒有使用Cookie
的困擾, 我們只需要寫一個方法, 往HTTPCookieStorage
裡面注入一個我們使用者的HTTPCookie
就可以了.同一個應用,不同UIWebView
之間的Cookie
是自動同步的。並且可以被其他網路類訪問比如NSURLConnection
,AFNetworking
。
它們都是儲存在HTTPCookieStorage
容器中。 當UIWebView
載入一個URL的時候,在載入完成時候,Http Response,對Cookie
進行寫入,更新或者刪除,結果更新Cookie
到HTTPCookieStorage
儲存容器中。
程式碼類似於:
public class func updateCurrentCookieIfNeeded() {
let cookieForWeb: HTTPCookie?
if let customer = CustomerUser.current {
var cookie1Props: [HTTPCookiePropertyKey: Any] = [:]
cookie1Props[HTTPCookiePropertyKey.domain] = customer.area?.webURLSource.webCookieHost
cookie1Props[HTTPCookiePropertyKey.path] = "/"
cookie1Props[HTTPCookiePropertyKey.name] = CustomerUser.CookieName
cookie1Props[HTTPCookiePropertyKey.value] = customer.cookie
cookieForWeb = HTTPCookie(properties: cookie1Props)
} else {
cookieForWeb = nil
}
let storage = HTTPCookieStorage.shared
if let cookie = cookieForWeb, let cookie65 = cookieFor65daigou(customer: CustomerUser.current) {
storage.setCookie(cookie)
storage.setCookie(cookie65)
} else {
guard let cookies = storage.cookies else { return }
let needDeleteCookies = cookies.filter { $0.name == CustomerUser.CookieName }
needDeleteCookies.forEach({ (cookie) in
storage.deleteCookie(cookie)
})
}
}
複製程式碼
但是在我遷移到WKWebView
的時候, 我發現這一招不管用了, WKWebView
例項不會把Cookie
存入到App標準的的Cookie
容器(HTTPCookieStorage
)中, WKWebView
擁有自己的私有儲存.
因為 NSURLSession
/NSURLConnection
等網路請求使用HTTPCookieStorage
進行訪問Cookie
,所以不能訪問WKWebView
的Cookie
,現象就是WKWebView
存了Cookie
,其他的網路類如NSURLSession
/NSURLConnection
卻看不到. 同時WKWebView
也不會讀取儲存在HTTPCookieStorage
中的Cookie
.
為了解決這一問題, 我查了大量的資料, 最後發現通過JS的方式注入Cookie
是對於我們目前的程式碼來說是最合適也是最方便的. 因為我們已經有了注入到HTTPCookieStorage
的程式碼, 那麼只需要把這些Cookie
轉化成JS並且注入到WKWebView
裡面就可以了.
fileprivate class func getJSCookiesString(_ cookies: [HTTPCookie]) -> String {
var result = ""
let dateFormatter = DateFormatter()
dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
dateFormatter.dateFormat = "EEE, d MMM yyyy HH:mm:ss zzz"
for cookie in cookies {
result += "document.cookie='\(cookie.name)=\(cookie.value); domain=\(cookie.domain); path=\(cookie.path); "
if let date = cookie.expiresDate {
result += "expires=\(dateFormatter.string(from: date)); "
}
if (cookie.isSecure) {
result += "secure; "
}
result += "'; "
}
return result
}
複製程式碼
注入的方法就是在每次initWkWebView
的時候, 使用下面的config
就可以了:
public class func wkWebViewConfig() -> WKWebViewConfiguration {
updateCurrentCookieIfNeeded()
let userContentController = WKUserContentController()
if let cookies = HTTPCookieStorage.shared.cookies {
let script = getJSCookiesString(cookies)
let cookieScript = WKUserScript(source: script, injectionTime: WKUserScriptInjectionTime.atDocumentStart, forMainFrameOnly: false)
userContentController.addUserScript(cookieScript)
}
let webViewConfig = WKWebViewConfiguration()
webViewConfig.userContentController = userContentController
return webViewConfig
}
public class func getJSCookiesString() -> String? {
guard let cookies = HTTPCookieStorage.shared.cookies else {
return nil
}
return getJSCookiesString(cookies)
}
複製程式碼
四、關於User-Agent
上面Cookie
的問題解決了, 我們們的前端又提出了新的問題, 他們需要知道使用者訪問了網頁是使用了客戶端(iOS/Android)來的.
這個就好解決了, 其實和WKWebVIew
的關係不大. 最合適新增的地方就是在User-Agent
裡面, 不過並沒有使用WKWebView自己的User-Agent
去定義, 因為這個欄位只支援iOS9
以上, 所以用下面的程式碼全域性新增就可以.
fileprivate func setUserAgent(_ webView: WKWebView) {
let userAgentHasPrefix = "xxxxxx "
webView.evaluateJavaScript("navigator.userAgent", completionHandler: { (result, error) in
guard let agent = result as? String , agent.hasPrefix(userAgentHasPrefix) == false else { return }
let newAgent = userAgentHasPrefix + agent
UserDefaults.standard.register(defaults: ["UserAgent":newAgent])
})
}
複製程式碼
五、關於國際化
解決了上面的問題, 我們們產品經理又提出了國際化的需求, 因為我們的APP同時為至少5個國家的客戶提供, 國際化的方案也是我做的, APP內部可以熱切換語言, 也許在下一篇博文中會介紹我們專案中的國際化方案.
那麼請求H5頁面的時候, 理所應當的就應該帶上語言資訊了.
這部分的內容, 因為雙十一臨近, 目前還沒有具體實施. 等功能上線以後, 再來補充.