JS 在 console 中有效,但在 React App 中无法创建表单
结论
问题通常不在于 React 能不能操作 DOM,而是 console、普通 HTML 与 React 的执行环境不同:
- React 中声明的
var iyziInit处于模块或组件作用域,不会自动成为window.iyziInit。如果第三方bundle.js从全局对象读取配置,就找不到这个变量。 - 如果代码在组件渲染期间执行,外部脚本开始加载时,
#iyzipay-checkout-form可能还没有挂载到 DOM。
解决办法是把相关代码放进组件挂载后的 useEffect:先将配置显式赋给 window.iyziInit,确认目标容器已经存在,再加载第三方脚本。
为什么在 console 和普通 HTML 中有效
在浏览器 console 或传统的非模块 <script> 中声明:
var iyziInit = {};
第三方脚本通常可以把它当作全局变量访问,其效果接近:
window.iyziInit = {};
React 项目通常会经过 webpack、Vite 等工具打包,并以 ES Module 方式运行。组件文件中的变量会被限制在模块作用域内:
var iyziInit = {};
React 代码可以访问这个 iyziInit,但之后加载的远程 bundle.js 不一定能访问。
组件函数开始执行,也不代表对应元素已经写入真实 DOM。如果直接在组件函数中加载脚本,执行顺序可能是:
执行组件函数
→ 添加第三方 script
→ 第三方脚本开始运行
→ React 才把支付表单容器挂载到 DOM
而在 console 中执行代码时,页面通常已经渲染完成,目标 div 也已经存在。
React 中的正确加载方式
可以把支付配置和脚本加载逻辑放进 useEffect:
import { useEffect } from "react";
export default function CheckoutForm() {
useEffect(() => {
const container = document.getElementById("iyzipay-checkout-form");
if (!container) {
console.error("iyzipay-checkout-form container was not found");
return;
}
window.iyziInit = {
currency: "TRY",
token: "23bfa27a-172a-493d-b42f-8b091589eaa0",
price: 2.4,
locale: "tr",
baseUrl: "https://sandbox-api.iyzipay.com",
merchantGatewayBaseUrl:
"https://sandbox-merchantgw.iyzipay.com",
registerCardEnabled: true,
bkmEnabled: true,
bankTransferEnabled: false,
bankTransferRedirectUrl: "https://localhost:3000/callback",
bankTransferCustomUIProps: {},
campaignEnabled: false,
creditCardEnabled: true,
bankTransferAccounts: [],
userCards: [],
fundEnabled: true,
memberCheckoutOtpData: {},
force3Ds: false,
isSandbox: true,
storeNewCardEnabled: true,
paymentWithNewCardEnabled: true,
enabledApmTypes: ["SOFORT", "IDEAL", "QIWI", "GIROPAY"],
payWithIyzicoUsed: false,
payWithIyzicoEnabled: true,
payWithIyzicoCustomUI: {},
buyerName: "John",
buyerSurname: "Doe",
merchantInfo: "",
cancelUrl: "",
buyerProtectionEnabled: false,
hide3DS: false,
gsmNumber: "",
email: "email@email.com",
checkConsumerDetail: {},
subscriptionPaymentEnabled: false,
ucsEnabled: false,
fingerprintEnabled: false,
payWithIyzicoFirstTab: false,
metadata: {}
};
const script = document.createElement("script");
script.src =
"https://sandbox-static.iyzipay.com/checkoutform/v2/bundle.js?v=1634688058781";
script.async = true;
script.dataset.iyzicoCheckout = "true";
script.onerror = () => {
console.error("Failed to load iyzico checkout script");
};
document.head.appendChild(script);
return () => {
script.remove();
container.replaceChildren();
if (window.iyziInit) {
delete window.iyziInit;
}
};
}, []);
return <div id="iyzipay-checkout-form" className="responsive" />;
}
其中:
window.iyziInit = { ... };
把初始化配置放到全局对象上,远程脚本就能读取它。
useEffect(() => {
// ...
}, []);
让代码在组件挂载到真实 DOM 后运行。
<div id="iyzipay-checkout-form" />
为第三方脚本提供指定 ID 的容器。
如果 API 返回的是完整脚本文本
不要通过 JSX 或 dangerouslySetInnerHTML 直接插入 <script>:
<div dangerouslySetInnerHTML={{ __html: scriptText }} />
浏览器通常不会执行通过 innerHTML 插入的 <script>。应先解析初始化数据并赋值给 window.iyziInit,再通过 document.createElement("script") 加载外部资源。
如果后端只能返回完整的 JavaScript 字符串,最好调整接口,改为返回结构化配置,例如:
{
"currency": "TRY",
"token": "...",
"price": 2.4,
"locale": "tr"
}
前端拿到配置后再执行:
window.iyziInit = checkoutConfig;
不建议使用 eval() 执行接口返回的脚本文本。这样做有明显的安全风险,错误处理和生命周期管理也会更麻烦。
React Strict Mode 的影响
在 React 18 开发环境中,StrictMode 可能会执行一次”挂载、清理、再次挂载”,以检查副作用是否安全。这可能让第三方脚本加载两次。
加载前可以先检查并移除已有脚本:
const oldScript = document.querySelector(
'script[data-iyzico-checkout="true"]'
);
if (oldScript) {
oldScript.remove();
}
但删除 <script> 未必能清除第三方脚本已经注册的事件监听器或全局状态。如果支付 SDK 提供销毁方法,应优先调用官方销毁 API。暂时关闭 StrictMode 可以帮助定位问题,但不能作为正式修复方案。
其他注意事项
支付表单最好使用一个空容器,让 React 不去管理它的内部子节点:
<div id="iyzipay-checkout-form" />
不要同时让 React 向这个 div 渲染其他内容。否则,组件重新渲染或重新挂载时,第三方脚本插入的节点可能会被清除。
还应检查浏览器 console 和 Network 面板,排查以下情况:
iyziInit is not defined- 第三方脚本被 Content Security Policy 拦截
- 脚本请求失败
- 支付 token 已过期或已经使用
- 页面中存在重复的
iyzipay-checkout-formID - 路由切换导致组件卸载后重新挂载
- 广告拦截器或隐私扩展阻止支付脚本
生产环境中,不要把支付凭据或可复用的敏感配置硬编码在前端。支付初始化信息和 token 应由服务端按照支付网关的要求生成,再交给前端渲染表单。
备注:内容仅供参考。
