javascript

Как я сделал расширение, которое показывает десктоп и мобильную версию сайта одновременно

  • понедельник, 15 июня 2026 г. в 00:00:04
https://habr.com/ru/articles/1047354/

Когда верстаешь адаптив, постоянно скачешь между десктопом и мобильной версией: то DevTools в режиме устройства, то ресайз окна, то открыть на телефоне. Десктоп и мобайл при этом никогда не видны одновременно — один прячется, когда смотришь на другой. А при показе работы заказчику демонстрация «узкого окна браузера» по видеосвязи выглядит так себе.

Готовые решения, конечно, есть. Я смотрел на мобильные симуляторы из Chrome Web Store — например «Mobile First»

Мобильный симулятор - тестирование адаптивного сайта
Мобильный симулятор - тестирование адаптивного сайта

«Симулятор телефона — мобильный»

Симулятор Телефона — Мобильный Эмулятор
Симулятор Телефона — Мобильный Эмулятор

и «U-eyes — мобильный симулятор»

U-Eyes: Мобильный Симулятор
U-Eyes: Мобильный Симулятор

Они прекрасно выполняют свою роль: дают набор устройств, вьюпорты, удобный предпросмотр. Но почти все они открывают превью в отдельном окне или в своей собственной области, и ни один не давал главного, что мне было нужно — быстро и удобно посмотреть адаптивное устройство прямо на вкладке, поверх десктопной версии, одновременно с ней. Хотелось видеть именно это: десктоп и телефон рядом, на той же странице, без переключений.

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

Идея

Расширение по клику строит оверлей: рамку телефона/планшета поверх текущей страницы, а внутри рамки — та же самая страница, но в мобильном вьюпорте. Прокрутка, клики и ввод синхронизируются между десктоп-страницей и превью.

Сразу встал главный вопрос: как отрендерить страницу внутри неё самой?

Проблема №1: страница в iframe, которую нельзя зафреймить

Очевидное решение — засунуть тот же URL в iframe и сузить его до ширины телефона. Но большинство сайтов отдают заголовки, которые запрещают встраивание:

  • X-Frame-Options: SAMEORIGIN / DENY

  • Content-Security-Policy: frame-ancestors …

iframe с такой страницей просто не загрузится.

Решение — declarativeNetRequest (MV3). Я динамически добавляю правило, которое для запросов превью-фрейма:

  • срезает X-Frame-Options;

  • вырезает из CSP только директиву frame-ancestors (не трогая остальной CSP);

  • подменяет User-Agent на мобильный, чтобы сайт отдал мобильную вёрстку и сработали media-queries, завязанные на UA.

Важные ограничения, чтобы это было безопасно и не «протекало»:

  • правило живёт только на время открытого превью и снимается при закрытии;

  • оно ограничено конкретной вкладкой;

  • никакие запросы не блокируются, не логируются и никуда не отправляются — меняются только заголовки ответа для рендера фрейма.

Проблема №2: оверлей не должен ломать стили сайта

Оверлей (рамка, панель управления, кнопки) рендерится на чужой странице, где какой угодно CSS. Чтобы стили сайта не поехали от моих и наоборот, весь UI живёт в Shadow DOM. Это даёт изоляцию стилей почти бесплатно: мои классы не конфликтуют с классами сайта.

Проблема №3: синхронизация прокрутки, кликов и ввода

Хотелось, чтобы можно было, например, заполнять форму на десктопе и видеть, как она заполняется в телефоне. Поскольку фрейм — это та же страница, я слушаю события на одной стороне и воспроизвожу на другой: scroll, клики по соответствующим элементам, ввод в поля. Тут много нюансов с маппингом элементов и защитой от бесконечных циклов событий (когда синхронизированное действие снова триггерит событие).

Скриншоты

Функция скриншота снимает видимое превью, локально обрезает по рамке устройства и отдаёт картинку как загрузку файла. Всё происходит на устройстве, ничего никуда не грузится.

Запись экрана

Здесь стоит быть честным про текущее состояние.

В сейчас опубликованной версии запись работает «стандартным» путём: через штатный диалог захвата экрана браузера (то самое разрешение от Google, где вы сами выбираете, что записывать). Видео кодируется локально и сохраняется на устройство файлом. Доступны два формата: WebM и MP4 (MP4 пока в тестовом режиме).

Параллельно я допиливаю более «бесшовный» вариант записи — без промпта на расшаривание и без сужения вьюпорта. Архитектурно он выглядит так:

  • content-скрипт шлёт в background команды recStart / recStop;

  • захват идёт через chrome.tabCapture + getUserMedia в offscreen-документе (в MV3 у service worker нет доступа к DOM/медиа, поэтому нужен offscreen);

  • offscreen возвращает мастер-видео, а при скачивании content лениво инжектит муксеры (mp4-muxer / webm-muxer) и через WebCodecs перекодирует мастер в выбранный формат (MP4 H.264 / WebM VP9) и качество (1080/720/480).

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

Приватность как ограничение архитектуры

Я сознательно делал так, чтобы расширению нечего было «сливать»: нет бэкенда, нет аналитики внутри расширения, нет удалённого кода. Настройки (выбранное устройство, ориентация, масштаб, тема) лежат в chrome.storage.local и не покидают устройство. Список вкладок с открытым превью — в chrome.storage.session, чтобы восстановиться после перезагрузки; это только идентификаторы вкладок, без содержимого и URL.

Разрешения в манифесте — ровно под задачу: activeTab, scripting, declarativeNetRequest, storage, notifications и host-доступ для рендера превью на любом сайте.

Итог

Получилось расширение Mobile View — десктоп и мобильная вёрстка одновременно, прямо на вкладке: синхронизация, скриншоты и запись экрана, всё локально. Оно бесплатное и лежит в Chrome Web Store.

Mobile View
Mobile View

Буду рад фидбэку именно по технической части: как бы вы решали обход X-Frame-Options, синхронизацию событий или перекодирование на WebCodecs иначе? И если найдёте баги на своей ОС/версии браузера — расскажите, очень помогает.

Ссылка на расширение: https://chromewebstore.google.com/detail/mobile-view-mobile-simu/hocbjiaeeijekejepphjihbpogikmofh