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

Async/Await流程问题:加载reCAPTCHA脚本后grecaptcha为何有时未定义?

问题:调用refresh()时window.grecaptcha偶尔为undefined

调用refresh()方法时,有时会出现window.grecaptcha为undefined的情况,推测是网络延迟导致,调试难度较大,本人对异步概念较陌生,请求排查代码问题。

相关代码

class RecaptchaController {
  loadPromise;

  async refresh() {
    await this.load();
    // window.grecaptcha is (sometimes) undefined in next line
    this.element.value = window.grecaptcha.execute(this.options.siteKey, { action: 'submit' });
  }

  load() {
    if (!this.loadPromise) {
      this.context.logDebugActivity('load');

      this.loadPromise = new Promise((resolve, reject) => {
        const url = new URL(this.options.apiUrl);
        url.searchParams.append('render', this.options.siteKey);
        url.searchParams.append('hl', this.options.locale);

        const script = document.createElement('script');
        script.setAttribute('id', this.identifier);
        script.setAttribute('src', url.toString());

        // The relevant part where I'm resolving the promise
        script.addEventListener('load', resolve);
        script.addEventListener('error', reject);

        document.body.appendChild(script);
      })
    }

    return this.loadPromise;
  }
}

错误信息

recaptcha_controller.js:27 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'execute')

调用场景示例

form.addEventListener('submit', async (e) => {
   // This is NOT how the initialization works!
   // Just and example about how I'm calling refresh()
   cost recaptchaController = new RecaptchaController();

   // This is actually what is happening
   await recaptchaController.refresh();

   // Submit handling via fetch()...
});

问题原因

当前代码监听了script的load事件来resolve Promise,但这个事件仅代表脚本文件下载完成,不代表reCAPTCHA已经完成初始化并将grecaptcha挂载到window上。reCAPTCHA脚本内部的初始化是异步的,所以偶尔会出现脚本下载完但grecaptcha还未就绪的情况,导致调用execute时出错。

解决方案

修改load方法,利用reCAPTCHA官方提供的onload回调参数,确保只有当grecaptcha完全初始化完成后才resolve Promise:

class RecaptchaController {
  loadPromise;

  async refresh() {
    await this.load();
    this.element.value = window.grecaptcha.execute(this.options.siteKey, { action: 'submit' });
  }

  load() {
    // 优先检查grecaptcha是否已初始化,避免重复加载
    if (window.grecaptcha) {
      return Promise.resolve();
    }

    if (!this.loadPromise) {
      this.context.logDebugActivity('load');

      this.loadPromise = new Promise((resolve, reject) => {
        // 生成唯一回调名称,避免全局命名冲突
        const callbackName = `recaptchaCallback_${this.identifier}`;
        const url = new URL(this.options.apiUrl);
        
        url.searchParams.append('render', this.options.siteKey);
        url.searchParams.append('hl', this.options.locale);
        // 添加onload参数,指定初始化完成后的回调函数
        url.searchParams.append('onload', callbackName);

        // 注册全局回调:当grecaptcha就绪时触发
        window[callbackName] = () => {
          resolve();
          // 清理全局函数,避免内存泄漏
          delete window[callbackName];
        };

        const script = document.createElement('script');
        script.setAttribute('id', this.identifier);
        script.setAttribute('src', url.toString());

        // 监听脚本加载失败的情况,清理回调并reject
        script.addEventListener('error', (error) => {
          delete window[callbackName];
          reject(error);
        });

        document.body.appendChild(script);
      });
    }

    return this.loadPromise;
  }
}

关键改动说明

  1. 添加onload回调参数:reCAPTCHA脚本会在自身初始化完成后,自动调用你指定的全局回调函数,此时window.grecaptcha肯定已经存在。
  2. 唯一回调名称:用控制器的identifier拼接回调名称,避免多个实例或其他脚本的命名冲突。
  3. 清理全局回调:回调触发后立即删除全局函数,避免内存泄漏和重复调用。
  4. 前置检查grecaptcha状态:如果grecaptcha已经初始化完成,直接返回已resolve的Promise,无需重复加载脚本。

内容的提问来源于stack exchange,提问作者gremo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 06:24:33