PAYATHON 2026

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 的接口说明为准。

备注:内容仅供参考。