Apple Pay集成实战从零到上线的全流程技术解析

小书妍 移动 阅读 2,656
赞 36 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

去年底上线 Apple Pay 支付流程,用户反馈「点支付按钮后要等好几秒才弹出钱包」,iOS 用户尤其明显。我们自己测也确实——在 iPhone 13 上,从点击「去支付」到 ApplePaySession 实例创建完成、canMakePayments() 返回 true,平均耗时 4.8s。更离谱的是,部分老机型(iPhone XS)直接卡死在 loading 状态,控制台连 error 都不报,就干等着。

Apple Pay集成实战从零到上线的全流程技术解析

这不是体验问题,是功能残缺。用户没耐心等 5 秒,尤其是电商场景,下单路径里多卡 1 秒,转化率就掉 0.3%(我们 A/B 测试过)。当时产品直接拍桌子:「要么下周修好,要么下线 Apple Pay」。

找到瘼颈了!

先用 Safari Web Inspector 连真机跑 Profile —— 不是 JS 执行慢,而是大量时间耗在「等待系统服务响应」。重点看 Network 标签,发现一个关键线索:每次调用 ApplePaySession.canMakePayments() 前,我们的代码都会先发一次 fetch('https://jztheme.com/api/payment/config') 拿 merchant ID 和支持的卡类型,再传给 Apple Pay 初始化。

但 Apple 官方文档写得很清楚:canMakePayments() 是同步检测本地能力的,根本不需要网络请求!我们却把它塞进了异步链里,还加了防抖和重试逻辑……折腾了半天才发现,这一步纯属自作多情。

第二个坑是初始化时机太晚。我们把整个 Apple Pay 流程封装成一个懒加载组件,用户点「支付」才 import() + new ApplePaySession()。结果 Safari 的 WebKit 在首次调用 Apple Pay API 时,要动态加载底层支付框架(约 2–3MB 的 native bridge),而这个加载过程是阻塞的,且无法预热。

核心优化:提前检测 + 预加载 + 缓存兜底

改法很简单,三步走:

  • 第一步:页面一打开就跑 canMakePayments(),不等用户点按钮。放在 <script> 标签里,DOMContentLoaded 后立刻执行,结果存在全局变量或 context 中。
  • 第二步:用 apple-pay-button 组件替代手写按钮,它自带「自动禁用/启用」逻辑,且触发时会复用已准备好的 session 状态,避免重复初始化。
  • 第三步:对 ApplePaySession 构造函数做轻量级预加载 —— 不是真建 session,而是用空配置提前触发 WebKit 加载支付桥接模块。

具体代码如下(这是最关键的部分,我贴全了):

// payment-optimizer.js
let applePayReady = false;
let canPayCache = null;

// 页面加载后立即检测,不等用户操作
if (window.ApplePaySession) {
  try {
    // 注意!这里必须用 try/catch,iOS 15 以下可能抛 ReferenceError
    canPayCache = ApplePaySession.canMakePayments();
    applePayReady = true;
  } catch (e) {
    canPayCache = false;
  }
}

// 预加载 Apple Pay bridge(关键!)
// 只需构造一次空 session 就能触发底层模块加载
if (window.ApplePaySession && !window.__APPLE_PAY_PRELOADED) {
  try {
    // 用最小化配置触发初始化(不传任何敏感字段)
    const dummyRequest = {
      countryCode: 'CN',
      currencyCode: 'CNY',
      supportedNetworks: ['visa', 'masterCard'],
      merchantCapabilities: ['supports3DS']
    };
    // 创建后立刻 abort,不走后续流程
    const dummySession = new ApplePaySession(1, dummyRequest);
    dummySession.abort();
    window.__APPLE_PAY_PRELOADED = true;
  } catch (e) {
    // 失败也不影响主流程,只是预加载没生效
  }
}

