WKWebView實踐分享

ezbuy_Metal團隊發表於2019-01-28

自從公司的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進行寫入,更新或者刪除,結果更新CookieHTTPCookieStorage儲存容器中。 程式碼類似於:

    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,所以不能訪問WKWebViewCookie,現象就是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頁面的時候, 理所應當的就應該帶上語言資訊了.

這部分的內容, 因為雙十一臨近, 目前還沒有具體實施. 等功能上線以後, 再來補充.

相關文章