显著提高iOS应用中Web页面的加载速度 - 提前下载页面的关键资源(如JavaScript、CSS和图像)

手动下载并缓存资源是一种有效的方式,可以确保在需要时资源已经在本地存储,这样可以显著提高加载速度。

缓存整个 web 页面的所有资源文件

具体实现步骤

  1. 下载和缓存资源:包括 HTML 文件、CSS、JavaScript 和图像。
  2. 在应用启动时预加载资源
  3. 构建包含所有预加载资源的 HTML 字符串
  4. 加载构建的 HTML 字符串到 WKWebView

资源下载和缓存管理类

import Foundation

class ResourceDownloader {
    static let shared = ResourceDownloader()
    private init() {}
    
    func downloadResources() {
        let resources = [
            URL(string: "https://www.example.com/styles.css")!,
            URL(string: "https://www.example.com/script.js")!,
            URL(string: "https://www.example.com/image.png")!,
            URL(string: "https://www.example.com/index.html")!
        ]
        
        for resource in resources {
            downloadResource(from: resource)
        }
    }
    
    private func downloadResource(from url: URL) {
        let task = URLSession.shared.downloadTask(with: url) { localURL, response, error in
            guard let localURL = localURL else { return }
            
            do {
                let data = try Data(contentsOf: localURL)
                self.cacheResource(data: data, url: url)
            } catch {
                print("Failed to load resource: \(error)")
            }
        }
        task.resume()
    }
    
    private func cacheResource(data: Data, url: URL) {
        let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
        let fileURL = cacheDirectory.appendingPathComponent(url.lastPathComponent)
        
        do {
            try data.write(to: fileURL)
            print("Resource cached: \(fileURL)")
        } catch {
            print("Failed to cache resource: \(error)")
        }
    }
    
    func getCachedResource(for url: URL) -> Data? {
        let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
        let fileURL = cacheDirectory.appendingPathComponent(url.lastPathComponent)
        
        return try? Data(contentsOf: fileURL)
    }
}

在应用启动时预加载资源

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // 预加载资源
        ResourceDownloader.shared.downloadResources()
        return true
    }
}

使用缓存的资源加载完整页面

import UIKit
import WebKit

class ViewController: UIViewController {
    
    var webView: WKWebView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        webView = WKWebView(frame: self.view.bounds)
        self.view.addSubview(webView)
        
        // 构建完整的HTML内容
        if let htmlURL = URL(string: "https://www.example.com/index.html"),
           let cachedHTML = ResourceDownloader.shared.getCachedResource(for: htmlURL),
           let htmlString = String(data: cachedHTML, encoding: .utf8) {
            
            let completeHTMLString = embedCachedResources(in: htmlString)
            
            // 加载HTML内容到webView
            webView.loadHTMLString(completeHTMLString, baseURL: nil)
        } else {
            // 如果没有缓存,则加载远程 URL
            let request = URLRequest(url: URL(string: "https://www.example.com")!)
            webView.load(request)
        }
    }
    
    private func embedCachedResources(in htmlString: String) -> String {
        var modifiedHTMLString = htmlString
        
        // 嵌入预加载的CSS
        if let cssURL = URL(string: "https://www.example.com/styles.css"),
           let cachedCSS = ResourceDownloader.shared.getCachedResource(for: cssURL),
           let cssString = String(data: cachedCSS, encoding: .utf8) {
            let cssTag = "<style>\(cssString)</style>"
            modifiedHTMLString = modifiedHTMLString.replacingOccurrences(of: "<link rel=\"stylesheet\" href=\"styles.css\">", with: cssTag)
        }
        
        // 嵌入预加载的JavaScript
        if let jsURL = URL(string: "https://www.example.com/script.js"),
           let cachedJS = ResourceDownloader.shared.getCachedResource(for: jsURL),
           let jsString = String(data: cachedJS, encoding: .utf8) {
            let jsTag = "<script>\(jsString)</script>"
            modifiedHTMLString = modifiedHTMLString.replacingOccurrences(of: "<script src=\"script.js\" defer></script>", with: jsTag)
        }
        
        // 嵌入预加载的图像
        if let imageURL = URL(string: "https://www.example.com/image.png"),
           let cachedImage = ResourceDownloader.shared.getCachedResource(for: imageURL) {
            let base64Image = cachedImage.base64EncodedString()
            let imgTag = "<img src='data:image/png;base64,\(base64Image)' alt='Preloaded Image'>"
            modifiedHTMLString = modifiedHTMLString.replacingOccurrences(of: "<img src=\"image.png\" alt=\"Preloaded Image\">", with: imgTag)
        }
        
        return modifiedHTMLString
    }
}

