1. 概述 #
本文档旨在详细阐述为 iOS WKWebView 设计并实现的一套高级、智能的缓存解决方案。该方案的目标是提升H5页面的加载性能、优化用户体验,并为复杂的Web应用(尤其是采用Webpack等现代前端工具构建的单页应用)提供强大的缓存控制能力。
核心目标 #
- 提升性能:最大化利用缓存,减少不必要的网络请求,加快页面和资源的加载速度。
- 永久缓存静态资源:对于文件名中带有hash值(如
app.a1b2c3d4.js)的不可变资源,实现“一次加载,永久使用”的效果。 - 智能决策:根据资源类型(HTML, JS, CSS)、URL特征(是否带hash)、网络状态(在线/离线)和手动配置,自动选择最优的缓存策略。
- 完全控制:实现对页面所有子资源(包括JS动态加载的资源)的请求拦截和缓存控制。
2. 缓存策略设计与权衡 #
我们探讨并实现了一套分层、优先的缓存决策逻辑,并对关键技术点进行了权衡。
2.1. 缓存决策的优先级 #
核心方法 getSmartCachePolicy 遵循以下优先级顺序进行决策:
最高优先级:网络状态
- 如果设备离线,无条件使用缓存 (
.returnCacheDataDontLoad),保证基础的离线访问能力。
- 如果设备离线,无条件使用缓存 (
次高优先级:手动配置规则
- 开发者可以预设强制刷新或永久缓存的URL路径前缀,实现对特定API或目录的精准控制。
标准优先级:智能识别规则
- 根据URL和资源类型自动判断,如带hash的资源、普通静态资源、HTML页面等。
最低优先级:默认协议
- 对于不匹配任何上述规则的请求,遵循HTTP协议本身定义的缓存行为 (
.useProtocolCachePolicy)。
- 对于不匹配任何上述规则的请求,遵循HTTP协议本身定义的缓存行为 (
2.2. HTML页面的缓存策略权衡 #
我们重点讨论了HTML页面应采用何种策略:
.reloadIgnoringLocalCacheData:强制刷新,最安全但性能较低。能确保用户总能获取最新的HTML,避免因HTML陈旧导致资源引用错误。.useProtocolCachePolicy:遵循服务器协议,性能更高但依赖后端配置。如果服务器为HTML配置了Cache-Control: no-cache和ETag,则可实现高效的304验证,是最佳实践。
最终决策:考虑到前端服务器已配置 Cache-Control: no-cache,我们选择 .useProtocolCachePolicy。这使得App具备了“面向未来”的适应性——一旦服务器增加了ETag支持,App无需更新即可自动获得性能提升。
3. 核心实现:从导航拦截到完全拦截 #
我们通过两个阶段的迭代,最终实现了对所有资源的完全拦截。
阶段一:导航拦截 (WKNavigationDelegate)
#
此方案通过WKNavigationDelegate拦截主文档的加载请求,实现简单、风险低,能解决大部分问题。
3.1.1. 核心组件 #
BaseWebViewController+Caching.swift (核心逻辑)
import UIKit
import WebKit
extension BaseWebViewController {
/// 获取完整的智能缓存策略
private func getSmartCachePolicy(for url: URL) -> URLRequest.CachePolicy {
print("--- [SmartCache] Evaluating URL: \(url.absoluteString)")
// 1. 最高优先级:网络状态
if !NetworkReachability.shared.isConnected {
print("--- [SmartCache] Rule: Offline. Policy: returnCacheDataDontLoad")
return .returnCacheDataDontLoad
}
let path = url.path
// 2. 次高优先级:手动配置规则
for noCachePath in CacheConfig.noCachePaths {
if path.hasPrefix(noCachePath) {
print("--- [SmartCache] Rule: Matched no-cache path '\(noCachePath)'. Policy: reloadIgnoringLocalCacheData")
return .reloadIgnoringLocalCacheData
}
}
for permanentPath in CacheConfig.permanentCachePaths {
if path.hasPrefix(permanentPath) {
print("--- [SmartCache] Rule: Matched permanent cache path '\(permanentPath)'. Policy: returnCacheDataElseLoad")
return .returnCacheDataElseLoad
}
}
// 3. 标准优先级:智能识别规则
if url.hasHashInFilename {
print("--- [SmartCache] Rule: Has hash in filename. Policy: returnCacheDataElseLoad")
return .returnCacheDataElseLoad
}
if url.isStaticResource {
print("--- [SmartCache] Rule: Is static resource. Policy: returnCacheDataElseLoad")
return .returnCacheDataElseLoad
}
// 最终采纳的HTML页面策略
if url.isHTMLPage {
print("--- [SmartCache] Rule: Is HTML page. Policy: useProtocolCachePolicy")
return .useProtocolCachePolicy
}
// 4. 最低优先级:默认
print("--- [SmartCache] Rule: No specific rule matched. Policy: useProtocolCachePolicy")
return .useProtocolCachePolicy
}
// ... loadWithSmartCache 方法 ...
}
CacheConfiguration.swift (手动配置)
import Foundation
struct CacheConfig {
static let permanentCachePaths: [String] = ["/static/", "/assets/", "/dist/"]
static let noCachePaths: [String] = ["/api/", "/user/", "/realtime/"]
}
URL+CacheHelpers.swift (URL辅助判断)
import Foundation
extension URL {
var isStaticResource: Bool { ... }
var hasHashInFilename: Bool { ... }
var isHTMLPage: Bool { ... }
}
3.1.2. 缺点 #
此方案无法拦截页面通过JavaScript动态加载的子资源(如 import() 或 fetch 加载的模块)。
阶段二:完全拦截 (WKURLSchemeHandler)
#
为了实现对所有资源的完全控制,我们引入了WKURLSchemeHandler,这是Apple官方推荐的终极方案。
3.2.1. 工作流程 #
- 注入JS:通过
WKUserScript在页面加载前注入一个url_rewriter.js脚本。 - 重写URL:该脚本通过
MutationObserver和“猴子补丁”技术,将页面中所有符合条件的资源URL(JS/CSS)从https://重写为自定义的app-cache-https://协议。 - 原生处理:
WKWebView将所有自定义协议的请求交给一个我们实现的CacheSchemeHandler对象。 - 应用策略:在
CacheSchemeHandler中,我们将URL还原,并可调用getSmartCachePolicy来应用缓存策略,然后手动发起网络请求或从本地缓存读取数据,最后将结果回调给WebView。
3.2.2. 核心组件 #
url_rewriter.js (注入脚本)
该脚本是实现完全拦截的关键,它结合了DOM监控和函数重写,以覆盖绝大多数资源加载场景。
(function() {
const newScheme = 'app-cache-https';
function rewriteUrl(originalUrl) { ... }
// --- 1. 拦截基于 DOM 的加载 ---
function rewriteDomElement(element) { ... }
const observer = new MutationObserver(...);
observer.observe(document, { childList: true, subtree: true });
// --- 2. 拦截基于 fetch 的加载 ---
const originalFetch = window.fetch;
window.fetch = function(input, init) { ... };
// --- 3. 拦截基于 XMLHttpRequest 的加载 ---
const originalXhrOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, async, user, password) { ... };
})();
CacheSchemeHandler.swift (自定义协议处理器)
import WebKit
class CacheSchemeHandler: NSObject, WKURLSchemeHandler {
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeT) {
// 1. 还原 URL
// 2. 调用 getSmartCachePolicy 获取缓存策略
// 3. 手动发起 URLSession 请求
// 4. 将 response 和 data 回传给 urlSchemeTask
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
// 取消网络请求
}
}
BaseWebViewController (配置注入)
// 在 WKWebViewConfiguration 的初始化过程中
// 1. 读取JS文件
if let jsPath = Bundle.main.path(forResource: "url_rewriter", ofType: "js"),
let jsSource = try? String(contentsOfFile: jsPath) {
// 2. 创建并添加 WKUserScript
let script = WKUserScript(source: jsSource, injectionTime: .atDocumentStart, forMainFrameOnly: false)
userContentController.add(script)
}
// 3. 注册 Scheme Handler
config.setURLSchemeHandler(CacheSchemeHandler(), forURLScheme: "app-cache-https")
4. 安全性与稳定性分析 #
安全性:可靠
- JS来源可信:注入的JS脚本由我们自己控制,并非来自外部,杜绝了恶意代码风险。
- 协议本地有效:自定义的
app-cache-https协议仅在当前WKWebView实例内部有效,不会暴露给系统或其他App,无法被外部调用。
稳定性:中等风险
- 主要风险在于JS的“猴子补丁”技术可能与某些前端框架或特定网站的JS代码不兼容,从而导致功能性问题。因此,必须在目标网页上进行充分的兼容性测试。
5. 结论 #
通过结合WKNavigationDelegate和WKURLSchemeHandler,我们构建了一套强大、灵活且安全的WKWebView缓存体系。该体系不仅满足了对带hash值的静态资源进行永久缓存的核心需求,还通过JS注入实现了对动态加载资源的完全拦截和控制,为深度优化H5性能提供了坚实的基础。