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
相关产品推荐
相关产品推荐

