Как я создал расширение Chrome за 6 месяцев с Vue 3 и Shadow DOM

Сегодня разбираем материал dev.to о теме «Я потратил 6 месяцев на создание расширения Chrome с Vue 3 и Shadow DOM». Материал полезен тем, кто хочет быстро понять суть темы и перевести идеи в прикладные действия.


Chrome с Vue 3 и Shadow DOM — вот что оказалось по-настоящему сложным

В разработке расширение выглядит идеально. Шрифты чёткие, макет чистый, утилиты Tailwind работают именно так, как ожидается.

Стили на хост-странице нарушали мой тщательно проработанный интерфейс, и в результате text-sm выглядел не так, как должен. Flex-контейнеры сжимались. Я добавил !important к нескольким правилам, потом к десяткам, и вскоре осознал, что веду бесполезную войну.

Процесс создания расширения занял полгода, и он был полон вызовов. Это расширение должно работать на множестве платформ, каждая из которых имеет свои уникальные CSS-условия, что могло разрушить интерфейс. Я расскажу, как я это сделал.

Подходы, которые не сработали

Прежде чем найти подходящее решение, я протестировал три распространённых метода: scoped CSS, цепочки с !important и изоляция через iframe. Несмотря на сложности, мне удалось найти решение.

Shadow DOM и adoptedStyleSheets

Shadow DOM создаёт жёсткую границу. CSS хост-страницы не может проникнуть в shadow root (теневой корень). Ваш CSS не может выйти наружу. А с помощью adoptedStyleSheets можно загружать таблицы стилей синхронно — без мерцания неоформленного контента, без инъекции тега <style>.

Основной вспомогательный код прост:

// Общий пример: создание CSSStyleSheet для Shadow DOM
function createSheet(css: string): CSSStyleSheet { const sheet = new CSSStyleSheet(); sheet.replaceSync(css); return sheet;
}

В сборке Vite можно импортировать CSS-файлы с суффиксом ?inline — это даёт CSS в виде сырой строки вместо инъекции в head документа. Такой подход идеально сочетается с adoptedStyleSheets: вы получаете обработку CSS на этапе сборки (PostCSS, Tailwind, Sass) и изоляцию через Shadow DOM во время выполнения.

Как работает паттерн монтирования

Общий подход к встраиванию приложения фреймворка внутрь Shadow DOM следует такому паттерну:

// Общий пример: монтирование приложения фреймворка внутри Shadow DOM
function injectExtensionUI() { // 1. Создаём хост-элемент в light DOM const host = document.createElement('div'); host.style.cssText = 'position:fixed;top:0;right:0;z-index:2147483647;' + 'width:0;height:0;overflow:visible;pointer-events:none;'; document.body.appendChild(host);
// 2. Прикрепляем shadow root const shadow = host.attachShadow({ mode: 'open' });
// 3. Загружаем таблицы стилей в правильном порядке каскада shadow.adoptedStyleSheets = [ createSheet(resetCSS), createSheet(utilityCSS), createSheet(componentCSS), createSheet(themeCSS), ];
// 4. Создаём точку монтирования и передаём её фреймворку const mountEl = document.createElement('div'); mountEl.style.pointerEvents = 'auto'; shadow.appendChild(mountEl);
// 5. Монтируем фреймворк (Vue, React, Svelte и т.д.) const app = createApp(RootComponent); app.mount(mountEl);
}

Это не зависит от конкретного фреймворка. Настройка Shadow DOM одинакова независимо от того, используете ли вы Vue, React, Svelte или ванильный JS. Специфичная для фреймворка часть — только последние несколько строк.

Детали, которые удалось выяснить только в процессе отладки

Некоторые из этих нюансов я обнаружил не из документации, а из часов отладки на живых сайтах.

z-index: 2147483647. Это максимальное 32-битное знаковое целое число. Оно гарантирует, что панель расширения располагается поверх всех элементов хост-страницы, включая модальные окна и фиксированные заголовки.

pointer-events: none на хосте, auto на точке монтирования. Хост-элемент перекрывает угол вьюпорта, но клики проходят сквозь него к странице ниже. Только реальный интерфейс расширения перехватывает события мыши. Без этого пользователи не смогут кликать на элементы страницы рядом с панелью расширения.

width:0; height:0; overflow:visible. Хост-элемент не занимает места в макете. Интерфейс расширения визуально выходит за его границы, но не сдвигает содержимое страницы.

Защита от двойной инъекции. Контент-скрипты могут перезагружаться при обновлении Chrome или при навигации внутри SPA (одностраничного приложения). Без проверки-защиты — например, проверки наличия существующего хост-элемента по ID — панели будут дублироваться и накладываться друг на друга.

Порядок CSS имеет значение

Порядок таблиц стилей в adoptedStyleSheets не произвольный. Более поздние таблицы имеют более высокую специфичность, следуя стандартному каскаду CSS:

  • Reset CSS — нормализует shadow root: box-sizing, настройки шрифтов по умолчанию
  • Utility CSS (например, Tailwind) — утилитарные классы как базовый слой
  • Component CSS — макет панели, стили компонентов
  • Theme CSS — цвета, переменные тёмного режима, переопределения темы

