React 中集成 Payment Module 时出现 Syntax error
结论
问题不在 widget.js 本身,而在于 <script> 标签中的 JavaScript 被当成了普通 JSX 内容。对象字面量 {language: 'de'} 会被 JSX 识别为表达式,因此触发 Unexpected token, expected "}"。
在 React 中,可以通过 useEffect 动态加载 widget.js,等脚本的 load 事件触发后再调用:
window.rnw.tamaro.runWidget(...)
这样可以避开 JSX 语法错误,并确保调用时脚本已经加载完成。
推荐实现
import React, { useEffect, useRef } from "react"
import Layout from "../components/Layout"
const WIDGET_SCRIPT_ID = "tamaro-widget-script"
const WIDGET_SCRIPT_URL =
"https://tamaro.raisenow.com/xxx/latest/widget.js"
const SpendenPage = () => {
const initialized = useRef(false)
useEffect(() => {
const runWidget = () => {
if (
initialized.current ||
!window.rnw ||
!window.rnw.tamaro ||
typeof window.rnw.tamaro.runWidget !== "function"
) {
return
}
window.rnw.tamaro.runWidget(".dds-widget-container", {
language: "de",
})
initialized.current = true
}
// 脚本可能已被页面中的其他组件加载。
if (
window.rnw &&
window.rnw.tamaro &&
typeof window.rnw.tamaro.runWidget === "function"
) {
runWidget()
return
}
let script = document.getElementById(WIDGET_SCRIPT_ID)
if (!script) {
script = document.createElement("script")
script.id = WIDGET_SCRIPT_ID
script.src = WIDGET_SCRIPT_URL
script.async = true
document.body.appendChild(script)
}
script.addEventListener("load", runWidget)
return () => {
script.removeEventListener("load", runWidget)
}
}, [])
return (
<Layout>
<div className="dds-widget-container" />
</Layout>
)
}
export default SpendenPage
请把 URL 中的 xxx 替换为 RaiseNow 提供的实际标识。
为什么原代码会报错
原代码中有这样一段:
<script>
window.rnw.tamaro.runWidget('.dds-widget-container', {language: 'de'});
</script>
JSX 不会像浏览器处理普通 HTML 那样执行 <script> 标签里的文本。JSX 还会使用 {} 嵌入 JavaScript 表达式,所以这里的:
{language: 'de'}
会干扰 JSX 的语法解析,导致构建阶段直接报错。
即使用字符串或 dangerouslySetInnerHTML 绕过了语法错误,加载顺序仍可能出问题。外部 widget.js 可能还没下载完成,内联代码就开始访问 window.rnw,随后出现类似错误:
Cannot read properties of undefined
因此,初始化代码应放在外部脚本的 load 回调中。
如果仍然希望使用 React Helmet
可以通过 onLoad 等待外部脚本加载完成:
import React from "react"
import { Helmet } from "react-helmet"
import Layout from "../components/Layout"
const SpendenPage = () => {
const initializeWidget = () => {
if (
window.rnw &&
window.rnw.tamaro &&
typeof window.rnw.tamaro.runWidget === "function"
) {
window.rnw.tamaro.runWidget(".dds-widget-container", {
language: "de",
})
}
}
return (
<Layout>
<Helmet>
<script
src="https://tamaro.raisenow.com/xxx/latest/widget.js"
onLoad={initializeWidget}
/>
</Helmet>
<div className="dds-widget-container" />
</Layout>
)
}
export default SpendenPage
不过,脚本是否已经存在、页面切换时会不会重复初始化,以及 react-helmet 如何处理事件属性,都可能受到项目版本和渲染方式的影响。对于需要在客户端初始化的第三方 widget,useEffect 通常更方便控制。
注意事项
1. 容器选择器必须一致
初始化代码使用的是:
".dds-widget-container"
页面中的容器也必须是:
<div className="dds-widget-container" />
如果改用官方示例中的 .rnw-widget-container,两处都要修改。
2. 不要在组件渲染期间直接访问 window
如果项目使用 Gatsby、Next.js 或其他服务端渲染方案,构建期间并不存在 window。useEffect 只会在浏览器端运行,可以避免下面的错误:
window is not defined
3. 必须等待脚本加载完成
下面的写法存在竞态条件,并不可靠:
document.body.appendChild(script)
window.rnw.tamaro.runWidget(...)
把 <script> 添加到页面中,不代表脚本已经加载完成。必须监听 load 事件或设置 script.onload。
4. 避免重复加载和初始化
React 开发模式、客户端路由切换和组件重新挂载,都可能让初始化逻辑执行多次。示例使用固定的脚本 id 避免重复插入脚本,并通过 useRef 防止同一次挂载中重复调用。
如果 widget 提供销毁或卸载 API,还应在 useEffect 的清理函数中调用。具体用法请以当前 RaiseNow/Tamaro SDK 的接口说明为准。
备注:内容仅供参考。
