CreepJS: CSS-медиазапросы и два канала настройки
Разбор закреплённого сборщика CSS media в CreepJS: одна настройка, прочитанная через свойство в @media и через matchMedia, четырнадцать ключей и сканирование диапазонов экрана.
BotBrowser Team
Нужна структурированная документация по теме Отпечатки?
Эта статья относится к редакционной библиотеке. Для пошаговой настройки, справки и постоянных обновлений переходите сразу в соответствующий раздел docs.
Настройки отображения это одна из немногих поверхностей, которую страница читает без запроса разрешения, и коллектор может прочитать каждую из них дважды: один раз, дав движку применить медиазапрос и прочитав полученное пользовательское свойство, и второй раз, спросив у matchMedia, совпадает ли запрос. Оба ответа приходят из одного движка, но не из одного пути кода, поэтому коллектор, который сохраняет оба, получает два независимых наблюдения одной настройки. Закреплённый открытый сборщик показывает это разделение: он называет четырнадцать ключей, сообщаемых через каждый канал, содержит ветвь, которая печатает пометку о блокировке вместо пустого объекта, и восстанавливает размеры устройства из медиазапросов, а не из screen. BotBrowser применяет конфигурацию отображения профиля внутри ядра браузера, поэтому ответ медиазапроса и сообщаемые значения экрана порождаются одной конфигурацией; он не управляет тем, что сторонний коллектор делает с расхождением между своими каналами, и ничто из перечисленного не является оценкой, рейтингом или утверждением о том, как какой-либо сайт классифицирует сессию.
TL;DR
- Коллектор записывает тридцать правил
@mediaв элемент style, а затем читает обратно четырнадцать пользовательских свойств черезgetComputedStyle, поэтому каждое значение приходит через каскад, а не через вызов API. - Затем он задаёт
matchMediaте же вопросы и сохраняет второй объект с теми же четырнадцатью ключами, поэтому у каждой настройки два сохранённых ответа. - Два ключа намеренно опрашивают
matchMediaокна верхнего уровня, а не фрейма, потому что фрейм может наследовать цветовую схему, отличную от схемы содержащего его окна. - Канал, у которого все четырнадцать ключей возвращаются ложными, отрисовывает пометку о блокировке; отдельный ключ, который не совпал, сохраняется как
undefined, а это другое утверждение. - Ширина и высота устройства восстанавливаются проверкой
device-widthиdevice-heightв одном кандидате плюс десять диапазонов по 1000 пикселей, поэтому сообщаемый размер экрана есть измерение, а не чтениеscreen.width.
Содержание
- Что читают два канала
- Что сохранённый ответ может и чего не может установить
- Сканирование диапазонов экрана
- Парная запись для одного прогона
- Практический вывод
Что читают два канала
Настройки отображения профиля и их значения описаны в разборе предпочтений медиа; CSS- и JavaScript-каналы считывают разрешённые браузером значения.
Оба канала отвечают на одни и те же четырнадцать вопросов. Разница в том, где вычисляется ответ.
Канал пользовательских свойств
Первый канал не вызывает функцию сопоставления вовсе. Он назначает телу документа элемент style с тридцатью правилами медиазапросов, где каждое правило задаёт одно пользовательское свойство при одном условии, затем читает вычисленный стиль тела и извлекает из него четырнадцать свойств:
body.innerHTML = `<style>@media (prefers-color-scheme: light) {body {--prefers-color-scheme: light}}</style>`;
const style = getComputedStyle(body);
style.getPropertyValue('--prefers-color-scheme').trim() || undefined;
Два свойства этого подхода важны для того, кто читает вывод. Значение производит движок стилей, применяющий условное правило, поэтому ключ, который не совпал, оставляет свойство незаданным, и выражение даёт undefined, а не пустую строку или false. И поскольку правила находятся в документе, ответ это то, что движок решил для условий этого документа, а не значение, вычисленное скриптом из других показаний. Отдельный модуль вычисленных стилей CreepJS перечисляет имена CSSStyleDeclaration и отдельно читает системные цвета и шрифты; он не использует этот канал предпочтений.
Правила также кодируют пару: (monochrome) задаёт --monochrome: monochrome, а (monochrome: 0) задаёт --monochrome: non-monochrome, поэтому сохранённое значение есть метка, выбранная между двумя ветвями, а не булево значение.
Канал matchMedia
Второй канал задаёт движку те же вопросы через Window.matchMedia, и каждый ключ проходит цепочку тернарных операторов от самого конкретного ответа к наименее конкретному:
pointer: win.matchMedia('(pointer: fine)').matches
? 'fine'
: win.matchMedia('(pointer: coarse)').matches
? 'coarse'
: win.matchMedia('(pointer: none)').matches
? 'none'
: undefined;
pointer и any-pointer пробуют по три ветви, color-gamut пробует rec2020, затем p3, затем srgb, а display-mode пробует fullscreen, standalone, minimal-ui, затем browser. Ключ, у которого отказали все ветви, равен undefined, то есть тому же сигнальному значению, которое использует первый канал, и поэтому два объекта сравнимы ключ за ключом напрямую.
Какое окно опрашивает каждый канал
Четырнадцать ключей не означают четырнадцать одинаковых областей действия. Большинство запросов идёт через win.matchMedia, где win это собственное окно сборщика. Два запроса идут иначе: prefers-color-scheme и orientation вызывают голый идентификатор matchMedia, а комментарий в исходном коде говорит, что предпочитается главное окно.
Этот выбор намеренный и наблюдаемый. Фрейм может находиться внутри документа, чья цветовая схема или ориентация сообщается его собственным контекстом иначе, и сборщик, которому нужен ответ содержащего окна, обязан спросить окно, а не фрейм. При сравнении двух прогонов записывайте, какую область действия использовал каждый ключ, потому что различие в prefers-color-scheme между прогонами может прийти из области действия так же легко, как из конфигурации.
Та же асимметрия появляется в канале @media по структурной причине: правила каскада применяются к документу, которому принадлежит элемент style, поэтому фрейм, читающий собственное тело, не может увидеть правило, записанное в тело родителя. У двух каналов поэтому разные области действия для одного ключа, и расхождение между ними не является автоматически несогласованностью.
| Ключ | Источник значения @media | Источник значения matchMedia |
|---|---|---|
prefers-reduced-motion | no-preference или reduce | no-preference или reduce |
prefers-color-scheme | light или dark | light или dark, запрошено у главного окна |
monochrome | monochrome или non-monochrome | те же две метки |
inverted-colors | inverted или none | те же две метки |
forced-colors | none или active | те же две метки |
any-hover | hover или none | те же две метки |
hover | hover или none | те же две метки |
any-pointer | fine, coarse или none | те же три метки |
pointer | fine, coarse или none | те же три метки |
device-aspect-ratio | сокращённая строка отношения | та же строка отношения |
device-screen | width x height | та же пара |
display-mode | fullscreen, standalone, minimal-ui или browser | те же четыре метки |
color-gamut | srgb, p3 или rec2020 | те же три метки |
orientation | landscape или portrait | те же две, запрошено у главного окна |
Таблица это та часть отчёта, которую читатель может проверить, ничего не запуская: если у сохранённого объекта есть ключ со значением, которого нет в его столбце, значение пришло не из этого канала.
Что сохранённый ответ может и чего не может установить
Объект с четырнадцатью ключами выглядит как измерение. Он есть набор меток, а полезные вопросы касаются того, какие метки отсутствуют и что отсутствие означает.
undefined как результат ветви
Каждый ключ оканчивается на undefined, когда ни одна ветвь не совпала, и сборщик сохраняет это значение как есть. Поэтому отчёт различает три состояния одного ключа: совпавшая метка, отсутствующий ключ, потому что ничего не совпало, и целый объект, который не отрисовался, потому что отказали все ключи. Только третье является условием блокировки.
При записи прогона держите два объекта раздельно, а не сливайте их. Слитый объект не скажет, пришло ли pointer: fine из каскада или из matchMedia, а это происхождение единственное, что добавляет второй канал.
Ветка блокировки
Функция отрисовки проверяет оба объекта, прежде чем что-либо нарисовать. Для каждого канала она считает ключи с истинным значением, и когда этот счётчик равен нулю, печатает пометку о блокировке вместо пустого списка:
!Object.keys(mediaCSS).filter((key) => !!mediaCSS[key]).length ? HTMLNote.BLOCKED : modal(...)
Различие важно, потому что у двух показаний разные причины. Пустой объект @media означает, что каскад не применил ни одного из тридцати правил, а это указывает на элемент style, документ или ограничение таблиц стилей. Пустой объект matchMedia означает, что ни одна ветвь не совпала ни для одного из четырнадцати запросов, что почти невозможно в обычной настольной сессии и потому описывает сильно ограниченный контекст, а не отсутствующую настройку.
Отчёт, который показывает блокировку для одного канала и метки для другого, описывает проблему области действия или таблицы стилей, а не настройку приватности. Его следует сначала рассматривать как находку о конфигурации.
Что означает расхождение
Совпадающие ключи позволяют сравнивать значения без дополнительной инструментации. Стоит назвать три исхода:
- Оба канала возвращают одну метку. Движок согласился сам с собой на двух путях кода, что есть свидетельство согласованности для этого ключа и ничего больше.
- Один канал возвращает метку, а другой возвращает
undefined. Сначала проверьте область действия:prefers-color-schemeиorientationзапрашиваются у главного окна, поэтому фрейм, чей собственный контекст отличается, может дать именно такую форму. - Оба канала возвращают метки, которые противоречат друг другу. Это интересный случай, и он является утверждением о таблице стилей и медиасостоянии документа, а не о физическом устройстве.
Ни один из этих исходов не называет человека, модель или вердикт. Группы сигналов, которыми пользуется детектор относят такой вывод к семействам, и разница между меткой семейства и сравнением каналов в том, что метка семейства назначается после решения, а сравнение каналов доступно до него.
Строка сенсорного устройства
Отрисованная панель добавляет одну производную строку, touch device, которая читает единственное сохранённое значение: когда any-pointer равен coarse, она печатает истину, а иначе печатает пометку о неизвестном, а не ложь. Грубый основной указатель обычен на телефонах и планшетах, и он также встречается на некоторых настольных сенсорных панелях, поэтому строка есть подсказка о возможностях указателя, а не утверждение о классе оборудования.
Сканирование диапазонов экрана
Третья часть сборщика не читает screen.width и screen.height и не останавливается на этом. Она проверяет их по медиазапросам.
Проба одного кандидата
Первая попытка задаёт два запроса ровно для тех значений, которые сообщил объект screen: (device-width: W px) и (device-height: H px). Если отвечают оба, пара возвращается без изменений, и сканирование заканчивается.
Сканирование начинается только тогда, когда один из них не отвечает. Такой порядок оставляет обычный случай дешёвым и делает дорогой путь диагностикой, а не значением по умолчанию.
Проход по десяти диапазонам
Запасной путь строит тысячу правил на шаг и проходит десять шагов, покрывая от нуля до десяти тысяч пикселей и заполняя то измерение, которое ещё не ответило. Оба измерения ищутся независимо внутри одного прохода, поэтому ширина, ответившая в первом диапазоне, не останавливает высоту.
Результат это пара чисел, восстановленных из собственных запросов движка к устройству, а не из объекта screen. Именно это делает чтение полезным: конфигурация, которая сообщает одно разрешение через screen и отвечает другим через device-width, даёт в одном отчёте два разных числа, и расхождение видно без дополнительной работы.
Соотношение сторон и проверка без панели задач
Из того же блока выходят два производных значения. Отношение вычисляется сокращением ширины и высоты вспомогательной функцией наибольшего общего делителя и форматируется как W/H, после чего оно и запрашивается через matchMedia, и записывается в правило @media.
Второе значение это эвристика о доступной площади. Сборщик вычисляет, равны ли сообщённые ширина и высота доступным ширине и высоте, и считает большой экран без разницы между ними условием пониженной энтропии:
const noTaskbar = !(width - availWidth || height - availHeight);
if (screen.width !== width || (width > 800 && noTaskbar)) {
LowerEntropy.IFRAME_SCREEN = true;
}
Флаг интересен по двум причинам. Настольная сессия обычно резервирует часть площади под панель задач или док, поэтому точное совпадение на большом экране есть форма синтетической поверхности. И флаг записан в общую запись, которая объявляет только шесть ключей, ни один из них не этот, поэтому метка есть специальная аннотация, а не документированное поле. Если эта запись читается из других скриптов, не считайте наличие ключа доказательством того, что он был задуман.
| Поле | Откуда берётся | Что устанавливает |
|---|---|---|
screenQuery.width | проба первого кандидата, затем десять диапазонов по 1000 px | что движок отвечает для device-width |
screenQuery.height | тот же проход, независимо | что движок отвечает для device-height |
device-aspect-ratio | ширина и высота, сокращённые на их наибольший общий делитель | строка отношения, а не тип панели |
device-screen | W x H, запрошенное через matchMedia | совпадает ли пара как одно условие |
LowerEntropy.IFRAME_SCREEN | сообщённый экран против доступной площади | подсказка об экране с пониженной энтропией, а не вердикт |
Сама геометрия экрана и окна есть отдельная поверхность со своими различиями между сообщённым разрешением, доступной площадью и вьюпортом раскладки; отпечаток экрана и окна рассматривает эти значения, тогда как этот сборщик проверяет по медиазапросам только два из них.
Парная запись для одного прогона
Два объекта сборщика делают запись дешёвой в построении при условии, что запись сохраняет оба.
Поля, которые стоит сохранять
Сохраняйте два объекта под отдельными ключами, пару экрана, восстановленную сканированием, идентификатор сборки браузера и URL документа. Добавьте одно поле, которого сборщик не производит: область действия, в которой был задан каждый чувствительный ключ. Это поле превращает расхождение в объяснение, а не в тревогу.
Храните сырые метки, а не хеш. Сборщик отрисовывает короткий хеш рядом с каждой модальной панелью, что удобно для сравнения двух отчётов на глаз и бесполезно для диагностики одного, потому что хеш четырнадцати меток не скажет, какая метка изменилась.
Повтор и сравнение
Два прогона с одной конфигурацией должны дать одинаковые метки для каждого ключа. Ключ, который меняется между прогонами, есть находка о конфигурации или о времени: таблица стилей могла не примениться в одном прогоне, фрейм мог находиться в другой области действия, или документ мог загрузиться в другой среде.
Сравнивайте по ключу, а не по объекту, и записывайте направление изменения. pointer, который сдвинулся с fine на undefined, это другая находка, чем сдвиг с fine на coarse, и только второй описывает изменившуюся возможность указателя.
Что запись не может решить
Запись описывает, что ответил движок документа. Она не описывает аппаратное обеспечение дисплея, операционную систему или модель, которую какой-либо сайт строит из этих меток. Чистое сравнение по обоим каналам говорит, что два пути кода согласились на этой сборке для этого документа, а это утверждение о согласованности, а не об идентичности.
BotBrowser позволяет настроить параметры отображения профиля внутри ядра браузера, поэтому значение медиазапроса и значение screen относятся к одной загруженной конфигурации; справка по CSS-сигналам перечисляет доступные настройки. Это не меняет вывод стороннего кода при расхождении и не может заставить экран с пониженной энтропией вести себя как конкретная физическая панель.
FAQ
Два канала всегда совпадают?
Не обязательно, и причины структурные, а не случайные. Канал каскада применяется только к документу, которому принадлежит элемент style, тогда как два ключа matchMedia запрашиваются у главного окна, поэтому фрейм в другом контексте может законно дать другие ответы для того же ключа.
Что означает пометка о блокировке?
Пометка о блокировке означает, что каждый ключ в этом канале вернулся ложным. Для канала каскада это указывает на неприменение таблицы стилей; для канала matchMedia это указывает на ограниченный контекст, потому что обычная настольная сессия совпадает хотя бы с одной ветвью для большинства из четырнадцати ключей.
Почему проверять медиазапросы вместо чтения значений screen?
Потому что проба есть независимый ответ на тот же вопрос. Конфигурация может сообщать одно разрешение через screen и отвечать другим через device-width, и сканирование делает это различие видимым в отчёте, а не оставляет его отдельной проверке.
Практический вывод
Начинайте с двух объектов, а не с отрисованной панели. Один канал приходит из каскада, другой из функции сопоставления, они отвечают на одни и те же четырнадцать вопросов, и каждый ключ может отсутствовать, быть помеченным или принадлежать каналу, который не отрисовался. Затем читайте значения экрана как измерение: сканирование проверяет сообщённую пару по device-width и device-height, а проверка без панели задач есть подсказка о доступной площади, а не вердикт.
Следующее действие это одна парная запись на документе, который находится под контролем. Сохраните оба объекта, восстановленную пару экрана и область действия каждого чувствительного ключа, затем повторите прогон и сравните по ключу. Если ключ сдвинулся, исследовать нужно таблицу стилей или область действия; если все ключи держатся, два канала согласились на этой сборке.
Источники
Эта статья входит в руководство: Что такое отпечаток браузера?
Похожие статьи
CSS-фингерпринтинг: как стили отслеживают вас
ПлатформаМедиазапросы: уважение пользовательских предпочтений в интерфейсах
ОтпечаткиCSS-сборщик CreepJS: вычисленные стили
ОтпечаткиОтпечаток экрана и окна: свойства отображения
ОтпечаткиГруппы сигналов CreepJS и как он их объединяет
ОтпечаткиСигналы Math в CreepJS: источники полей
Выполните ту же проверку в BotBrowser
Загрузите профиль, сохраните внутри браузера те сигналы, о которых только что прочитали, и подтвердите результат на своей машине до раскатки.