Rails 7 Turbo 环境集成 reCAPTCHA 的方法
最近有人反馈 GeekNote 的注册流程体验很差(#23),我调试之后发现之前集成 reCAPTCHA 的代码有错,会导致验证经常失败。解决的过程记录如下。
问题
reCAPTCHA 是 Google 提供的验证码服务,Rails 有一个 Gem recaptcha 可以帮助集成。按照这个 Gem 的文档,在需要验证码的地方只需加入以下代码:
<%= form_for @foo do |f| %>
# …
<%= recaptcha_tags %>
# …
<% end %>
recaptcha_tags
插入的内容如下:
<script src="https://www.recaptcha.net/recaptcha/api.js" async defer></script>
<div class="g-recaptcha" data-sitekey="<%= ENV['RECAPTCHA_SITE_KEY'] %>"></div>
<noscript>
...
<textarea name="g-recaptcha-response"></textarea>
</noscirpt>
在访问页面时,api.js
会在 <head>
插入另一段 <script>
,这个 <script>
才是真正执行代码。在没有 Turbo 时它可以正常工作,但是在 Turbo 开启的情况下会发生以下错误:
- 由于 Turbo 的页面间
<head>
内容会被保留,导致多次访问有验证码的页面时产生重复执行。 - 由于代码重复执行,过程会抛出错误,
<noscript>
的内容没有被正确处理,导致提交空的g-recaptcha-response
参数。 - 空的
g-recaptcha-response
参数导致验证失败。
以上错误已经提交了 Issues(#47),但是还没被官方解决。
那么要如何正确集成 reCAPTCHA 呢?
解决方案
首先要解决重复执行的问题,由于 Turbo 环境 <head>
会被保留,所以不希望重复执行的代码应该放到 <head>
里。
在 layouts/application.html.erb
加入以下代码:
<head>
...
<script src="https://www.recaptcha.net/recaptcha/api.js" async defer></script>
</head>
提示:如果想选择性载入 reCAPTCHA,可以用
yield :head
和content_for :head
的组合仅在需要的页面插入 script。
接着,在需要验证码的地方加入以下内容:
<%= form_for @foo do |f| %>
# …
<div class="g-recaptcha" data-sitekey="<%= ENV['RECAPTCHA_SITE_KEY'] %>" data-controller="recaptcha"></div>
# …
<% end %>
如果这个页面是访问的首个页面,recaptcha/api.js
在加载后会自动查找包含 .g-recaptcha
的元素进行渲染。但如果是通过 Turbo 访问的页面,recaptcha/api.js
的渲染逻辑不会执行,所以这里需要一个 Stimulus 控制器进行处理。
在 Stimulus 控制器目录创建以下代码:
// recaptcha_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
// Skip this if grecaptcha has not been loaded or has already been rendered.
if (window.grecaptcha && window.grecaptcha.render && this.element.childElementCount == 0) {
grecaptcha.render(this.element, {
'sitekey' : this.element.dataset.sitekey
})
}
}
}
该控制器判断,如果 recaptcha api 已经加载,并且元素本身还没被 recaptcha 渲染,则调用 grecaptcha.render
进行渲染。这样它就能处理 Turbo 环境下验证码的初始化。
经过以上修改后,不再出现 api.js
重复加载,也不会出现提交空的 g-recaptcha-response
参数的情况,问题解决。