Главная » Статьи » Адаптивные элементы DOM: решение проблемы с запросами контейнеров

Адаптивные элементы DOM: решение проблемы с запросами контейнеров

Адаптивные элементы DOM: решение проблемы с запросами контейнеров

От автора: запросы контейнеров — это предложение, которое позволит веб-разработчикам создавать DOM элемент на основе размера содержащего элемента, а не размера окна просмотра браузера. Если вы веб-разработчик, то, наверное, раньше слышали о запросах контейнеров. Примерно до тех пор, пока у нас был отзывчивый веб-дизайн, у нас были разработчики, просящие их (изначально запрос элементов, а затем переход к запросам контейнеров). На самом деле, запросы контейнеров могут быть самой популярной функцией CSS, которой до сих пор нет в браузерах.

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

Вместо того, чтобы уделять особое внимание конкретному предложению функций CSS, которое мы называем «запросы контейнеров», я хочу сосредоточиться на более широкой концепции построения компонентов, отвечающих за их окружение. И если вы примете это большое обрамление, то фактически это будут новые веб-API, которые уже позволяют вам её достичь.

Правильно, нам не нужно ждать, пока запросы контейнеров начнут создавать адаптивные компоненты. Мы можем начать строить их сейчас!

Стратегия, которую я собираюсь предложить в этой статье, может быть использована уже сегодня, и она разработана как усовершенствование, поэтому браузеры, которые не поддерживают новые API или не запускают JavaScript, будут работать точно так же, как и в сейчас. Она также проста в реализации (копирование / вставка), высокопроизводительна и не требует каких-либо специальных инструментов сборки, библиотек или фреймворков.

Чтобы увидеть некоторые примеры этой стратегии в действии, я создал демонстрационный сайт Responsive Components. Каждая демонстрационная ссылка ссылается на исходный код CSS, поэтому вы можете увидеть, как она работает.

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

Стратегия

Наиболее отзывчивые дизайнерские стратегии или методологии (они ничем не отличаются) работают в соответствии с двумя основными принципами:

Для каждого компонента сначала определите набор общих, базовых стилей, которые будут применяться независимо от того, в какой среде находится компонент.

Затем определите дополнения или переопределения для тех базовых стилей, которые будут применяться в определенных условиях среды.

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

В большинстве случаев базовые стили, определенные выше в пункте 1, являются стилями, которые работают с минимально возможными размерами экрана (поскольку маленькие экраны имеют тенденцию быть более ограничительными, чем большие экраны), и они не обёрнуты в какой-либо медиа-запросы (поэтому применяются где угодно).

Вот пример, который определяет базовые стили для .MyComponent и переопределяет их на двух произвольных контрольных точках 36emи 48em:

.MyComponent { /* Base styles that work for any screen size */
} @media (min-width: 36em) { .MyComponent { /* Overrides the above styles on screens larger than 36em */ }
} @media (min-width: 48em) { .MyComponent { /* Overrides the above styles on screens larger than 48em */ }
}

Конечно, эти брейкпоинты используют медиа-запросы, поэтому они применяются по размеру окна браузера. Чего хотят сторонники запросов контейнеров — это способность делать что-то вроде этого (обратите внимание, это предлагаемый синтаксис, а не официальный):

.Container:media(min-width: 36em) > .MyComponent { /* Overrides that only apply for medium container sizes */
}

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

.MyComponent { /* Base styles that work on any screen size */
} .MD > .MyComponent { /* Overrides that apply for medium container sizes */
} .LG > .MyComponent { /* Overrides that apply for large container sizes */
}

Конечно, этот код предполагает, что в контейнерах компонентов добавлены правильные классы (в этом примере — .MD и .LG ). Но, игнорируя пока что эту деталь, если вы разработчик CSS, который хочет создать отзывчивый компонент, второй синтаксис, вероятно, для вас все еще имеет смысл.

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

Чтобы пояснить остальную часть этой статьи, позвольте мне определить классы именованных брейкпоинтов, которые я использую со следующим отображением (где min-width применяется к контейнеру , а не к окну просмотра):

SM — min-width: 24em

MD — min-width: 36em

LG — min-width: 48em

XL — min-width: 60em

Теперь все, что нам нужно сделать, это обеспечить, чтобы наши элементы контейнера всегда имели правильные классы брейкпоинтов, поэтому селекторы компонентов должны соответствовать.

