一、背景
在 iOS 开发中,WKWebView 是承载 H5 的核心组件。
在实际业务中,经常会遇到以下问题:
- 每次调用
webView.load(request)后,backForwardList.backList都会不断增长 - 无法像
UIWebView或浏览器那样手动清空历史 - 登录 / 切账号 / 切环境时,希望 Web 页面“像第一次打开一样”
- Web 返回逻辑与 Native 返回逻辑产生冲突
因此,有必要从原理层面理解 WKWebView 的导航栈机制,并给出可落地的工程解决方案。
二、核心结论
WKWebView 不提供任何公开 API 来直接清空
backForwardList。
backForwardList是否增长,取决于 WebKit 是否判定为一次 new navigation,而不是你是否调用了load()。
三、原理解析
1. backForwardList 是什么?
WKBackForwardList是 WebKit 内部维护的导航历史栈-
包含三部分:
-
backList:可回退页面 -
currentItem:当前页面 -
forwardList:可前进页面
-
只读、不可修改
webView.backForwardList.backList // 只读
webView.backForwardList.currentItem
webView.backForwardList.forwardList
2. 是否压栈的根本判断标准
是否被 WebKit 认为是一次 “new navigation”
与以下因素有关:
- URL 是否变化
- 是否是 same-document navigation
- 是否是 reload
- 是否是 replace 行为
- 是否涉及 POST / header / redirect
❌ 与 load() 调用次数不等价
四、行为分类与压栈规则
1️⃣ 一定会压栈的情况
| 行为 | 说明 |
|---|---|
load(newURL) |
标准新导航 |
| POST 请求 | 即使 URL 相同 |
| Header / Cookie 变化 | WebKit 视为新请求 |
| 302 / 307 重定向 | 源 URL 会进栈 |
location.href = |
JS 新导航 |
2️⃣ 不会压栈的情况
| 行为 | 说明 |
|---|---|
reload() |
刷新当前 entry |
location.replace() |
替换当前 entry |
history.replaceState() |
Web 内替换 |
| hash 变化 | same-document |
| SPA 内部路由 | 不进入 native 栈 |
五、关键代码示例
1. 观察 backForwardList 变化(调试必备)
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("backList:",
webView.backForwardList.backList.map { $0.url.absoluteString })
print("current:",
webView.url?.absoluteString ?? "")
}
2. “等效清空历史”的方案一:loadHTMLString
let html = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
window.location.replace('\(url.absoluteString)');
</script>
</head>
<body></body>
</html>
"""
webView.loadHTMLString(html, baseURL: nil)
效果:
- 重置导航起点
-
backList为空 - 返回直接交给 Native
3. “彻底清空”的方案二:重建 WKWebView(最稳)
func recreateWebView(with request: URLRequest) {
webView.removeFromSuperview()
webView.navigationDelegate = nil
webView.uiDelegate = nil
let config = WKWebViewConfiguration()
let newWebView = WKWebView(frame: container.bounds,
configuration: config)
newWebView.navigationDelegate = self
newWebView.uiDelegate = self
container.addSubview(newWebView)
webView = newWebView
webView.load(request)
}
六、时序图(Navigation & backForwardList)
Native(WKWebView) WebKit
│ │
│ load(A) │
├────────────────────────▶│
│ │ create entry A
│ │ backList = []
│◀────────────────────────┤
│
│ load(B) │
├────────────────────────▶│
│ │ new navigation
│ │ backList = [A]
│◀────────────────────────┤
│
│ reload() │
├────────────────────────▶│
│ │ no new entry
│ │ backList = [A]
│◀────────────────────────┤
│
│ location.replace(C) │
├────────────────────────▶│
│ │ replace current
│ │ backList = [A]
│◀────────────────────────┤
七、常见误区
| 误区 | 说明 |
|---|---|
以为 load() 次数 = 历史条数 |
❌ |
| 用 JS 清 native 栈 | ❌ 不可能 |
stopLoading() 能清历史 |
❌ |
allowsBackForwardNavigationGestures 能影响栈 |
❌ 只影响手势 |
八、工程级建议
推荐决策树
-
希望“像第一次打开”
→loadHTMLString作为新起点 -
账号 / 登录态 / 环境切换
→ 重建WKWebView -
Web 内部返回逻辑
→ SPA +replaceState
九、总结
WKWebView.backForwardList是 WebKit 内部导航栈,不可直接清空是否压栈取决于 new navigation 判定
load(request)≠ 一定压栈,但大多数业务场景会-
工程上只有两条“正道”:
- 重置起点(loadHTMLString)
- 重建实例(recreate WKWebView)
所有“试图直接操作 backList 的方案”都是不可行的
正确的姿势不是“怎么清栈”,而是“是否应该有栈”。