详细说明

  1. 资源下载和缓存

    • 使用 URLSession 下载资源(包括HTML文件、CSS、JavaScript和图像),并将其缓存到本地存储。
    • 下载完成后,资源数据被写入本地存储,以便后续使用。
  2. 预加载资源

    • 在应用启动时调用 downloadResources() 方法,预先下载和缓存所需的资源。
  3. 使用缓存的资源加载完整页面

    • viewDidLoad() 方法中,通过 getCachedResource(for:) 获取缓存的HTML内容。
    • 使用 embedCachedResources(in:) 方法,将预加载的 CSS、JavaScript 和图像嵌入到 HTML 内容中。
    • 使用 webView.loadHTMLString(completeHTMLString, baseURL: nil) 加载修改后的 HTML 内容。

通过这种方式,可以确保在加载页面时直接使用本地缓存的资源,从而显著提高页面加载速度,提供更好的用户体验。


只缓存 CSS、JavaScript 和图像

只有 CSS、JavaScript 和图像是预加载的,而 HTML 文件是在打开 WKWebView 时才开始下载的。这种策略在某些情况下可能更加高效,尤其是当 HTML 文件需要动态生成或者频繁更新时。

为什么预加载 CSS、JavaScript 和图像?

预加载 CSS、JavaScript 和图像有几个优点:

  • 减少首次渲染时间:通过提前加载关键资源,可以"显著减少"页面首次渲染的时间,提高用户体验。
  • 减轻服务器压力:本地缓存的资源可以减少重复请求,减轻服务器的负载。
  • 提高离线体验:在某些情况下,即使没有网络连接,用户也能看到页面的一部分内容。

什么时候 HTML 文件在打开 WKWebView 时下载?

  • 动态内容:如果 HTML 文件内容是动态生成的(例如,包含个性化数据或实时更新的数据),那么在每次加载页面时请求最新的 HTML 文件是必要的。
  • 频繁更新:如果 HTML 文件内容频繁更新,预加载 HTML 文件可能会导致内容不一致。
  • 用户特定数据:某些应用需要根据用户的身份或状态生成不同的 HTML 内容。

完整的 Swift 代码实现

展示如何预加载 CSS、JavaScript 和图像,并在打开 WKWebView 时下载 HTML 文件。

  • web 容器配置
import UIKit
import WebKit

class ViewController: UIViewController {
    
    var webView: WKWebView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 创建并配置 WKWebView
        let configuration = WKWebViewConfiguration()
        let urlSchemeHandler = LocalResourceHandler()
        // `local://some/path`的 scheme 为 `local`,同理,`http://sqi.io` 的 scheme 为 `http`
        configuration.setURLSchemeHandler(urlSchemeHandler, forURLScheme: "local")
        
        webView = WKWebView(frame: self.view.bounds, configuration: configuration)
        self.view.addSubview(webView)
        
        // 加载远程 HTML 文件
        if let htmlURL = URL(string: "https://www.example.com/index.html") {
            let request = URLRequest(url: htmlURL)
            webView.load(request)
        }
    }
}

  • 自定义处理特定 URL

