WKWebView實踐分享

自從公司的ezbuy App最低支持版本提高到iOS8之後, 使用更多的iOS8之後才特有的新特性就被提上了議程, 好比WebKit. 做爲公司最沒有節操, 最沒有底線的程序員之一, 這項任務不可避免的就落到了個人身上.前端

既然要使用Webkit, 那麼首先咱們就得明白爲何要使用它, 它相對於UIWebView來講, 有什麼優點, 同時, 還得知道它的劣勢,以及這些劣勢是否會對公司現有業務形成影響.程序員

首先咱們來講說它的優點:web

  • 性能更高 高達60fps的滾動刷新以及內置手勢
  • 內存佔用更低 內存佔用只有UIWebView的1/4左右
  • 容許JavaScript的Nitro庫的加載並使用
  • 支持更多的HtML5特性
  • 原生支持加載進度
  • 支持自定義UserAgent(iOS9以上)

再來講說它的劣勢:swift

  • 不支持緩存
  • 不能攔截修改Request

說完了優點劣勢, 那下面就來講說它的基本用法.api

1、加載網頁

加載網頁的方法和UIWebView相同, 代碼以下:緩存

let webView = WKWebView(frame: self.view.bounds,configuration: config)
webView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
view.addSubview(webView)
複製代碼
2、WKWebView的代理方法

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)
複製代碼

WKUIDelegatedom

// 建立一個新的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裏面相應的代理方法挪過來而已.

3、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)
    }
複製代碼
4、關於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])
        })
    }
複製代碼
5、關於國際化

解決了上面的問題, 我們產品經理又提出了國際化的需求, 由於咱們的APP同時爲至少5個國家的客戶提供, 國際化的方案也是我作的, APP內部能夠熱切換語言, 也許在下一篇博文中會介紹咱們項目中的國際化方案. 那麼請求H5頁面的時候, 理所應當的就應該帶上語言信息了.

這部分的內容, 由於雙十一臨近, 目前尚未具體實施. 等功能上線之後, 再來補充.

相關文章
相關標籤/搜索