Сегодня разбираем материал dev.to о теме «Я потратил 6 месяцев на создание расширения Chrome с Vue 3 и Shadow DOM». Материал полезен тем, кто хочет быстро понять суть темы и перевести идеи в прикладные действия.
Chrome с Vue 3 и Shadow DOM — вот что оказалось по-настоящему сложным
В разработке расширение выглядит идеально. Шрифты чёткие, макет чистый, утилиты Tailwind работают именно так, как ожидается.
Стили на хост-странице нарушали мой тщательно проработанный интерфейс, и в результате text-sm выглядел не так, как должен. Flex-контейнеры сжимались. Я добавил !important к нескольким правилам, потом к десяткам, и вскоре осознал, что веду бесполезную войну.
Процесс создания расширения занял полгода, и он был полон вызовов. Это расширение должно работать на множестве платформ, каждая из которых имеет свои уникальные CSS-условия, что могло разрушить интерфейс. Я расскажу, как я это сделал.
- Подходы, которые не сработали
- Shadow DOM и adoptedStyleSheets
- Как работает паттерн монтирования
- Детали, которые удалось выяснить только в процессе отладки
- Порядок CSS имеет значение
- Поддержка нескольких платформ с одним и тем же паттерном
- Помимо 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-битного знакового целого числа — верхний предел, который браузеры гарантированно обрабатывают корректно. Любое большее значение может вести себя непредсказуемо, а меньшее рискует оказаться ниже модальных окон или фиксированных заголовков хост-страницы.