WKURLSchemeHandler 是 WebKit 框架中的一个协议,允许在 WKWebView 中自定义处理特定 URL 方案。通过实现该协议,开发者可以拦截并管理这些 URL 请求,比如提供本地文件、修改请求或实现自定义缓存策略。这对于提高性能、处理自定义协议或在 iOS 应用中创建安全通信通道非常有用。

具体实现涉及两个主要方法:

  1. webView:startURLSchemeTask: - 启动处理特定 URL 请求。
  2. webView:stopURLSchemeTask: - 停止处理特定 URL 请求。

更多详细信息可以参考官方文档

// 自定义 URL 方案处理器
class LocalResourceHandler: NSObject, WKURLSchemeHandler {
    
    func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
        guard let url = urlSchemeTask.request.url else {
            return
        }
        
        // 仅处理特定的 URL 方案,例如 "local"
        guard url.scheme == "https" else {
            // 对于非 "local" 方案的请求,不处理
            return
        }
        
        if let data = ResourceDownloader.shared.getCachedResource(for: url) {
        	// 根据 url 确定响应的具体 MIME 类型
            let mimeType = determineMimeType(for: url)
            // 创建一个 URL 响应对象
            let response = URLResponse(url: url, mimeType: mimeType, expectedContentLength: data.count, textEncodingName: nil)
            urlSchemeTask.didReceive(response)
            urlSchemeTask.didReceive(data)
            urlSchemeTask.didFinish()
        } else {
            // 缓存资源不可用,发起网络请求
            downloadResource(from: url, urlSchemeTask: urlSchemeTask)
        }
    }
    
    func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
    	// Clean up if needed
    }
    
    private func determineMimeType(for url: URL) -> String {
        switch url.pathExtension {
        case "css":
            return "text/css"
        case "js":
            return "application/javascript"
        case "png":
            return "image/png"
        default:
            return "text/plain"
        }
    }
    
    private func downloadResource(from url: URL, urlSchemeTask: WKURLSchemeTask) {
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                urlSchemeTask.didFailWithError(error)
                return
            }
            
            guard let data = data, let response = response else {
                urlSchemeTask.didFailWithError(NSError(domain: "LocalResourceHandler", code: 404, userInfo: nil))
                return
            }
            
            // 缓存资源
            ResourceDownloader.shared.cacheResource(data: data, url: url)
            
            // 返回资源
            urlSchemeTask.didReceive(response)
            urlSchemeTask.didReceive(data)
            urlSchemeTask.didFinish()
        }
        task.resume()
    }
}

  • 下载和缓存
class ResourceDownloader {
    static let shared = ResourceDownloader()
    private init() {}
    
    func downloadResources() {
        let resources = [
            URL(string: "https://www.example.com/styles.css")!,
            URL(string: "https://www.example.com/script.js")!,
            URL(string: "https://www.example.com/image.png")!
        ]
        
        for resource in resources {
            downloadResource(from: resource)
        }
    }
    
    private func downloadResource(from url: URL) {
        let task = URLSession.shared.downloadTask(with: url) { localURL, response, error in
            guard let localURL = localURL else { return }
            
            do {
                let data = try Data(contentsOf: localURL)
                self.cacheResource(data: data, url: url)
            } catch {
                print("Failed to load resource: \(error)")
            }
        }
        task.resume()
    }
    
    func cacheResource(data: Data, url: URL) {
        let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
        let fileURL = cacheDirectory.appendingPathComponent(url.lastPathComponent)
        
        do {
            try data.write(to: fileURL)
            print("Resource cached: \(fileURL)")
        } catch {
            print("Failed to cache resource: \(error)")
        }
    }
    
    func getCachedResource(for url: URL) -> Data? {
        let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
        let fileURL = cacheDirectory.appendingPathComponent(url.lastPathComponent)
        
        return try? Data(contentsOf: fileURL)
    }
}

