手动下载并缓存资源是一种有效的方式,可以确保在需要时资源已经在本地存储,这样可以显著提高加载速度。
缓存整个 web 页面的所有资源文件
具体实现步骤
- 下载和缓存资源:包括 HTML 文件、CSS、JavaScript 和图像。
- 在应用启动时预加载资源。
- 构建包含所有预加载资源的 HTML 字符串。
- 加载构建的 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
}
}
详细说明
-
资源下载和缓存:
- 使用
URLSession
下载资源(包括HTML文件、CSS、JavaScript和图像),并将其缓存到本地存储。 - 下载完成后,资源数据被写入本地存储,以便后续使用。
- 使用
-
预加载资源:
- 在应用启动时调用
downloadResources()
方法,预先下载和缓存所需的资源。
- 在应用启动时调用
-
使用缓存的资源加载完整页面:
- 在
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 应用中创建安全通信通道非常有用。
具体实现涉及两个主要方法:
webView:startURLSchemeTask:
- 启动处理特定 URL 请求。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.lastPathComponent
是 URL
类的一个属性,用来获取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 中添加版本号查询参数来实现。
步骤:
- 更新资源文件 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">
- 修改 URL 拦截逻辑
在WKNavigationDelegate
或URLProtocol
中,处理 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. 使用缓存策略
利用缓存策略来确保本地缓存的文件与服务器上的文件版本一致。可以在每次应用启动时,检查文件的版本信息,如果不匹配,则重新下载。
步骤:
-
保存文件版本信息
在本地保存每个文件的版本信息,可以使用UserDefaults
或其他存储方式。 -
检查版本并更新文件
在应用启动时,检查文件版本是否匹配,如果不匹配则下载新版本。
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. 文件完整性检查
通过文件哈希值或校验和来确保本地文件的完整性,如果文件损坏或版本不一致,则重新下载。
步骤:
-
计算文件哈希值
计算下载文件的哈希值并与服务器上的哈希值进行比较,确保文件完整性。 -
下载和验证
在每次启动时,下载文件并验证哈希值是否匹配。
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. 使用服务器配置文件
使用服务器上的配置文件来管理资源的版本信息,每次启动时下载配置文件并根据版本信息更新资源文件。
步骤:
- 创建配置文件
在服务器上创建一个 JSON 文件,包含所有资源文件的版本信息。
{
"style.css": "1.0.0",
"script.js": "1.0.0",
"image.png": "1.0.0"
}
- 下载配置文件并检查版本
在应用启动时,下载配置文件并检查每个资源文件的版本,如果版本不匹配则下载新文件。
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 的渲染速度和稳定性。