自從公司的ezbuy
App最低支持版本提高到iOS8
之後, 使用更多的iOS8
之後才特有的新特性就被提上了議程, 好比WebKit
. 做爲公司最沒有節操, 最沒有底線的程序員之一, 這項任務不可避免的就落到了個人身上.前端
既然要使用Webkit
, 那麼首先咱們就得明白爲何要使用它, 它相對於UIWebView
來講, 有什麼優點, 同時, 還得知道它的劣勢,以及這些劣勢是否會對公司現有業務形成影響.程序員
首先咱們來講說它的優點:web
iOS9
以上)再來講說它的劣勢:swift
說完了優點劣勢, 那下面就來講說它的基本用法.api
加載網頁的方法和UIWebView
相同, 代碼以下:緩存
let webView = WKWebView(frame: self.view.bounds,configuration: config)
webView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
view.addSubview(webView)
複製代碼
WKNavigationDelegate
服務器
用來追蹤加載過程(頁面開始加載、加載完成、加載失敗)的方法:cookie
// 頁面開始加載時調用
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
dom
// 建立一個新的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
裏面相應的代理方法挪過來而已.
因爲咱們的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)
}
複製代碼
上面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頁面的時候, 理所應當的就應該帶上語言信息了.
這部分的內容, 由於雙十一臨近, 目前尚未具體實施. 等功能上線之後, 再來補充.