url.lastPathComponentURL 类的一个属性,用来获取URL路径的最后一个部分。举个例子,如果URL是 http://example.com/path/to/resource, 那么 url.lastPathComponent 将返回 resource。这个属性通常用于提取URL中的文件名或特定路径部分。

示例代码:

let url = URL(string: "http://example.com/path/to/resource")!
let lastPathComponent = url.lastPathComponent
print(lastPathComponent) // 输出: resource

这个属性对于处理URL路径中的具体部分非常有用,尤其是在解析文件路径或处理特定资源时。


在应用启动时预加载资源
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // 预加载资源
        ResourceDownloader.shared.downloadResources()
        return true
    }
}

注意点 - 自定义 URL scheme 的真实目的

确保在加载页面时使用 local:// 这样的自定义 URL scheme 来引用本地缓存的文件

这意味着在你的 HTML 文件中引用 CSS、JS 和图片资源时,需要使用自定义的 local URL scheme,这样 WKWebView 会使用本地缓存的文件,而不是从网络上重新下载资源。具体来说,就是将 HTML 文件中的资源路径改为使用 local:// 开头的路径。

以下是详细的步骤和示例:

1. 修改 HTML 文件

假设你的 HTML 文件中有如下资源引用:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="https://example.com/style.css">
    <script src="https://example.com/script.js"></script>
</head>
<body>
    <img src="https://example.com/image.png" alt="example image">
</body>
</html>

你需要将资源路径修改为使用自定义的 local URL scheme:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="local://style.css">
    <script src="local://script.js"></script>
</head>
<body>
    <img src="local://image.png" alt="example image">
</body>
</html>

2. 加载 HTML 文件到 WKWebView

当你将 HTML 文件加载到 WKWebView 时,它会使用你设置的 WKURLSchemeHandler 来处理 local:// 开头的请求,并从本地缓存中读取文件。

import UIKit
import WebKit

class ViewController: UIViewController {
    var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let webConfiguration = WKWebViewConfiguration()
        webConfiguration.setURLSchemeHandler(LocalFileSchemeHandler(), forURLScheme: "local")

        webView = WKWebView(frame: self.view.frame, configuration: webConfiguration)
        self.view.addSubview(webView)

        if let htmlPath = Bundle.main.path(forResource: "index", ofType: "html"),
           let htmlContent = try? String(contentsOfFile: htmlPath, encoding: .utf8) {
            webView.loadHTMLString(htmlContent, baseURL: nil)
        }
    }
}

3. LocalFileSchemeHandler 实现

你已经实现了 LocalFileSchemeHandler,这个 handler 会拦截 local:// 开头的请求,并从本地缓存中读取相应的文件:

import WebKit

class LocalFileSchemeHandler: NSObject, WKURLSchemeHandler {
    func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
        guard let url = urlSchemeTask.request.url else {
            return
        }

        let fileManager = FileManager.default
        let documentsURL = try! fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
        let localFileURL = documentsURL.appendingPathComponent(url.lastPathComponent)

        if fileManager.fileExists(atPath: localFileURL.path) {
            if let data = try? Data(contentsOf: localFileURL), let mimeType = mimeTypeForPath(path: localFileURL.path) {
                let response = URLResponse(url: url, mimeType: mimeType, expectedContentLength: data.count, textEncodingName: nil)
                urlSchemeTask.didReceive(response)
                urlSchemeTask.didReceive(data)
                urlSchemeTask.didFinish()
            } else {
                urlSchemeTask.didFailWithError(NSError(domain: "LocalFileSchemeHandler", code: 404, userInfo: nil))
            }
        } else {
            urlSchemeTask.didFailWithError(NSError(domain: "LocalFileSchemeHandler", code: 404, userInfo: nil))
        }
    }

    func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {}

    private func mimeTypeForPath(path: String) -> String? {
        let pathExtension = (path as NSString).pathExtension
        if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue() {
            if let mimeType = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
                return mimeType as String
            }
        }
        return nil
    }
}

