You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React新手求助:如何调用外部脚本中的sagepayOwnForm函数?

解决React中动态加载外部脚本并调用全局函数的问题

你的核心问题是现有useScript钩子未处理脚本加载完成的状态,也没有提供加载完成后的回调机制,导致无法确定何时可以安全调用sagepayOwnForm函数。下面是具体的实现方案:

1. 完善useScript钩子

先修改钩子,让它能返回脚本的加载状态(加载中/成功/失败),并支持按需触发加载:

import { useEffect, useState } from 'react';

export const useScript = (url: string, loadOnDemand = false) => {
  const [status, setStatus] = useState<'idle' | 'loading' | 'ready' | 'error'>('idle');

  useEffect(() => {
    // 未触发加载时直接返回
    if (!loadOnDemand) return;

    // 检查页面是否已存在该脚本,避免重复加载
    const existingScript = document.querySelector(`script[src="${url}"]`);
    if (existingScript) {
      setStatus('ready');
      return;
    }

    const script = document.createElement('script');
    script.src = url;
    script.async = true;
    setStatus('loading');

    // 脚本加载成功回调
    const handleLoad = () => setStatus('ready');
    // 脚本加载失败回调
    const handleError = () => {
      setStatus('error');
      document.body.removeChild(script);
    };

    script.addEventListener('load', handleLoad);
    script.addEventListener('error', handleError);

    document.body.appendChild(script);

    return () => {
      script.removeEventListener('load', handleLoad);
      script.removeEventListener('error', handleError);
      // 仅移除当前钩子创建的脚本
      if (!existingScript) document.body.removeChild(script);
    };
  }, [url, loadOnDemand]);

  return status;
};

2. 在React组件中实现需求

在支付表单组件中,结合状态管理实现点击提交时加载脚本并调用目标函数:

import { useState } from 'react';
import { useScript } from './useScript';

const SAGEPAY_SCRIPT_URL = 'https://pi-test.sagepay.com/api/v1/js/sagepay.js';

const PaymentForm = () => {
  // 用React状态管理表单数据,替代原生DOM查询
  const [cardDetails, setCardDetails] = useState({
    cardholderName: '',
    cardNumber: '',
    expiryDate: '',
    securityCode: '',
  });
  // 控制脚本是否开始加载
  const [triggerScriptLoad, setTriggerScriptLoad] = useState(false);
  // 获取脚本加载状态
  const scriptStatus = useScript(SAGEPAY_SCRIPT_URL, triggerScriptLoad);

  // 表单输入变更处理
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setCardDetails(prev => ({ ...prev, [name]: value }));
  };

  // 提交按钮点击逻辑
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    
    // 脚本未加载时触发加载
    if (scriptStatus === 'idle') {
      setTriggerScriptLoad(true);
      return;
    }

    // 脚本加载完成后调用目标函数
    if (scriptStatus === 'ready' && window.sagepayOwnForm) {
      window.sagepayOwnForm({
        merchantSessionKey: '5528D329-A83B-435F-B6A7-E38A2BC60DC9',
      }).tokeniseCardDetails({
        cardDetails,
        onTokenised: (result) => {
          if (result.success) {
            // 填充卡片标识并提交表单
            const form = document.querySelector('form');
            if (form) {
              const cardIdInput = form.querySelector('[name="card-identifier"]') as HTMLInputElement;
              if (cardIdInput) {
                cardIdInput.value = result.cardIdentifier;
                form.submit();
              }
            }
          } else {
            alert(JSON.stringify(result));
          }
        },
      });
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>持卡人姓名</label>
        <input
          type="text"
          name="cardholderName"
          data-card-details="cardholder-name"
          value={cardDetails.cardholderName}
          onChange={handleInputChange}
          required
        />
      </div>
      <div>
        <label>卡号</label>
        <input
          type="text"
          name="cardNumber"
          data-card-details="card-number"
          value={cardDetails.cardNumber}
          onChange={handleInputChange}
          required
        />
      </div>
      <div>
        <label>有效期</label>
        <input
          type="text"
          name="expiryDate"
          data-card-details="expiry-date"
          value={cardDetails.expiryDate}
          onChange={handleInputChange}
          required
        />
      </div>
      <div>
        <label>安全码</label>
        <input
          type="text"
          name="securityCode"
          data-card-details="security-code"
          value={cardDetails.securityCode}
          onChange={handleInputChange}
          required
        />
      </div>
      <input type="hidden" name="card-identifier" />
      <button type="submit" disabled={scriptStatus === 'loading'}>
        {scriptStatus === 'loading' ? '加载中...' : '提交支付'}
      </button>
    </form>
  );
};

export default PaymentForm;

3. 关键补充说明

  • TypeScript类型声明:由于TS默认不知道window上存在sagepayOwnForm,可以添加类型声明文件(如sagepay.d.ts):
    declare global {
      interface Window {
        sagepayOwnForm: (options: { merchantSessionKey: string }) => {
          tokeniseCardDetails: (params: {
            cardDetails: {
              cardholderName: string;
              cardNumber: string;
              expiryDate: string;
              securityCode: string;
            };
            onTokenised: (result: {
              success: boolean;
              cardIdentifier?: string;
              [key: string]: any;
            }) => void;
          }) => void;
        };
      }
    }
    
    export {};
    
  • 避免重复加载:钩子中加入了脚本存在性检查,防止用户多次点击导致重复加载。
  • 状态驱动逻辑:用React状态替代原生DOM操作,更符合React开发规范,代码可维护性更高。

内容的提问来源于stack exchange,提问作者hanushi-thana

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 05:35:21