Apple Pay 支付集成实战与安全验证机制解析

西门景荣 移动 阅读 842
赞 28 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

上个月上线 Apple Pay 支付功能时,我差点被用户反馈砸死。不是支付失败,而是点开支付按钮后整个页面卡住 3~5 秒,手指滑动完全没反应。iOS 用户尤其明显——Safari 里直接白屏转圈,连 cancel 都点不动。测试机是 iPhone 12,不算老,但体验差得离谱。

Apple Pay 支付集成实战与安全验证机制解析

最开始我以为是网络问题,毕竟 Apple Pay 要调用 Apple 的 JS SDK,还要发请求到后端。但抓包一看,API 响应其实很快(200ms 内),卡顿发生在点击按钮到弹出原生支付窗口之间的空白期。这期间主线程完全被占满,连 console.log 都打不出来。

找到瓶颈了!

折腾了半天,终于用 Safari 的 Web Inspector 抓到了真凶。Performance 面板里一录,发现每次触发 Apple Pay 时,有大量同步的 DOM 操作和重排,而且集中在 ApplePaySession 初始化前后。再细看,原来是我们封装的支付组件在每次调用时都重新创建了整个支付配置对象,还顺带触发了父组件的 rerender。

更坑的是,我们为了“兼容性”,在调用前手动检查了十几次 window.ApplePaySession 是否存在,每次还都加了 console.log 调试(上线忘了删……)。这些看似无害的操作,在 iOS 的 JS 引擎里叠加起来,直接把帧率干到个位数。

核心优化:懒加载 + 配置缓存

试了几种方案,最后这个效果最好:**把 Apple Pay 的初始化逻辑彻底懒加载,并且缓存支付配置**。

首先,不要在页面加载时就 import Apple Pay 相关代码。我们用动态 import 把它拆成独立 chunk:

// 原来的写法:页面一加载就引入
import { createApplePaySession } from './apple-pay';

// 优化后:点击按钮才加载
const handleApplePayClick = async () => {
  const { createApplePaySession } = await import('./apple-pay');
  // ...后续逻辑
};

其次,支付配置(比如 merchantIdentifier、supportedNetworks)其实不会变,没必要每次点都重新生成。我搞了个简单的缓存:

// apple-pay.js
let cachedPaymentRequest = null;

function getPaymentRequest() {
  if (cachedPaymentRequest) return cachedPaymentRequest;
  
  cachedPaymentRequest = {
    countryCode: 'US',
    currencyCode: 'USD',
    supportedNetworks: ['visa', 'masterCard', 'amex'],
    merchantCapabilities: ['supports3DS'],
    total: { label: 'Total', amount: '10.00' }
  };
  return cachedPaymentRequest;
}

export function createApplePaySession() {
  const paymentRequest = getPaymentRequest();
  return new ApplePaySession(3, paymentRequest);
}

这里注意我踩过好几次坑:不要在 getPaymentRequest 里放任何动态数据(比如订单金额)。如果金额会变,就得每次重建对象,但其他静态字段(countryCode、networks)还是可以缓存的。我们后来改成了只缓存基础结构,金额单独 merge:

function getPaymentRequest(baseAmount) {
  const baseConfig = {
    countryCode: 'US',
    currencyCode: 'USD',
    supportedNetworks: ['visa', 'masterCard', 'amex'],
    merchantCapabilities: ['supports3DS']
  };
  return {
    ...baseConfig,
    total: { label: 'Total', amount: baseAmount }
  };
}

次要优化:砍掉冗余检查

之前为了“健壮性”,写了这种代码:

// 优化前:多次检查 + console.log
if (window.ApplePaySession) {
  console.log('Apple Pay available');
  if (ApplePaySession.canMakePayments()) {
    console.log('Can make payments');
    // ...初始化
  }
}

现在直接一行搞定,而且移到动态 import 之后,避免主线程阻塞:

// 优化后
const { ApplePaySession } = window;
if (!ApplePaySession || !ApplePaySession.canMakePayments()) return;

别小看这几行 console.log,实测在低端 iPhone 上能省下 100~200ms。另外,不要在 Apple Pay 流程中触发任何 React setState,否则会强制 rerender 整个组件树。我们把支付状态改用 useRef 管理,完全绕过 React 的更新机制。

性能数据对比

优化前后在 iPhone 12(iOS 16)上的实测数据:

  • 首屏到可点击 Apple Pay 按钮:从 2.1s → 1.3s(因为懒加载减少了主包体积)
  • 点击按钮到弹出原生支付窗口:从平均 4.8s → 780ms(P95 数据)
  • 主线程阻塞时间:从 3200ms → 210ms(Web Inspector Performance 面板测量)

最明显的感受是,现在点完按钮几乎立刻弹出支付界面,手指滑动也不会卡住。虽然偶尔还有 100ms 左右的延迟(主要是 Apple 的 JS SDK 初始化开销),但已经不影响用户体验了。

还有个小坑:Safari 的严格模式

差点忘了提,iOS 15+ 的 Safari 对 Apple Pay 的调用上下文要求更严了。必须在用户手势(click/tap)的直接回调里调用 new ApplePaySession(),不能放在 setTimeout 或 Promise then 里。我们之前为了“优雅”加了个 loading 动画,结果在部分机型上直接报错:InvalidAccessError: Trying to start an Apple Pay session from an insecure context

解决方案很简单:把 loading 动画改成 CSS-only 的,或者确保 ApplePaySession 初始化在 click 事件的同步代码里。比如:

// 错误示范
button.addEventListener('click', async () => {
  showLoading(); // 触发 rerender
  await delay(100); // 这里已经脱离用户手势上下文
  const session = new ApplePaySession(...); // 可能报错
});

// 正确做法
button.addEventListener('click', () => {
  const session = new ApplePaySession(...); // 必须同步创建
  session.begin();
  showLoading(); // 后续操作可以异步
});

总结一下

这次优化的核心就两点:懒加载 Apple Pay 代码 + 缓存静态配置。其他都是小修小补。改完后虽然还有极少数用户反馈“偶尔慢”,但基本都在 1 秒内完成,属于可接受范围。毕竟 Apple Pay 的 JS SDK 本身就有几百 KB,再怎么优化也绕不开它的初始化成本。

以上是我踩坑后的总结,希望对你有帮助。如果有更优的实现方式(比如用 Web Worker?),欢迎评论区交流!

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论