这样,当你在 HTML 中使用 local:// 作为资源路径时,WKWebView 会使用本地缓存的文件进行渲染,从而提高加载速度。


为什么选择通过 WKURLSchemeHandler 来拦截请求并返回本地缓存的文件,而不是通过 WKNavigationDelegate 拦截请求,也不是通过 URLProtocol 拦截请求 ?

使用 WKNavigationDelegate 实现拦截请求并返回本地缓存文件

使用 WKNavigationDelegate 拦截请求并返回本地缓存文件,可以在 webView(_:decidePolicyFor:decisionHandler:) 方法中处理请求。

import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {
    var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let webConfiguration = WKWebViewConfiguration()
        webView = WKWebView(frame: self.view.frame, configuration: webConfiguration)
        webView.navigationDelegate = self
        self.view.addSubview(webView)

        if let htmlPath = Bundle.main.path(forResource: "index", ofType: "html"),
           let htmlContent = try? String(contentsOfFile: htmlPath, encoding: .utf8) {
            webView.loadHTMLString(htmlContent, baseURL: nil)
        }
    }

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        if let url = navigationAction.request.url, url.scheme == "local" {
            if let localFileURL = getLocalFileURL(for: url) {
                webView.loadFileURL(localFileURL, allowingReadAccessTo: localFileURL.deletingLastPathComponent())
                decisionHandler(.cancel)
                return
            }
        }
        decisionHandler(.allow)
    }

    private func getLocalFileURL(for url: URL) -> URL? {
        let fileManager = FileManager.default
        let documentsURL = try? fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
        return documentsURL?.appendingPathComponent(url.lastPathComponent)
    }
}

使用 URLProtocol 实现拦截请求并返回本地缓存文件

使用 URLProtocol 拦截请求并返回本地缓存文件,需要自定义一个 URLProtocol 子类,并注册它来处理特定的 URL scheme。

1. 定义自定义 URLProtocol 子类
import Foundation
import MobileCoreServices

class LocalFileURLProtocol: URLProtocol {
    override class func canInit(with request: URLRequest) -> Bool {
        return request.url?.scheme == "local"
    }

    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    override func startLoading() {
        guard let url = request.url else {
            return
        }

        if let localFileURL = getLocalFileURL(for: url) {
            do {
                let data = try Data(contentsOf: localFileURL)
                let mimeType = mimeTypeForPath(path: localFileURL.path) ?? "application/octet-stream"
                let response = URLResponse(url: url, mimeType: mimeType, expectedContentLength: data.count, textEncodingName: nil)
                client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
                client?.urlProtocol(self, didLoad: data)
                client?.urlProtocolDidFinishLoading(self)
            } catch {
                client?.urlProtocol(self, didFailWithError: error)
            }
        }
    }

    override func stopLoading() {}

    private func getLocalFileURL(for url: URL) -> URL? {
        let fileManager = FileManager.default
        let documentsURL = try? fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
        return documentsURL?.appendingPathComponent(url.lastPathComponent)
    }

    private func mimeTypeForPath(path: String) -> String? {
        let pathExtension = (path as NSString).pathExtension
        if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue() {
            if let mimeType = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
                return mimeType as String
            }
        }
        return nil
    }
}
2. 注册 URLProtocol

AppDelegate 或适当的位置注册 URLProtocol

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        URLProtocol.registerClass(LocalFileURLProtocol.self)
        return true
    }
}
3. 加载 HTML 文件

与前面的方法类似,加载 HTML 文件到 WKWebView

import UIKit
import WebKit

class ViewController: UIViewController {
    var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let webConfiguration = WKWebViewConfiguration()
        webView = WKWebView(frame: self.view.frame, configuration: webConfiguration)
        self.view.addSubview(webView)

        if let htmlPath = Bundle.main.path(forResource: "index", ofType: "html"),
           let htmlContent = try? String(contentsOfFile: htmlPath, encoding: .utf8) {
            webView.loadHTMLString(htmlContent, baseURL: nil)
        }
    }
}

