一、背景
在 iOS 開發(fā)中,WKWebView 是承載 H5 的核心組件。
在實(shí)際業(yè)務(wù)中,經(jīng)常會(huì)遇到以下問題:
- 每次調(diào)用
webView.load(request)后,backForwardList.backList都會(huì)不斷增長 - 無法像
UIWebView或?yàn)g覽器那樣手動(dòng)清空歷史 - 登錄 / 切賬號(hào) / 切環(huán)境時(shí),希望 Web 頁面“像第一次打開一樣”
- Web 返回邏輯與 Native 返回邏輯產(chǎn)生沖突
因此,有必要從原理層面理解 WKWebView 的導(dǎo)航棧機(jī)制,并給出可落地的工程解決方案。
二、核心結(jié)論
WKWebView 不提供任何公開 API 來直接清空
backForwardList。
backForwardList是否增長,取決于 WebKit 是否判定為一次 new navigation,而不是你是否調(diào)用了load()。
三、原理解析
1. backForwardList 是什么?
WKBackForwardList是 WebKit 內(nèi)部維護(hù)的導(dǎo)航歷史棧-
包含三部分:
-
backList:可回退頁面 -
currentItem:當(dāng)前頁面 -
forwardList:可前進(jìn)頁面
-
只讀、不可修改
webView.backForwardList.backList // 只讀
webView.backForwardList.currentItem
webView.backForwardList.forwardList
2. 是否壓棧的根本判斷標(biāo)準(zhǔn)
是否被 WebKit 認(rèn)為是一次 “new navigation”
與以下因素有關(guān):
- URL 是否變化
- 是否是 same-document navigation
- 是否是 reload
- 是否是 replace 行為
- 是否涉及 POST / header / redirect
? 與 load() 調(diào)用次數(shù)不等價(jià)
四、行為分類與壓棧規(guī)則
1?? 一定會(huì)壓棧的情況
| 行為 | 說明 |
|---|---|
load(newURL) |
標(biāo)準(zhǔn)新導(dǎo)航 |
| POST 請求 | 即使 URL 相同 |
| Header / Cookie 變化 | WebKit 視為新請求 |
| 302 / 307 重定向 | 源 URL 會(huì)進(jìn)棧 |
location.href = |
JS 新導(dǎo)航 |
2?? 不會(huì)壓棧的情況
| 行為 | 說明 |
|---|---|
reload() |
刷新當(dāng)前 entry |
location.replace() |
替換當(dāng)前 entry |
history.replaceState() |
Web 內(nèi)替換 |
| hash 變化 | same-document |
| SPA 內(nèi)部路由 | 不進(jìn)入 native 棧 |
五、關(guān)鍵代碼示例
1. 觀察 backForwardList 變化(調(diào)試必備)
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)
效果:
- 重置導(dǎo)航起點(diǎn)
-
backList為空 - 返回直接交給 Native
3. “徹底清空”的方案二:重建 WKWebView(最穩(wěn))
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)
}
六、時(shí)序圖(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]
│?────────────────────────┤
七、常見誤區(qū)
| 誤區(qū) | 說明 |
|---|---|
以為 load() 次數(shù) = 歷史條數(shù) |
? |
| 用 JS 清 native 棧 | ? 不可能 |
stopLoading() 能清歷史 |
? |
allowsBackForwardNavigationGestures 能影響棧 |
? 只影響手勢 |
八、工程級(jí)建議
推薦決策樹
-
希望“像第一次打開”
→loadHTMLString作為新起點(diǎn) -
賬號(hào) / 登錄態(tài) / 環(huán)境切換
→ 重建WKWebView -
Web 內(nèi)部返回邏輯
→ SPA +replaceState
九、總結(jié)
WKWebView.backForwardList是 WebKit 內(nèi)部導(dǎo)航棧,不可直接清空是否壓棧取決于 new navigation 判定
load(request)≠ 一定壓棧,但大多數(shù)業(yè)務(wù)場景會(huì)-
工程上只有兩條“正道”:
- 重置起點(diǎn)(loadHTMLString)
- 重建實(shí)例(recreate WKWebView)
所有“試圖直接操作 backList 的方案”都是不可行的
正確的姿勢不是“怎么清?!?,而是“是否應(yīng)該有?!薄?/strong>