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; } }
关键改动说明
- 添加
onload回调参数:reCAPTCHA脚本会在自身初始化完成后,自动调用你指定的全局回调函数,此时window.grecaptcha肯定已经存在。 - 唯一回调名称:用控制器的
identifier拼接回调名称,避免多个实例或其他脚本的命名冲突。 - 清理全局回调:回调触发后立即删除全局函数,避免内存泄漏和重复调用。
- 前置检查grecaptcha状态:如果grecaptcha已经初始化完成,直接返回已resolve的Promise,无需重复加载脚本。
内容的提问来源于stack exchange,提问作者gremo
相关产品推荐
相关产品推荐