Изменение размеров контейнера

За большую часть истории веб-разработки можно было наблюдать изменения в окне, но было трудно или невозможно (по крайней мере, по-настоящему) наблюдать изменения размеров отдельных элементов DOM. Всё изменилось, когда Chrome 64 отправил ResizeObserver.

ResizeObserver, следуя по стопам аналогичных API, таких как MutationObserver и IntersectionObserver, позволяет веб-разработчикам наблюдать изменения размеров элементов DOM в высокоэффективном режиме. Вот код, необходимый для того, чтобы CSS в предыдущем разделе работал с ResizeObserver:

// Only run if ResizeObserver is supported.
if ('ResizeObserver' in self) { // Create a single ResizeObserver instance to handle all // container elements. The instance is created with a callback, // which is invoked as soon as an element is observed as well // as any time that element's size changes. var ro = new ResizeObserver(function(entries) { // Default breakpoints that should apply to all observed // elements that don't define their own custom breakpoints. var defaultBreakpoints = {SM: 384, MD: 576, LG: 768, XL: 960}; entries.forEach(function(entry) { // If breakpoints are defined on the observed element, // use them. Otherwise use the defaults. var breakpoints = entry.target.dataset.breakpoints ? JSON.parse(entry.target.dataset.breakpoints) : defaultBreakpoints; // Update the matching breakpoints on the observed element. Object.keys(breakpoints).forEach(function(breakpoint) { var minWidth = breakpoints[breakpoint]; if (entry.contentRect.width >= minWidth) { entry.target.classList.add(breakpoint); } else { entry.target.classList.remove(breakpoint); } }); }); }); // Find all elements with the `data-observe-resizes` attribute // and start observing them. var elements = document.querySelectorAll('[data-observe-resizes]'); for (var element, i = 0; element = elements; i++) { ro.observe(element); }
}

Примечание: в этом примере используется синтаксис ES5, потому что (это я объясню позже) я рекомендую вставить этот код непосредственно в свой HTML, а не включать его во внешний файл JavaScript. Более старый синтаксис используется для более широкой поддержки браузеров.

Этот код создает один ResizeObserver экземпляр с функцией обратного вызова. Затем он запрашивает DOM для элементов с data-observe-resizes атрибутом и начинает их наблюдать. Функция обратного вызова, которая вызывается сначала при наблюдении, а затем снова после любого изменения, проверяет размер каждого элемента и добавляет (или удаляет) соответствующие классы брейкпоинтов.

Другими словами, этот код превратит элемент контейнера размером в 600 пикселей из этого:

<div data-observe-resizes> <div class="MyComponent">...</div>
</div>

В это:

<div class="SM MD" data-observe-resizes> <div class="MyComponent">...</div>
</div>

И эти классы будут автоматически и мгновенно обновляться в любое время при изменении размера контейнера.

Теперь с ним все .SM и .MD селекторы в предыдущем разделе будут соответствовать (но не .LG или .XL селекторам), и код будет работать!

Настройка брейкпоинтов

Код в обратном вызове ResizeObserver выше определяет набор брейкпоинтов по умолчанию и позволяет указывать пользовательские брейкпоинты на основе каждого компонента, передавая JSON через data-breakpoints атрибут.

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

<div data-observe-resizes data-breakpoints='{"BP1":400,"BP2":800,"BP3":1200}'> <div class="MyComponent">...</div>
</div>

На сайте «Мои адаптивные компоненты» приведен пример компонента, устанавливающего собственные пользовательские брейкпоинты наряду с компонентами, использующими брейкпоинты по умолчанию.

Обработка динамических изменений DOM

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

Есть одно решение для решения этой проблемы — это расширение фрагмента выше, чтобы включить MutationObserver, который отслеживает все добавленные элементы DOM. Это подход, который я использую на демонстрационном сайте Responsive Components, и он хорошо работает для небольших и средних сайтов с ограниченными изменениями DOM.

На больших сайтах с часто обновляемым DOM есть вероятность, что вы уже используете что-то вроде Custom Elements или веб-фреймворк с методами жизненного цикла компонентов, которые отслеживают, когда элементы добавляются и удаляются из DOM. Если это так, то лучше просто подключиться к этому механизму. Вероятно, вы даже захотите создать общий, многоразовый компонент контенера.