比较和总结

  • WKNavigationDelegate:

    • 优点: 实现简单,可以快速集成。
    • 缺点: 控制粒度较粗,适用于简单的请求拦截和处理。
  • URLProtocol:

    • 优点: 强大灵活,可以处理所有类型的请求,适用于复杂的需求。
    • 缺点: 实现相对复杂,需要在应用启动时注册。
  • WKURLSchemeHandler:

    • 优点: 专为 WKWebView 设计,集成度高,适用于现代化需求。
    • 缺点: 需要 iOS 11.0+。

根据你的具体需求和支持的最低 iOS 版本,选择合适的方法实现请求拦截和本地缓存文件的返回。


版本不匹配问题

当打开 web 容器,下载的 html 文件与本地缓存的 css, js, 图片资源可能不匹配,就就涉及版本的匹配问题

要解决 HTML 版本与本地 CSS、JS,PNG 文件版本不匹配的问题,你可以采用以下几种方法:

1. 使用版本控制和版本号

为每个资源文件添加版本号,确保每次更新时客户端能够识别出新版本并下载最新的文件。这可以通过在 URL 中添加版本号查询参数来实现。

步骤:
  1. 更新资源文件 URL
    在 HTML 文件中,资源 URL 添加版本号。例如:
<link rel="stylesheet" href="local://style.css?v=1.0.0">
<script src="local://script.js?v=1.0.0"></script>
<img src="local://image.png?v=1.0.0" alt="example image">
  1. 修改 URL 拦截逻辑
    WKNavigationDelegateURLProtocol 中,处理 URL 的版本号,确保本地文件匹配正确的版本号。
import Foundation

class LocalFileURLProtocol: URLProtocol {
    override class func canInit(with request: URLRequest) -> Bool {
        return request.url?.scheme == "local"
    }

    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    override func startLoading() {
        guard let url = request.url else {
            return
        }

        // 从 URL 中去掉版本号查询参数
        let strippedURL = removeVersionParameter(from: url)
        if let localFileURL = getLocalFileURL(for: strippedURL) {
            do {
                let data = try Data(contentsOf: localFileURL)
                let mimeType = mimeTypeForPath(path: localFileURL.path) ?? "application/octet-stream"
                let response = URLResponse(url: url, mimeType: mimeType, expectedContentLength: data.count, textEncodingName: nil)
                client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
                client?.urlProtocol(self, didLoad: data)
                client?.urlProtocolDidFinishLoading(self)
            } catch {
                client?.urlProtocol(self, didFailWithError: error)
            }
        }
    }

    override func stopLoading() {}

    private func getLocalFileURL(for url: URL) -> URL? {
        let fileManager = FileManager.default
        let documentsURL = try? fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
        return documentsURL?.appendingPathComponent(url.lastPathComponent)
    }

    private func mimeTypeForPath(path: String) -> String? {
        let pathExtension = (path as NSString).pathExtension
        if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue() {
            if let mimeType = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
                return mimeType as String
            }
        }
        return nil
    }

    private func removeVersionParameter(from url: URL) -> URL {
        var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
        components?.queryItems = components?.queryItems?.filter { $0.name != "v" }
        return components?.url ?? url
    }
}

2. 使用缓存策略

利用缓存策略来确保本地缓存的文件与服务器上的文件版本一致。可以在每次应用启动时,检查文件的版本信息,如果不匹配,则重新下载。

步骤:
  1. 保存文件版本信息
    在本地保存每个文件的版本信息,可以使用 UserDefaults 或其他存储方式。

  2. 检查版本并更新文件
    在应用启动时,检查文件版本是否匹配,如果不匹配则下载新版本。

import Foundation

class FileDownloader {
    static let shared = FileDownloader()

    private init() {}

