ios App完成h5支付返回后localStorage缓存异常
结论
问题通常不在 localStorage API 本身。更常见的原因是支付返回后,App 新建了 WKWebView、切换到了不同的 WKWebsiteDataStore,或者重新显示了导航栈中旧的 H5 实例。支付前后的页面虽然看起来一样,实际可能不属于同一个 WebView、存储空间或页面生命周期。
排查和处理时,应优先确认以下几点:
- 支付前后是否复用了同一个
WKWebView。 - 所有
WKWebView是否统一使用持久化的WKWebsiteDataStore.default()。 - 支付结果是否以服务端订单状态为准,而不是由
localStorage判断。
为什么会出现“页面像复制了一份”
localStorage 按 Origin 隔离。协议、域名和端口必须完全一致,页面才能访问同一份数据:
https://example.com/page
http://example.com/page
https://www.example.com/page
https://example.com:8443/page
这些地址对应的存储空间彼此独立。
在 iOS App 中,还要检查 WebView 的存储配置。如果支付返回时重新创建了 WKWebView,并且使用非持久化数据仓库:
configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent()
那么页面对 localStorage 的修改只会保存在这个临时 WebView 中。下次打开原来的持久化 WebView,看到的仍然是旧数据。
还可能出现这样的情况:
- 原页面所在的 WebView 一直没有销毁。
- 支付回调通过 URL Scheme 或 Universal Link 再次打开页面控制器。
- 新控制器又创建了一个 WebView。
- 用户下次进入时,App 展示的仍是导航栈中的旧 WebView。
这并不是网页真的被复制了,而是两个页面实例同时存在。
原生端统一 WebView 和数据仓库
创建 WKWebView 时,应明确使用默认的持久化存储:
import WebKit
final class WebViewController: UIViewController {
private lazy var webView: WKWebView = {
let configuration = WKWebViewConfiguration()
configuration.websiteDataStore = WKWebsiteDataStore.default()
let webView = WKWebView(
frame: .zero,
configuration: configuration
)
return webView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(webView)
webView.frame = view.bounds
}
}
支付完成后,不要直接新建 WebViewController。应找到原来的支付页面,返回该页面并通知它刷新订单状态,例如:
func handlePaymentCallback(orderId: String) {
guard let webViewController = findExistingWebViewController() else {
return
}
navigationController?.popToViewController(
webViewController,
animated: true
)
let script = """
window.dispatchEvent(new CustomEvent('payment-return', {
detail: { orderId: '\(orderId)' }
}));
"""
webViewController.webView.evaluateJavaScript(script)
}
实际项目中,传给 JavaScript 的字符串必须经过安全序列化,不能直接拼接不可信数据。参数可以通过 JSONSerialization 生成。
如果业务必须创建多个 WKWebView,至少应确保它们全部使用:
WKWebsiteDataStore.default()
不要让一个页面使用 default(),另一个页面使用 nonPersistent()。
检查返回地址是否同源
分别在支付前、支付返回后和下次进入页面时输出以下信息:
console.log({
href: location.href,
origin: location.origin,
protocol: location.protocol,
hostname: location.hostname,
port: location.port,
token: localStorage.getItem('token')
});
重点检查这些变化:
http是否变成了https- 主域名是否变成了子域名
example.com是否变成了www.example.com- 端口是否改变
- 页面是否被重定向到支付平台的中间域名
- 页面是否通过
file://、自定义协议或本地离线包加载
支付回跳地址最好固定为明确的 HTTPS 地址,再由该地址通知 App:
https://m.example.com/payment/result?orderId=123
如果支付前后无法保持同源,就不要通过 localStorage 在两个页面之间传递数据。
正确处理页面恢复事件
iOS 从外部支付页面返回时,不一定会完整地重新加载页面。页面可能只触发 visibilitychange、pageshow,或者经历原生容器的前后台切换。
H5 可以监听这些事件,并在页面重新可见时主动查询订单:
let checking = false;
async function refreshPaymentStatus() {
if (checking) return;
checking = true;
try {
const orderId = sessionStorage.getItem('pendingOrderId');
if (!orderId) return;
const response = await fetch(
`/api/orders/${encodeURIComponent(orderId)}/status`,
{
method: 'GET',
credentials: 'include',
cache: 'no-store'
}
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const result = await response.json();
if (result.status === 'PAID') {
sessionStorage.removeItem('pendingOrderId');
localStorage.setItem('lastPaidOrderId', orderId);
renderPaymentSuccess(result);
}
} finally {
checking = false;
}
}
window.addEventListener('payment-return', refreshPaymentStatus);
window.addEventListener('pageshow', event => {
console.log('pageshow persisted:', event.persisted);
refreshPaymentStatus();
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
refreshPaymentStatus();
}
});
event.persisted === true 表示页面可能是从 Back/Forward Cache 恢复的。此时不能假设页面已经重新执行初始化流程,应再次拉取关键业务状态。
验证写入是否真的发生
写入后可以立即读取,并监听其他页面实例中的存储变化:
function updateStorage() {
localStorage.removeItem('paymentStatus');
localStorage.setItem('paymentStatus', 'paid');
console.log(
'paymentStatus after update:',
localStorage.getItem('paymentStatus')
);
}
window.addEventListener('storage', event => {
console.log('storage changed:', {
key: event.key,
oldValue: event.oldValue,
newValue: event.newValue,
url: event.url
});
});
storage 事件通常只会发送给其他同源页面实例,不会在执行写入的当前页面中触发。它可以帮助判断是否同时存在多个同源页面,但不能单独用来证明当前页面已经写入成功。
调试时,还可以为每个页面实例生成一个标识:
const pageInstanceId =
`${Date.now()}-${Math.random().toString(16).slice(2)}`;
console.log('page instance:', pageInstanceId);
如果支付返回后与再次进入页面时打印出的实例不同,同时原生导航栈里还保留着旧控制器,基本可以确认存在重复 WebView 或重复页面。
不要通过清空全部缓存修复
下面的操作也许能暂时让问题消失,但不适合作为正式解决方案:
WKWebsiteDataStore.default().removeData(
ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(),
modifiedSince: Date(timeIntervalSince1970: 0)
) {
// 完成
}
它会同时清除 Cookie、缓存、IndexedDB 和 localStorage 等网站数据,可能导致登录状态丢失,而且不能解决重复创建 WebView、Origin 不一致或导航栈错误。
H5 端也不建议直接执行:
localStorage.clear();
这会删除当前 Origin 下所有业务模块的数据。应只删除确定不再需要的键:
localStorage.removeItem('paymentStatus');
localStorage.removeItem('pendingOrderId');
推荐的支付返回流程
可以按照以下流程处理支付返回:
- H5 创建订单并保存
orderId。 - App 或 H5 拉起支付。
- 支付完成后,通过 URL Scheme、Universal Link 或 SDK 回调唤醒 App。
- App 返回原来的
WKWebView,不要重复创建页面。 - App 通过 JavaScript Bridge 通知 H5。
- H5 使用
orderId请求服务端查询订单状态。 - 服务端确认支付成功后,H5 再更新界面和本地辅助缓存。
localStorage 只适合保存非关键的页面状态,不能作为支付成功凭证。无论支付应用返回成功、客户端 SDK 返回成功,还是本地存在 paid 标记,都不能替代服务端对支付通知和订单状态的确认。
统一使用 WKWebsiteDataStore.default(),确认支付回跳地址同源,并在支付返回时复用原有 WKWebView 后,通常就能解决“当前修改有效,下次进入又恢复旧缓存”的问题。
备注:内容仅供参考。
