В современных веб-приложениях часто используется динамический контент, в частности, модальные окна Bootstrap. Если форма внутри такого окна содержит Google reCAPTCHA, возникает дилемма: либо загружать тяжелый скрипт капчи при старте страницы (влияя на скорость загрузки), либо загружать его по требованию (ленивая загрузка).
При ленивой загрузке ключевая проблема — избежать ошибки **"reCAPTCHA has already been rendered in this element"** при повторном показе окна. В этой статье мы рассмотрим, как интегрировать ленивую загрузку скрипта reCAPTCHA, используя нативные события Bootstrap, и как безопасно рендерить виджет только в открывшемся динамическом модальном окне.
Проблема: Производительность и повторный рендеринг
Скрипт Google reCAPTCHA является внешним и может замедлять начальную загрузку страницы. Если модальное окно с формой открывается редко, логично загружать этот скрипт только в момент необходимости. Однако, если мы просто добавим тег `<script>` в обработчик открытия окна, мы столкнемся с двумя сложностями:
- Скрипт может загружаться повторно при каждом открытии модального окна.
- Если скрипт уже был загружен, повторный вызов `grecaptcha.render()` для того же ID элемента вызовет фатальную ошибку.
Решение: Ленивая загрузка через событие Bootstrap
Идеальное решение заключается в следующем:
- Загружать внешний скрипт reCAPTCHA только один раз за сессию, используя глобальный флаг.
- Использовать событие Bootstrap `shown.bs.modal`, которое срабатывает, когда окно **полностью видимо**.
- Использовать делегирование событий на `document`, чтобы перехватывать событие от **любого** модального окна.
- При закрытии окна очищать DOM-элементы, созданные reCAPTCHA, чтобы разрешить повторный рендеринг при следующем показе.
Пример реализации
Для реализации нам потребуются три основные части: управление состоянием загрузки, функция загрузки скрипта и функция рендеринга с защитой от повтора.
// Переменная, чтобы знать, загружен ли уже API
let isRecaptchaApiLoaded = false;
function loadRecaptchaScript(callback) {
if (isRecaptchaApiLoaded) {
// Если скрипт уже загружен, просто вызываем колбэк
callback();
return;
}
const scriptUrl = "//www.google.com/recaptcha/api.js?onload=handleRecaptchaLoad&hl=ru";
const script = document.createElement('script');
script.src = scriptUrl;
script.async = true;
script.defer = true;
// Устанавливаем глобальную функцию, которую вызовет Google после загрузки
window.handleRecaptchaLoad = function() {
isRecaptchaAfpiLoaded = true;
callback();
};
document.head.appendChild(script);
}
function renderSpecificRecaptcha(containerId) {
const container = $(containerId); // Или используйте document.getElementById(containerId.substring(1))
if (container.length === 0) {
console.error("Контейнер не найден:", containerId);
return;
}
const captchaSelector = '.g-recaptcha_webalan';
// Проверяем, есть ли элемент, который мы будем рендерить
if (container.find(captchaSelector).length === 0) {
return; // Нет нужной разметки в этом контейнере
}
const kap = container.find(captchaSelector);
const ids = kap.attr('id');
const sitekey = kap.data('sitekey');
// ----------------------------------------------------
// КЛЮЧЕВАЯ ЗАЩИТА ОТ ПОВТОРНОГО РЕНДЕРИНГА:
// Проверяем, содержит ли этот конкретный div уже класс g-recaptcha
// или какой-либо iframe, созданный Google.
if (kap.find('.g-recaptcha').length > 0 || kap.find('iframe').length > 0) {
console.warn('reCAPTCHA уже отрисован в:', ids, '. Пропускаем.');
return;
}
// ----------------------------------------------------
if (!ids || !sitekey) {
console.error("Не хватает ID или Sitekey для рендеринга.");
return;
}
// Рендеринг
grecaptcha.render(ids, {
'sitekey': sitekey,
// ... другие опции
});
console.log(`reCAPTCHA успешно отрендерена в: ${ids}`);
}
$(document).on('shown.bs.modal', function (e) {
// e.target содержит DOM-элемент самого модального окна, которое только что показалось
const $modalElement = $(e.target);
if ($modalElement.find('.g-recaptcha_webalan').length > 0) {
var id = "#"+$modalElement.attr('id');
// 1. Загружаем скрипт (он загрузится только один раз)
loadRecaptchaScript(function() {
// 2. Рендерим капчу внутри ТОЛЬКО что показанного элемента
renderSpecificRecaptcha(id);
});
}
});
// вызов рекапчи
loadRecaptchaScript(function() {
renderSpecificRecaptcha(id);
});
Разбор ключевых моментов
Для успешной реализации необходимо понимать роль каждого компонента:
- Ленивая загрузка скрипта: Функция
loadRecaptchaScriptдобавляет тег `<script>` только один раз, используя `window.handleRecaptchaLoad` как колбэк, который вызывается самой Google после готовности API. - Событие
shown.bs.modal: Это событие гарантирует, что мы пытаемся рендерить виджет только тогда, когда модальное окно стало полностью видимым. - Делегирование на
$(document): Позволяет отслеживать события любых модальных окон, независимо от их ID. - Защита от повтора: Проверка
kap.find('.g-recaptcha').length > 0вrenderSpecificRecaptcha— это решающий фактор, предотвращающий ошибку "already been rendered", даже если обработчик показа срабатывает повторно.
HTML-разметка для контейнера
HTML-структура внутри любого модального окна должна содержать контейнер с нужным ID и ключом:
<div class="modal fade" id="dynamicModal1" tabindex="-1" role="dialog">
<div class="modal-body">
<!-- Этот контейнер будет найден JS-кодом -->
<div class="g-recaptcha_webalan"
id="modal_captcha_123"
data-sitekey="ВАШ_ПУБЛИЧНЫЙ_SITEKEY_ЗДЕСЬ">
</div>
</div>
</div>
Заключение
Использование событий Bootstrap в сочетании с ленивой загрузкой и проверкой состояния API позволяет значительно улучшить производительность загрузки страницы, отложив инициализацию reCAPTCHA до момента, когда она действительно потребуется пользователю в динамически открытом модальном окне. При этом использование делегированных событий обеспечивает гибкость для любых модальных окон в приложении.