    func downloadFile(from url: URL, version: String, completion: @escaping (URL?) -> Void) {
        let currentVersion = UserDefaults.standard.string(forKey: url.lastPathComponent)
        if currentVersion == version {
            // 版本一致,直接返回本地文件路径
            let documentsURL = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
            let localFileURL = documentsURL?.appendingPathComponent(url.lastPathComponent)
            completion(localFileURL)
            return
        }

        // 版本不一致,下载新文件
        let task = URLSession.shared.downloadTask(with: url) { location, response, error in
            guard let location = location, error == nil else {
                completion(nil)
                return
            }
            let fileManager = FileManager.default
            do {
                let documentsURL = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
                let destinationURL = documentsURL.appendingPathComponent(url.lastPathComponent)
                try fileManager.moveItem(at: location, to: destinationURL)
                UserDefaults.standard.set(version, forKey: url.lastPathComponent)
                completion(destinationURL)
            } catch {
                completion(nil)
            }
        }
        task.resume()
    }
}

3. 文件完整性检查

通过文件哈希值或校验和来确保本地文件的完整性,如果文件损坏或版本不一致,则重新下载。

步骤:
  1. 计算文件哈希值
    计算下载文件的哈希值并与服务器上的哈希值进行比较,确保文件完整性。

  2. 下载和验证
    在每次启动时,下载文件并验证哈希值是否匹配。

import CommonCrypto

func calculateMD5(for fileURL: URL) -> String? {
    let fileManager = FileManager.default
    if let fileData = fileManager.contents(atPath: fileURL.path) {
        var hash = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
        fileData.withUnsafeBytes {
            _ = CC_MD5($0.baseAddress, CC_LONG(fileData.count), &hash)
        }
        return hash.map { String(format: "%02x", $0) }.joined()
    }
    return nil
}

func validateFile(at fileURL: URL, withExpectedHash expectedHash: String) -> Bool {
    if let fileHash = calculateMD5(for: fileURL) {
        return fileHash == expectedHash
    }
    return false
}

4. 使用服务器配置文件

使用服务器上的配置文件来管理资源的版本信息,每次启动时下载配置文件并根据版本信息更新资源文件。

步骤:
  1. 创建配置文件
    在服务器上创建一个 JSON 文件,包含所有资源文件的版本信息。
{
    "style.css": "1.0.0",
    "script.js": "1.0.0",
    "image.png": "1.0.0"
}
  1. 下载配置文件并检查版本
    在应用启动时,下载配置文件并检查每个资源文件的版本,如果版本不匹配则下载新文件。
func downloadConfigFile(completion: @escaping ([String: String]?) -> Void) {
    let configURL = URL(string: "https://example.com/config.json")!
    let task = URLSession.shared.dataTask(with: configURL) { data, response, error in
        guard let data = data, error == nil else {
            completion(nil)
            return
        }
        let config = try? JSONDecoder().decode([String: String].self, from: data)
        completion(config)
    }
    task.resume()
}

func updateFiles(with config: [String: String]) {
    let baseURL = URL(string: "https://example.com/")!
    let dispatchGroup = DispatchGroup()

    for (fileName, version) in config {
        dispatchGroup.enter()
        let fileURL = baseURL.appendingPathComponent(fileName)
        FileDownloader.shared.downloadFile(from: fileURL, version: version) { localURL in
            if let localURL = localURL {
                print("File downloaded to: \(localURL)")
            } else {
                print("Failed to download file: \(fileURL)")
            }
            dispatchGroup.leave()
        }
    }

    dispatchGroup.notify(queue: .main) {
        print("All files have been updated.")
    }
}

downloadConfigFile { config in
    if let config = config {
        updateFiles(with: config)
    }
}

通过以上方法,可以有效地解决 HTML 版本与本地资源文件版本不匹配的问题,确保在每次启动时都能使用最新的资源文件,从而提升 WKWebView 的渲染速度和稳定性。

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

依旧风轻

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值