PAYATHON 2026

ios App完成h5支付返回后localStorage缓存异常

支付小周

结论

问题通常不在 localStorage API 本身。更常见的原因是支付返回后,App 新建了 WKWebView、切换到了不同的 WKWebsiteDataStore,或者重新显示了导航栈中旧的 H5 实例。支付前后的页面虽然看起来一样,实际可能不属于同一个 WebView、存储空间或页面生命周期。

排查和处理时,应优先确认以下几点:

  1. 支付前后是否复用了同一个 WKWebView。
  2. 所有 WKWebView 是否统一使用持久化的 WKWebsiteDataStore.default()。
  3. 支付结果是否以服务端订单状态为准,而不是由 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,看到的仍然是旧数据。

还可能出现这样的情况:

  1. 原页面所在的 WebView 一直没有销毁。
  2. 支付回调通过 URL Scheme 或 Universal Link 再次打开页面控制器。
  3. 新控制器又创建了一个 WebView。
  4. 用户下次进入时,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');

推荐的支付返回流程

可以按照以下流程处理支付返回:

  1. H5 创建订单并保存 orderId。
  2. App 或 H5 拉起支付。
  3. 支付完成后,通过 URL Scheme、Universal Link 或 SDK 回调唤醒 App。
  4. App 返回原来的 WKWebView,不要重复创建页面。
  5. App 通过 JavaScript Bridge 通知 H5。
  6. H5 使用 orderId 请求服务端查询订单状态。
  7. 服务端确认支付成功后,H5 再更新界面和本地辅助缓存。

localStorage 只适合保存非关键的页面状态,不能作为支付成功凭证。无论支付应用返回成功、客户端 SDK 返回成功,还是本地存在 paid 标记,都不能替代服务端对支付通知和订单状态的确认。

统一使用 WKWebsiteDataStore.default(),确认支付回跳地址同源,并在支付返回时复用原有 WKWebView 后,通常就能解决“当前修改有效,下次进入又恢复旧缓存”的问题。

备注:内容仅供参考。