// 导出一个稳定可用的检测函数
export function isApplePayAvailable() {
  if (canPayCache !== null) return canPayCache;
  if (window.ApplePaySession) {
    try {
      return ApplePaySession.canMakePayments();
    } catch {
      return false;
    }
  }
  return false;
}

然后在 React 组件里这么用:

// ApplePayButton.jsx
import { isApplePayAvailable } from './payment-optimizer';

export default function ApplePayButton({ onPay }) {
  const [isAvailable, setIsAvailable] = useState(false);

  useEffect(() => {
    // 页面加载时已预检,这里直接读缓存
    setIsAvailable(isApplePayAvailable());
  }, []);

  const handleClick = () => {
    if (!isAvailable) return;

    const request = {
      countryCode: 'CN',
      currencyCode: 'CNY',
      supportedNetworks: ['visa', 'masterCard', 'amex'],
      merchantCapabilities: ['supports3DS'],
      total: { label: '商品', amount: '299.00' }
    };

    const session = new ApplePaySession(1, request);
    
    session.onvalidatemerchant = async (event) => {
      // 这里才是真正的网络请求(仅此一次)
      const response = await fetch('https://jztheme.com/api/applepay/validate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ validationURL: event.validationURL })
      });
      session.completeMerchantValidation(await response.json());
    };

    session.onpaymentauthorized = (event) => {
      onPay(event.payment);
      session.completePayment(ApplePaySession.STATUS_SUCCESS);
    };

    session.begin();
  };

  return (
    <apple-pay-button
      buttonStyle="black"
      buttonType="buy"
      onClick={handleClick}
      disabled={!isAvailable}
    />
  );
}

这里重点提醒三点(我踩过好几次坑):

  • 预加载那行 new ApplePaySession(1, dummyRequest) 必须在页面加载早期执行,放到 useEffect(() => {}, []) 里就晚了,因为 React 渲染完才触发;
  • apple-pay-button 元素的 disabled 属性必须靠 JS 控制,不能只靠 CSS,否则 Safari 会忽略点击;
  • onvalidatemerchant 里那个 fetch 请求,千万别加 loading 状态遮罩——Apple Pay 自带原生 loading 动画,你加了反而冲突。

优化后:流畅多了

改完上线灰度 5%,数据立马下来了:

  • iPhone 13:平均响应时间从 4800ms → 780ms(含网络验证);
  • iPhone XS:从「白屏卡死」→ 稳定 1200ms 内唤起;
  • 首屏可交互时间(TTI)无影响,因为预加载逻辑不到 2KB,且非阻塞。

最关键是用户行为数据:Apple Pay 点击率上升 22%,支付成功转化率回到预期水平。客服工单里关于「Apple Pay 卡住」的投诉归零。

当然,还有个小问题没彻底解决:iOS 14.5 以下设备仍会偶发 canMakePayments() 返回 false(实际可以),我们加了个 fallback:如果检测失败,但用户点了按钮,就尝试直接 new Session —— 大部分情况能兜住。不是最优解,但比让用户干等强。

性能数据对比

这是我们埋点统计的真实均值(取最近 7 天线上数据):

机型 优化前(ms) 优化后(ms) 提升
iPhone 13 4820 780 84%
iPhone XR 5100 920 82%
iPhone XS Timeout / crash 1190 ✅ 可用

注意:所有数据都是从用户点击「支付」按钮开始计时,到 Apple Pay 界面完全渲染并可操作为止。不含网络验证时间(那是后端的事),只算前端可控部分。

以上是我个人对 Apple Pay 性能优化的完整实践总结,有更优的实现方式欢迎评论区交流

比如有人用 Service Worker 缓存 merchant session,或者用 WKWebView 的私有 API 强制预热 —— 我试过,不稳定,而且 App Store 审核风险高,就没上。现在这套方案简单、合规、有效,够用了。

后续我还会写一篇《Apple Pay 在微信内嵌页的降级策略》,因为 iOS 微信 WebView 对 Apple Pay 支持极差,但我们硬是搞出了兼容方案。感兴趣可以关注下。

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

暂无评论