Например, пользовательский <responsive-container> элемент может выглядеть примерно так:

// Create a single observer for all <responsive-container> elements.
const ro = new ResizeObserver(...); class ResponsiveContainer extends HTMLElement { // ... connectedCallback() { ro.observe(this); }
} self.customElements.define('responsive-container', ResponsiveContainer);

Примечание: в то время как может возникнуть соблазн создать новый ResizeObserver для каждого элемента контейнера, на самом деле гораздо лучше создать единый ResizeObserver, который соблюдает многие элементы. Чтобы узнать больше, см выводы Алекса Тотик о производительности ResizeObserver в списке рассылки blink-dev.

Вложенные компоненты

В моем первоначальном эксперименте с этой стратегией я не привязывал каждый компонент к элементу контейнера. Вместо этого я использовал один элемент контейнера для отдельной области содержимого (заголовок, боковая панель, нижний колонтитул и т. Д.), А в моем CSS я использовал компиляторы потомков вместо дочерних комбинаторов.

Это привело к более простой разметке и CSS, но она быстро развалилась, когда я попытался вложить компоненты в другие компоненты (как делают многие сложные сайты). Проблема заключается в том, что при использовании метода комбинации потомков селекторы будут одновременно сопоставлять несколько контейнеров.

После создания нескольких нетривиальных демонстраций стало ясно, что прямой структурой child / parent для каждого компонента и его контейнера намного проще управлять и масштабировать их. Обратите внимание, что контейнеры могут содержать более одного компонента, если каждый размещенный компонент является прямым потомком.

Расширенные селекторы и альтернативные подходы

Стратегия, изложенная в этой статье, использует дополнительный подход к компоновке стилей. Другими словами, вы начинаете с базовых стилей, а затем добавляете больше стилей сверху. Однако это не единственный способ подойти к компонентам стиля. В некоторых случаях вы захотите определить стили, которые соответствуют исключительно и применяются только к определенному брейкпоинту (т. е. вместо того, чтобы (min-width: 48em )вы хотели бы чего-то типа (min-width: 48em) и (max-width: 60em)).

Если этот подход более предпочтительней, то нужно немного изменить код обратного вызова ResizeObserver, чтобы применить только имя класса текущего брейкпоинта. Поэтому, если компонент имел свой «большой» размер, а не SM MD LG задавал имя класса, просто установите LG.

Затем в CSS вы можете написать селекторы вроде этих:

/* To match breakpoints exclusively */
.SM > .MyComponent { }
.MD > .MyComponent { }
.LG > .MyComponent { } /* To match breakpoints additively */
:matches(.SM) > .MyComponent { }
:matches(.SM, .MD) > .MyComponent { }
:matches(.SM, .MD, .LG) > .MyComponent { }

Обратите внимание, что при использовании рекомендуемой мной стратегии для аддитивного сопоставления вы все равно можете сопоставлять брейкпониты исключительно с помощью селектора .MD:not(.LG), хотя это, возможно, не очень понятно.

В конце концов, вы можете выбрать, какая конвенция имеет для вас наибольший смысл и лучше всего подходит для вашей ситуации.

Примечание: :matches() селектор не очень хорошо поддерживается в современных браузерах. Однако вы можете использовать такие инструменты, как postcss-selector- matches, чтобы перевести :matches() в какой-то работающий кросс-браузер.

Брейкпоинты на основе высоты

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

Однако ничто в этой стратегии не позволит компоненту реагировать на высоту своего контейнера. ResizeObserver сообщает как размеры ширины, так и высоты, поэтому, если вы хотите наблюдать изменения высоты, вы можете определить отдельный набор классов брейкпоинтов, возможно, с W-префиксом для брейкпоинтов на основе ширины и H-префиксом для брейкпоинтов на основе высоты.

Поддержка браузера

Хотя ResizeObserver в настоящее время поддерживается только в Chrome , нет абсолютно никакой причины, по которой вы не можете (или не должны) использовать его. Стратегия, которую я изложил здесь, намеренно разработана, чтобы работать нормально, если браузер не поддерживает ResizeObserver или, если JavaScript отключен. В любом из этих случаев пользователи будут видеть ваши стили по умолчанию, которых должно быть более чем достаточно, чтобы обеспечить отличную работу. Фактически, они, вероятно, будут теми же стилями, которые вы используете уже сегодня.

