Перейти к содержимому

Язык

Разработка4 мин чтения

Согласие на файлы cookie в одностраничных приложениях

Согласие на cookie в React, Vue и других SPA: общее состояние согласия, учет смены маршрутов, гидратация при SSR, блокировка встраиваний и мгновенный отзыв.

Редакция CookieshОпубликовано

В одностраничном приложении (SPA) страница никогда не перезагружается полностью, поэтому согласие на файлы 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», обычно в подвале сайта, которая снова открывает настройки без загрузки страницы. Когда посетитель отзывает согласие для категории:

  1. Обновите хранилище и сохраните новый выбор.
  2. Если вы используете теги Google, отправьте gtag('consent', 'update', …) с соответствующими сигналами в значении 'denied'.
  3. Прекратите вызывать функции отслеживания; код, зависящий от хранилища, делает это автоматически.
  4. Удалите собственные файлы cookie, установленные отозванной категорией, если вам известны их имена и домены. Ваши скрипты не могут удалить файлы cookie, установленные на других доменах.
  5. Перезагрузите страницу, если какой-либо из отозванных скриптов уже работает. Библиотеки хранят таймеры, обработчики событий и идентификаторы в памяти, и перезагрузка — единственный надежный способ от них избавиться.

Перезагрузка после отзыва согласия — разумный компромисс: это случается редко, а альтернатива — выполнять код, на запуск которого у вас больше нет разрешения.

Итоги

В SPA согласие — это состояние: по умолчанию — отказ, одно общее хранилище, скрипты и встраиваемый контент зависят от него, просмотры страниц отправляются только при наличии согласия, а отзыв вступает в силу сразу. Такие инструменты, как Cookiesh, могут взять на себя большую часть этой связующей работы: React SDK предоставляет CookieshProvider и хук useConsent(), а для всего остального есть событие consent:updated.

Эта статья носит общий информационный характер и не является юридической консультацией. Для решений, касающихся вашей конкретной ситуации, обратитесь к квалифицированному специалисту по защите данных.

Частые вопросы

Нужно ли снова показывать баннер cookie после каждой смены маршрута?

Нет. Показывайте его, пока посетитель не сделает выбор, сохраните этот выбор и оставьте постоянную ссылку на настройки. Спрашивайте снова, только если ваши цели или поставщики существенно изменились или срок действия сохраненного выбора истек.

Можно ли хранить выбор согласия в localStorage, а не в файле cookie?

В браузере работают оба варианта, но файл cookie еще и отправляется на ваш сервер с каждым запросом, поэтому страницы с серверным рендерингом могут показать правильное состояние уже при первой отрисовке. localStorage доступен только клиентскому коду после загрузки приложения.

Обрабатывают ли согласие за меня компоненты скриптов во фреймворках, например next/script?

Нет. Такие вспомогательные средства, как компонент Script в Next.js, управляют тем, когда скрипт загружается относительно гидратации, а не тем, согласился ли на него посетитель. Отрисовывайте их в зависимости от состояния согласия, чтобы они не добавлялись на страницу до того, как посетитель даст согласие.

Источники

  1. Directive 2002/58/EC (ePrivacy Directive), EUR-Lex
  2. Regulation (EU) 2016/679 (GDPR), EUR-Lex
  3. Google: обзор режима согласия

Редакция Cookiesh

Инженеры по защите данных и специалисты по комплаенсу пишут практические руководства о согласии, файлах cookie и защите данных для продуктовых команд.

Запускайте баннеры cookie по всем правилам — за минуты, а не за спринты

Начните бесплатно с одним доменом. Переходите на платный тариф, когда понадобятся дополнительные домены, геотаргетинг или собственный брендинг.

Без привязки карты · Отмена в любой момент