Если поставить тему перед стилями компонентов, переменные темы будут перекрыты более специфичными селекторами. Если поставить утилиты после компонентов, утилитарные классы переопределят кастомный макет. Порядок такой: нормализация, затем утилиты, затем компоненты, затем тема.

Поддержка нескольких платформ с одним и тем же паттерном

Мы используем эту идентичную архитектуру на разных платформах электронной коммерции с разным позиционированием для каждой:

  • Одна платформа получает плавающую боковую панель справа
  • Другая получает нижнюю панель инструментов с эффектом стеклянного размытия

Каждая платформа получает собственный CSS компонентов и CSS темы, но настройка shadow root, вспомогательная функция createSheet() и последовательность монтирования фреймворка идентичны. Добавление новой платформы означает написание короткого скрипта монтирования и двух CSS-файлов — архитектура масштабируется чисто.

adoptedStyleSheets и attachShadow поддерживаются во всех браузерах на основе Chromium. Поскольку речь идёт о расширении Chrome, совместимость с разными браузерами здесь не является проблемой.

Помимо CSS: другие сложные части

CSS-изоляция была самой большой проблемой, но не единственной.

Кросс-origin запросы в MV3 (Manifest V3). Контент-скрипты не могут делать кросс-origin fetch-запросы. Пришлось построить слой передачи сообщений, где контент-скрипт просит фоновый service worker выполнять запросы от его имени. Service worker имеет необходимые разрешения и возвращает ответ через канал обмена сообщениями.

Клиентская оценка. Расширение запускает несколько алгоритмов оценки полностью в браузере. Каждый из них — чистая TypeScript-функция: никаких сетевых вызовов, никакой ML-модели, никакого бэкенда. Анализ продукта завершается за миллисекунды.

Совместимость с Trusted Types. Некоторые сайты электронной коммерции применяют строгую политику безопасности контента (Content Security Policy, CSP) с Trusted Types. Инъекция DOM-элементов через Shadow DOM требует создания кастомной политики Trusted Types во избежание нарушений CSP. Это была особенно неприятная проблема для отладки, потому что ошибки появляются только на конкретных сайтах.

Что я бы сделал иначе

Оглядываясь назад после шести месяцев, выделю три решения, которые стоило принять с первого дня.

Начать с Shadow DOM сразу. Я потратил три месяца, пробуя scoped CSS и всё более отчаянные цепочки !important, прежде чем переключиться. Миграция была болезненной, потому что стили уже были тесно связаны с предположением, что они живут в основном документе.

Использовать строгий TypeScript с самого начала. Мы мигрировали постепенно — нестрогий режим, разрешение JS-файлов — что означает наличие устаревших файлов в смеси. Начало со строгого режима позволило бы поймать десятки ошибок раньше.

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

Если вы создаёте расширение Chrome, которое встраивает UI в сторонние страницы, Shadow DOM с adoptedStyleSheets — это подход, который я бы рекомендовал без колебаний. Это единственное решение, которое даёт истинную двунаправленную CSS-изоляцию, нулевые накладные расходы во время выполнения и полную поддержку фреймворков.

Ответы на эти вопросы могут быть для вас полезными

Почему scoped CSS не подходит для расширений Chrome, встраивающих UI в сторонние страницы?

Scoped CSS ограничивает область видимости ваших стилей, но не защищает от стилей хост-страницы. Агрессивные CSS-сбросы и глобальные правила сторонних сайтов всё равно проникают внутрь и ломают интерфейс расширения.

Можно ли использовать iframe вместо Shadow DOM для изоляции стилей?

Iframe даёт изоляцию, но создаёт серьёзные ограничения: сложная коммуникация между фреймами, проблемы с позиционированием поверх контента страницы и дополнительные накладные расходы. Shadow DOM решает задачу изоляции без этих компромиссов.

Как правильно обрабатывать кросс-origin запросы в расширениях на Manifest V3?

Контент-скрипты не имеют прав на кросс-origin fetch. Решение — слой передачи сообщений: контент-скрипт отправляет запрос фоновому service worker, тот выполняет fetch с нужными разрешениями и возвращает результат обратно.

Что такое Trusted Types и когда они становятся проблемой для расширений?

Trusted Types — это механизм CSP, который ограничивает небезопасные операции с DOM. Некоторые сайты включают строгую политику Trusted Types, и тогда инъекция элементов через Shadow DOM требует создания кастомной политики. Сложность в том, что ошибки проявляются только на конкретных сайтах, а не в разработке.

Зачем устанавливать z-index: 2147483647 вместо просто большого числа?

Это максимальное значение для 32-битного знакового целого числа — верхний предел, который браузеры гарантированно обрабатывают корректно. Любое большее значение может вести себя непредсказуемо, а меньшее рискует оказаться ниже модальных окон или фиксированных заголовков хост-страницы.

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x