Мой рекомендуемый подход — использовать медиа-запросы для макета вашего сайта, а затем эту гибкую стратегию компонентов для конкретных компонентов, которые в ней нуждаются (многие не будут).

Если вы действительно хотите обеспечить согласованный пользовательский интерфейс во всех браузерах, вы можете загрузить полифил ResizeObserver, который имеет отличную поддержку браузера (IE9 +). Однако убедитесь, что вы загружаете его, если пользователь действительно в нем нуждается.

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

Демо-сайт Respive Components использует этот последний подход. Он загружает полифил, но только если браузер пользователя не поддерживает ResizeObserver и если ширина экрана пользователя не менее 48em.

Ограничения и будущие улучшения

В целом, я думаю, что стратегия адаптивных компонентов, изложенная здесь, невероятно универсальна и имеет очень мало недостатков. Я твердо убежден, что каждый сайт с областями контента, размер которых может измениться независимо от области просмотра, должен реализовывать стратегию адаптивных компонентов, а не полагаться только на медиа-запросы (или на основе JavaScript-решения, которое не использует ResizeObserver).

При этом эта стратегия имеет несколько ограничений, которые, как мне кажется, стоит обсудить.

Это не чистый CSS

Одним из очевидных недостатков этого решения является то, что для этого требуется больше, чем просто CSS. Помимо определения стилей в CSS, вы также должны аннотировать свои контейнеры в HTML и координировать работу обоих с JavaScript.

Хотя я думаю, что мы все согласны с тем, что чистое решение CSS — это конечная цель, я надеюсь, что мы, как сообщество, можем поспособствовать её достижению.

В таких вопросах я хотел бы напомнить себе эту цитату из принципов дизайна HTML W3C: В случае конфликта, ставьте пользователей над авторами, над разработчиками, над спецификаторами и над теоретической чистотой.

Мигание правильно / неправильно оформленного контента

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

Хотя это не худший опыт, с чистым CSS вам не о чем беспокоиться. А поскольку эта стратегия предполагает координацию с JavaScript, вам также нужно координировать, когда применяются ваши стили и брейкпоинты, чтобы избежать повторной компоновки.

Я нашел лучший способ справиться с этим — вставить код запроса контейнера в конец HTML-шаблонов, так он запускается быстрее. Затем нужно добавить класс или атрибут к элементам контейнера, когда они будут инициализированы и видимы, так вы узнаете, когда их можно будет показать (и убедитесь, что вы рассматриваете случай, когда JavaScript отключен или есть ошибка запуска). Вы можете увидеть пример того, как я это делаю на демонстрационном сайте.

Единицы базируются в пикселях

Многие (если не большинство) разработчики CSS предпочитают определять стили на основе единиц с большей контекстуальной релевантностью (например, em на основе размера шрифта или vh на основе высоты экрана просмотра и т. д.), тогда как ResizeObserver, как и большинство DOM API, возвращает все свои значения в пикселях. На самом деле в настоящий момент нет хорошего способа.

В будущем, когда браузеры реализуют CSS Typed OM (один из новых спецификаций CSS Houdini ), мы сможем легко и просто конвертироваться между различными единицами CSS любого элемента. Но до тех пор, такое преобразование так ударит по производительности, что это станет заметно в UX.

Заключение

В этой статье описывается стратегия использования современных веб-технологий для создания гибких компонентов: элементы DOM, которые могут обновлять свой стиль и макет в ответ на изменения размера их контейнера.

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

К счастью, теперь у нас есть API-интерфейсы браузера, которые позволяют нам создавать эффективные решения. Стратегия, изложенная в этой статье:

Будет работать сегодня на любом веб-сайте

Легко реализуется (копирование / вставка)

Выполняет так же, как и решение на основе CSS

Не требует каких-либо конкретных библиотек, фреймворков или инструментов сборки.

Использует прогрессивное усовершенствование, поэтому пользователи в браузере, которые не имеют необходимых API-интерфейсов или имеют отключенный JavaScript, все еще могут использовать этот сайт.

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

Автор: Per Harald Borgen

Источник: https://medium.freecodecamp.org/

Редакция: Команда webformyself.