Разработка4 мин чтения
Согласие на файлы cookie в одностраничных приложениях
Согласие на cookie в React, Vue и других SPA: общее состояние согласия, учет смены маршрутов, гидратация при SSR, блокировка встраиваний и мгновенный отзыв.
В одностраничном приложении (SPA) страница никогда не перезагружается полностью, поэтому согласие на файлы cookie должно храниться в состоянии приложения, а не в скрипте, который выполняется один раз. Сохраняйте выбор посетителя, делитесь им со всеми компонентами, включайте или блокируйте скрипты при его изменении, отправляйте просмотры страниц при смене маршрута только при наличии согласия и делайте так, чтобы отзыв согласия вступал в силу сразу, а не при следующей загрузке страницы.
Почему в одностраничном приложении с согласием на файлы cookie сложнее?
Большинство схем работы с согласием рассчитаны на полную загрузку страниц, а SPA загружается один раз и затем переключает представления с помощью JavaScript. Это ломает четыре допущения:
- Скрипты загружаются один раз. Если аналитика была заблокирована при загрузке, а посетитель согласился позже, никакая загрузка страницы ее не подхватит. Включать ее придется на месте.
- Просмотры страниц виртуальные. Смена маршрута не перезагружает страницу, поэтому о просмотрах должен сообщать роутер — и сначала он должен проверять согласие.
- Выбор меняется посреди сеанса. Посетитель может согласиться, походить по сайту, а затем отозвать согласие по ссылке в подвале, когда скрипты уже работают в памяти.
- Серверный рендеринг добавляет гидратацию. Во фреймворках вроде Next.js или Nuxt HTML с сервера должен совпадать с первой отрисовкой на клиенте, но сервер может не знать, какой выбор сделал посетитель.
Как моделировать состояние согласия в SPA?
Рассматривайте согласие как небольшое хранилище, из которого читают все части приложения, с методом подписки, чтобы компоненты и скрипты реагировали на изменения. По умолчанию устанавливайте для каждой необязательной категории отказ, чтобы код, выполняющийся до загрузки сохраненного выбора, никогда не мог допустить утечку данных.
// consent-store.ts
export type Category = 'necessary' | 'preferences' | 'analytics' | 'marketing';
export type Consent = Record<Category, boolean>;
export const DENIED: Consent = {
necessary: true, preferences: false, analytics: false, marketing: false,
};
let state: Consent = DENIED;
const listeners = new Set<() => void>();
export const consentStore = {
get: (): Consent => state,
set(next: Partial<Consent>) {
state = { ...state, ...next, necessary: true };
listeners.forEach((notify) => notify());
},
subscribe(listener: () => void) {
listeners.add(listener);
return () => { listeners.delete(listener); };
},
};
В реальном приложении set также сохранял бы выбор в собственном (first-party) файле cookie, записывал бы его в журнал согласий и обновлял бы Google Consent Mode с помощью gtag('consent', 'update', …). В React хук useSyncExternalStore связывает хранилище с компонентами, и тот же хук может блокировать встраиваемый контент, который устанавливает файлы cookie:
// consent.tsx
import { useSyncExternalStore } from 'react';
import { consentStore, DENIED } from './consent-store';
export function useConsent() {
return useSyncExternalStore(consentStore.subscribe, consentStore.get, () => DENIED);
}
export function VideoEmbed({ videoId }: { videoId: string }) {
const { marketing } = useConsent();
if (!marketing) {
return (
<div className="embed-placeholder">
<p>Это видео размещено на YouTube, который может устанавливать маркетинговые файлы cookie.</p>
<button type="button" onClick={() => consentStore.set({ marketing: true })}>
Разрешить маркетинговые файлы cookie и воспроизвести
</button>
</div>
);
}
return (
<iframe
src={`https://www.youtube-nocookie.com/embed/${videoId}`}
title="Видео о продукте"
allowFullScreen
/>
);
}
Третий аргумент useSyncExternalStore — это снимок состояния, который используется при серверном рендеринге и гидратации. Если возвращать там DENIED, сервер и первая отрисовка на клиенте совпадут, а сразу после гидратации React перерисует компонент с реальным состоянием. Во Vue ту же роль играет composable-функция, оборачивающая реактивный ref.
На скрипты, которые не являются компонентами, подписывайтесь напрямую через хранилище и загружайте их динамическим import(), как только для их категории дано согласие. Схемы активации описаны в нашем руководстве о том, как блокировать скрипты до получения согласия.
Как отслеживать смену маршрутов только при наличии согласия?
Отправляйте виртуальный просмотр страницы из роутера после каждого перехода и проверяйте согласие в этот момент. Если посетитель дает согласие на аналитику посреди сеанса, разумно зафиксировать страницу, на которой он находится, ведь именно с нее начинается действие согласия.
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useConsent } from './consent';
import { trackPageView } from './analytics';
export function PageViewTracker() {
const { pathname } = useLocation();
const { analytics } = useConsent();
useEffect(() => {
if (analytics) trackPageView(pathname);
}, [pathname, analytics]);
return null;
}
Следите за двойным учетом. Некоторые инструменты аналитики умеют автоматически отправлять просмотры страниц при изменениях истории браузера; в Google Analytics 4 это доступно как параметр улучшенной статистики. Используйте либо автоматический вариант, либо собственный трекер, но не оба сразу.
Как избежать несоответствий при гидратации и мерцания баннера?
Решите, где выбор посетителя будет известен при первой отрисовке. Есть два надежных варианта:
- Считывайте файл cookie согласия на сервере. Файлы cookie отправляются с каждым запросом, поэтому страницы с серверным рендерингом могут корректно показать или скрыть баннер с первого же байта. Это работает, только если HTML не кешируется и не используется совместно разными посетителями.
- Отрисовывайте баннер только на клиенте. Для статически сгенерированных или кешируемых в CDN страниц ничего не отрисовывайте на сервере и показывайте баннер после монтирования приложения. Размещайте его как фиксированный оверлей, чтобы позднее появление не вызывало сдвига макета; см. статью баннеры cookie и Core Web Vitals.
В любом случае файл cookie или запись в хранилище, запоминающая выбор, как правило, считается необходимой для соблюдения этого выбора; см. статью какие файлы cookie считаются строго необходимыми.
Как обрабатывать отзыв согласия посреди сеанса?
Сделайте отзыв мгновенным и таким же простым, как предоставление согласия, — этого требует статья 7(3) Общего регламента по защите данных (GDPR). На практике это означает постоянную ссылку «Настройки cookie», обычно в подвале сайта, которая снова открывает настройки без загрузки страницы. Когда посетитель отзывает согласие для категории:
- Обновите хранилище и сохраните новый выбор.
- Если вы используете теги Google, отправьте
gtag('consent', 'update', …)с соответствующими сигналами в значении'denied'. - Прекратите вызывать функции отслеживания; код, зависящий от хранилища, делает это автоматически.
- Удалите собственные файлы cookie, установленные отозванной категорией, если вам известны их имена и домены. Ваши скрипты не могут удалить файлы cookie, установленные на других доменах.
- Перезагрузите страницу, если какой-либо из отозванных скриптов уже работает. Библиотеки хранят таймеры, обработчики событий и идентификаторы в памяти, и перезагрузка — единственный надежный способ от них избавиться.
Перезагрузка после отзыва согласия — разумный компромисс: это случается редко, а альтернатива — выполнять код, на запуск которого у вас больше нет разрешения.
Итоги
В SPA согласие — это состояние: по умолчанию — отказ, одно общее хранилище, скрипты и встраиваемый контент зависят от него, просмотры страниц отправляются только при наличии согласия, а отзыв вступает в силу сразу. Такие инструменты, как Cookiesh, могут взять на себя большую часть этой связующей работы: React SDK предоставляет CookieshProvider и хук useConsent(), а для всего остального есть событие consent:updated.
Эта статья носит общий информационный характер и не является юридической консультацией. Для решений, касающихся вашей конкретной ситуации, обратитесь к квалифицированному специалисту по защите данных.
Частые вопросы
Нужно ли снова показывать баннер cookie после каждой смены маршрута?
Нет. Показывайте его, пока посетитель не сделает выбор, сохраните этот выбор и оставьте постоянную ссылку на настройки. Спрашивайте снова, только если ваши цели или поставщики существенно изменились или срок действия сохраненного выбора истек.
Можно ли хранить выбор согласия в localStorage, а не в файле cookie?
В браузере работают оба варианта, но файл cookie еще и отправляется на ваш сервер с каждым запросом, поэтому страницы с серверным рендерингом могут показать правильное состояние уже при первой отрисовке. localStorage доступен только клиентскому коду после загрузки приложения.
Обрабатывают ли согласие за меня компоненты скриптов во фреймворках, например next/script?
Нет. Такие вспомогательные средства, как компонент Script в Next.js, управляют тем, когда скрипт загружается относительно гидратации, а не тем, согласился ли на него посетитель. Отрисовывайте их в зависимости от состояния согласия, чтобы они не добавлялись на страницу до того, как посетитель даст согласие.
Источники
Редакция Cookiesh
Инженеры по защите данных и специалисты по комплаенсу пишут практические руководства о согласии, файлах cookie и защите данных для продуктовых команд.