Русский
Русский
English
Статистика
Реклама

Css

Перевод Пользовательские CSS-переменные, инверсия светлоты цветов и создание тёмной темы за 5 минут

15.04.2021 12:10:33 | Автор: admin
Вы, наверное, уже знаете о том, что для хранения сведений об отдельных компонентах цвета можно применять пользовательские CSS-переменные. Это позволяет избавиться от необходимости повторения одних и тех же цветовых координат в стилях, описывающих цветовую тему сайта. Возможно, вы даже знаете о том, что одну и ту же переменную можно использовать для настройки нескольких компонентов цвета.



Например тона (hue) и насыщенности (saturation) при использовании цветовой модели HSL:

:root {--primary-hs: 250 30%;}h1 {color: hsl(var(--primary-hs) 30%);}article {background: hsl(var(--primary-hs) 90%);}article h2 {background: hsl(var(--primary-hs) 40%);color: white;}

Вот очень простая страница, спроектированная с использованием этого подхода.


Пример страницы

Пользовательские переменные, в отличие от переменных препроцессора, можно даже локально переопределять, используя их, например, для задания особого акцентного цвета для некоторых блоков:

:root {--primary-hs: 250 30%;--secondary-hs: 190 40%;}article {background: hsl(var(--primary-hs) 90%);}article.alt {--primary-hs: var(--secondary-hs);}



Пример страницы, акцентный цвет одного из блоков которой переопределён

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

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

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

Основная идея моего подхода заключается в том, чтобы использовать пользовательские CSS-свойства для хранения сведений о светлоте (lightness) цветов, а не полной информации о цветах. В тёмном режиме эти переменные переопределяются, в них записываются значения, вычисляемые по формуле 100% lightness. Это, в целом, позволяет получить светлые цвета из тёмных цветов, цвета средней светлоты из цветов средней светлоты, и тёмные цвета из светлых цветов. При таком подходе вполне можно пользоваться встроенными определениями цветов, необязательно применять переменные при настройке абсолютно всех цветов проекта. Вот как, учитывая вышесказанное, могут выглядеть стили демонстрационного проекта:

root {--primary-hs: 250 30%;--secondary-hs: 190 40%;--l-0: 0%;--l-30: 30%;--l-40: 40%;--l-50: 50%;--l-90: 90%;--l-100: 100%;}@media (prefers-color-scheme: dark) {:root {--l-0: 100%;--l-30: 70%;--l-40: 60%;--l-90: 10%;--l-100: 0%;}}body {background: hsl(0 0% var(--l-100));color: hsl(0 0% var(--l-0));}h1 {color: hsl(var(--primary-hs) var(--l-30));}article {background: hsl(var(--primary-hs) var(--l-90));}article h2 {background: hsl(var(--primary-hs) 40%);color: white;}footer {color: hsl(0 0% var(--l-40));}

Ниже показан вид страницы этого проекта в светлом и тёмном режимах.


Светлая тема


Тёмная тема

Учитывайте то, что цвета для тёмной темы получены автоматически.

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


Тёмная тема, при применении которой цвет заголовков статей не изменился

При создании этого варианта нашей тёмной темы автоматическая настройка цветов использована не для всех без исключения элементов. А именно, тут оставлен цвет фона и цвет текста для article > h2.

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

Проблема с цветовой моделью HSL


Почему заголовки статей легче читать в том случае, когда они, в тёмной теме, выводятся тем же цветом, что и в светлой, а не цветом, светлота которого инвертирована? Главная причина этого кроется в том, что компонент HSL-цвета, отвечающий за светлоту, на самом деле, не имеет отношения к той светлоте, которую воспринимает человек. В результате одинаковые изменения светлоты могут приводить к очень разным изменениям яркости цветов, воспринимаемой человеком.

Именно это и можно назвать большой проблемой данного подхода: тут предполагается, что HSL-светлота имеет некий реальный смысл, но, и мы уже об этом говорили, на самом деле, это не так. Жёлтый и синий цвета, имеющие одну и ту же HSL-светлоту (50%), выглядят очень по-разному. Кроме того, анализируя HSL-цвета, можно обратить внимание на то, что тёмные цвета имеют меньшие отличия друг от друга, чем светлые цвета, так как цветовая модель HSL не является однородной для восприятия (в отличие от LCH и Lab).

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

На самом деле всё не так уж и мрачно.

Довольно скоро мы сможем пользоваться в браузерах LCH-цветами. Первая браузерная реализация поддержки таких цветов недавно появилась в Safari, производители других браузеров тоже работают в этом направлении.

Цветовое пространство LCH подходит для реализации моей методики гораздо лучше HSL, так как LCH-светлота далеко не бессмысленный показатель, это не просто разные варианты одного и того же цвета, различающиеся по яркости. LCH-светлота имеет отношение и к тону (hue) цвета, и к его цветности (chroma).

Для того чтобы вживую посмотреть этот пример вам понадобится Safari TP 120+. Сравните два показанных там градиента. Верхний это разные HSL-цвета со светлотой 50%, а нижний это LCH цвета с той же светлотой. В верхней части окна есть слайдер, который позволяет посмотреть градиенты для другой светлоты. Если у вас нет Safari TP 120+ ниже приведён скриншот примера.


HSL- и LCH-цвета с одной и той же светлотой

Обратите внимание на то, что некоторые HSL-цвета (вроде жёлтого и светло-голубого) гораздо светлее других. А вот все LCH-цвета одной светлоты имеют, собственно, одинаковую светлоту.

Тут стоит учитывать то, что компонент Chroma (цветность) модели LCH, на самом деле, не связан напрямую с компонентом Lightness (светлота) модели HSL. Поэтому, даже несмотря на то, что мы устанавливаем их в одинаковые значения, это не приводит к одним и тем же результатам.

Как адаптировать предложенную мной методику создания тёмных тем к работе с LCH-цветами?

Я использовала этот инструмент для преобразования существующих HSL-цветов в LCH-цвета, затем вручную немного подстроила получившиеся значения, так как цвета, после конверсии, не выглядели достаточно хорошо во всех вариантах LCH-светлоты. Обратите внимание на то, что HSL-цвета с одними и теми же тоном и насыщенностью могут иметь различные тон и цветность при их преобразовании к LCH-цветам. Обратное попросту сделало бы использование LCH-цветов бессмысленным!

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


Светлый вариант сайта, созданного с применением LCH-цветов


Тёмный вариант сайта, созданного с применением LCH-цветов

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


Слева тёмная тема, основанная на модификации HSL-цветов, справа тёмная тема, при создании которой использованы LCH-цвета

А ниже приведено анимированное сравнение этих двух тем.

lea.verou.me/wp-content/uploads/2021/03/hsl-dm.png

Анимированное сравнение тёмных тем

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

Автоматизация генерирования переменных, хранящих сведения о светлоте цветов


Если вы пользуетесь препроцессором, который поддерживает циклы, например Sass, это значит, что вы можете автоматизировать генерирование переменных, хранящих сведения о светлоте цветов. При таком подходе легко создать набор переменных, хранящих значения светлоты, изменяющиеся с небольшим шагом, например 5%:

:root {@for $i from 0 through 20 {--l-#{$i * 5}: #{$i * 5}%;}}@media (prefers-color-scheme: dark) {:root {@for $i from 0 through 20 {--l-#{$i * 5}: #{100 - $i * 5}%;}}}

Можно ли сделать работу с переменными, хранящими сведения о светлоте, лучше соответствующей принципу DRY?


Кому-то из вас может не понравиться повторение значений. Например, нам нужно объявить переменную --l-40 и, для использования в светлом режиме, записать в неё 40%, а потом, при переходе в тёмный режим, в неё нужно записать 60%. Можно ли как-то получить это значение, просто вычитая уже имеющееся у нас значение из 100%?

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

--l-40: calc(100% - var(--l-40));

Но работать эта конструкция не будет. CSS это не императивный язык программирования. Тут нет неких этапов вычислений, не выполняются какие-то шаги, когда переменные имеют разные значения до и после выполнения такого шага. Здесь нет концепции времени все имеющиеся объявления вступают в силу одновременно. Работа CSS больше похожа на реактивное вычисление значений ячеек электронной таблицы с формулами, чем на вычисления, которые выполняют в JavaScript и в других языках программирования (есть универсальные реактивные языки программирования, но они не особенно широко известны). В результате объявления, похожие на то, которое показано выше, считаются циклическими: переменная --l-40 не может ссылаться сама на себя поэтому возникает ошибка. При исправлении этой ошибки в дело вступает механизм, который устанавливает переменную --l-40 в её исходное значение (это происходит из-за того, что CSS не выбрасывает ошибок).

Итак, есть ли способ избежать двойного объявления переменных для хранения сведений о светлоте цвета, то есть одной переменной для светлой темы, а второй для тёмной?

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

Вместо того, чтобы записывать в --l-40 значение 40%, мы запишем в эту переменную значение, выражающее отличие необходимого нам значения от 50%. То есть -10%. В результате конструкция calc(50% + var(--l-40)) даст нам 40%, а конструкция calc(50% var(--l-40)) даст 60%. Это именно те значения, которые нам и нужны. А значит мы можем объявить единственную переменную, которая равняется -1 в тёмном режиме и 1 в светлом. Потом мы просто будем умножать соответствующие значения на эту переменную.

Вот фрагмент кода, созданного с учётом вышеприведённых рассуждений:

:root {--dm: 1;/* Пример объявления переменной: */--l-40: -10%;}@media (prefers-color-scheme: dark) {:root {--dm: -1;}}/* Пример использования имеющихся переменных: */footer {color: hsl(0 0% calc(50% + var(--dm) * var(--l-40));/* Некрасиво! */}

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

Как вы создаёте светлые и тёмные темы для своих проектов?

Подробнее..

Перевод Создание видеочата с помощью Node.js Socket.io WebRTC

06.04.2021 18:11:06 | Автор: admin

Сегодня, специально к старту нового потока по веб-разработке, поделимся с вами туториалом, из которого вы узнаете, как создать видеочат с помощью JavaScript и NodeJS. Также вы научитесь использовать PeerJS, WebRTC и Socket.io.


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

Подготовка к проекту

Вот что вам понадобится:

  • NodeJS: посетите официальный веб-сайт Node.js, чтобы загрузить и установить Node;

  • NPM: программа NPM устанавливается на ваш компьютер при установке Node.js.

Настройка проекта

Весь код этого проекта можно найти в репозитории GitHub.

  1. Создайте пустой каталог с именем video-chat-app.

  2. Откройте консоль, перейдите в наш новый каталог и запустите npm init.

  3. Заполните необходимую информацию для инициализации нашего проекта.

  4. Запустите npm install express ejs socket.io uuid peer. Команда установит все зависимости, необходимые для создания этого приложения.

  5. А также в качестве dev-зависимости установите Nodemon. Нужно выполнить npm install-dev nodemon. Это установит nodemon как dev-зависимость.

  6. Создайте файл server.js в этом файле будет храниться вся ваша серверная логика.

Теперь, когда у вас настроен наш проект, вы можете приступить к созданию приложения!

Создание сервера (с Express JS)

Первое, что вам нужно сделать, это запустить ваш сервер. Мы собираемся использовать для этого Express. Express это минималистичный веб-фреймворк для Node.js. Express позволяет очень легко создавать и запускать веб-сервер с помощью Node.

Давайте создадим шаблонный файл начального приложения Express.

// server.jsconst express = require(express);const app = express();const server = require(http).Server(app);app.get(/, (req, res) => {    res.status(200).send(Hello World);});server.listen(3030);

Теперь ваш сервер запущен, вы можете протестировать его, запустив:

> nodemon server.js

Теперь откройте свой браузер и перейдите по адресу: localhost:3000, вы должны увидеть Hello World.

Создание первой страницы

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

Чтобы использовать EJS в Express, вам нужно настроить ваш шаблонизатор. Для настройки добавьте эту строку кода в файл server.js.

app.set(view engine, ejs)

Доступ к EJS по умолчанию осуществляется в каталоге views. Теперь создайте новую папку views в каталоге. В этой папке добавьте файл с именем room.ejs. Пока что думайте о нашем файле room.ejs как о HTML-файле.

Вот как выглядит ваша файловая структура:

|-- video-chat-app   |-- views      |-- room.ejs   |-- package.json   |-- server.js

Теперь добавьте HTML-код в файл room.ejs.

Как только вы скопируете приведённый выше код, нужно немного поменять app.js:

app.get(/, function (req, res) { // OLD CODE res.status(200).send("Hello World");})

Выше приведён старый код, в котором вы отправляете клиенту текст Hello World!. Вместо этого вы хотите отправить файл room.ejs:

app.get(/, function (req, res) { // NEW CODE res.render(room);})

Теперь откройте браузер и перейдите по адресу: localhost:3030, и вы увидите, что отображается файл room.ejs!

Добавление CSS

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

Нам нужно будет добавить новую папку в проект под названием public. В этой папке создайте файлы style.css и script.js. Вот ваша новая файловая структура:

|-- weather-app   |-- views      |-- index.ejs   |-- public      |-- style.css      |-- script.js   |-- package.json   |-- server.js

Express не даёт доступа к этому файлу по умолчанию, поэтому вам нужно открыть его с помощью следующей строки кода:

app.use(express.static(public));

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

После того как вы добавили CSS, вы можете посетить: localhost:3030. Вы заметите, что приложение выглядит немного лучше.

Настройка комнат

К настоящему моменту ваш файл server.js должен выглядеть так:

У вас есть один GET-роут и запуск сервера. Однако, чтобы ваше приложение работало, нужно всякий раз, когда новый пользователь посещает ваш роут по умолчанию, перенаправлять его на уникальный URL-адрес. Следует использовать библиотеку uuid для создания случайного уникального URL-адреса для каждой комнаты.

UUID это библиотека javascript, которая позволяет вам создавать уникальные идентификаторы. В вашем приложении вы будете использовать uuid версии 4 для создания уникального URL. Но сначала импортируйте uuid в server.js.

const { v4: uuidv4 } = require("uuid");

Теперь нужно использовать uuid для создания случайного уникального идентификатора для каждой комнаты и перенаправлять пользователя в эту комнату.

app.get(/, (req, res) => {    res.redirect(`/${uuidv4()}`);});

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

app.get(/:room, (req, res) => {    res.render(room, { roomId: req.param.room });});

Вы передали roomId в room.ejs на этом закончили настройку ваших комнат. А теперь, если вы посетите localhost:3030, вы будете перенаправлены на уникальный URL.

Добавление видео пользователя

Вы будете работать с файлом script.js, который вы создали ранее. script.js будет содержать весь клиентский код приложения.

Итак, вот что необходимо сделать: нужно получить видеопоток, а затем добавить этот поток в элемент видео.

let myVideoStream;const videoGrid = document.getElementById("video-grid");const myVideo = document.createElement("video");myVideo.muted = true;navigator.mediaDevices.getUserMedia({    audio: true,    video: true,}).then((stream) => {    myVideoStream = stream;    addVideoStream(myVideo, stream);});

Теперь создайтем функцию addVideoStream, которая добавит поток к видеоэлементу.

const addVideoStream = (video, stream) => {    video.srcObject = stream;    video.addEventListener("loadedmetadata", () => {       video.play();       videoGrid.append(video);    });};

Этот код добавит пользовательский поток к видеоэлементу. Вы можете проверить это, посетив localhost:3030, и вы увидите всплывающее окно с видео

Добавление возможности разрешить другим пользователям транслировать свои видео в потоковом режиме.

Пришло время использовать Socket.io и PeerJS. Для тех, кто не знает, Socket.io позволяет взаимодействовать серверу и клиенту в режиме реального времени. PeerJS позволяют реализовать WebRTC.

Сначала импортируйте socket.io и peerjs в server.js и прослушайте событие соединения.

// server.jsconst express = require(express);const app = express();const server = require(http).Server(app);const { v4: uuidv4 } = require(uuid);app.set(view engine, ejs);const io = require(socket.io)(server);const { ExpressPeerServer } = require(peer);const peerServer = ExpressPeerServer(server, {    debug: true,});app.use(/peerjs, peerServer);app.use(express.static(public));app.get(/, (req, res) => {    res.redirect(`/${uuidv4()}`);});app.get(/:room, (req, res) => {    res.render(room, { roomId: req.param.room });});io.on(connection, (socket) => {    socket.on(join-room, (roomId, userId) => {    socket.join(roomId);    socket.to(roomId).broadcast.emit(user-connected, userId);    });});server.listen(3030);

Теперь ваш сервер прослушивает событие присоединения к комнате. Далее настройте ваш script.js.

// public/script.jsconst socket = io(/);const videoGrid = document.getElementById(video-grid);const myVideo = document.createElement(video);myVideo.muted = true;var peer = new Peer(undefined, {  path: /peerjs,  host: /,  port: 3030,});let myVideoStream;navigator.mediaDevices  .getUserMedia({  audio: true,  video: true,}).then((stream) => {  myVideoStream = stream;  addVideoStream(myVideo, stream);  peer.on(call, (call) => {    call.answer(stream);    const video = document.createElement(video);    call.on(stream, (userVideoStream) => {      addVideoStream(video, userVideoStream);      });    });  socket.on(user-connected, (userId) => {      connectToNewUser(userId, stream);  });});const connectToNewUser = (userId, stream) => {  const call = peer.call(userId, stream);  const video = document.createElement(video);  call.on(stream, (userVideoStream) => {      addVideoStream(video, userVideoStream);  });};peer.on(open, (id) => {    socket.emit(join-room, ROOM_ID, id);});const addVideoStream = (video, stream) => {  video.srcObject = stream;  video.addEventListener(loadedmetadata, () => {    video.play();    videoGrid.append(video);  });};

Теперь, если в комнату войдёт новый пользователь, вы увидите его видео.

Создание пользовательского интерфейса

С видеочастью закончили. А теперь займитесь стилизацией. Но сначала добавьте контент в файл room.ejs. (Добавьте CDN font-awesome внутри тега head.)

// views/room.ejs<body>  <div class="header">    <div class="logo">      <h3>Video Chat</h2>    </div>  </div>  <div class="main">    <div class="main__left">      <div class="videos__group">        <div id="video-grid"></div>      </div>      <div class="options">        <div class="options__left">          <div class="options__button">            <i class="fa fa-video-camera" aria-hidden="true"></i>          </div>        <div class="options__button">            <i class="fa fa-microphone" aria-hidden="true"></i>        </div>       </div>       <div class="options__right">         <div class="options__button background__red">           <i class="fa fa-phone" aria-hidden="true"></i>         </div>       </div>     </div>    </div>    <div class="main__right">      <div class="main__chat_window">        <ul class="messages"></ul>     </div>     <div class="main__message_container">       <input id="chat_message" type="text" placeholder="Type message here...">       <div class="options__button">         <i class="fa fa-plus" aria-hidden="true"></i>       </div>     </div>    </div>  </div></body>

Затем откройте файл style.css и добавьте немного CSS.

@import url(http://personeltest.ru/aways/fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap");:root {   main-darklg: #1d2635;   main-dark: #161d29;   primary-color: #2f80ec;   main-light: #eeeeee;  font-family: Poppins, sans-serif;}* {  margin: 0;  padding: 0;}.header {  display: flex;  justify-content: center;  align-items: center;  height: 8vh;  width: 100%;  background-color: var(  main-darklg);}.logo > h3 {    color: var(  main-light);}.main {  overflow: hidden;  height: 92vh;  display: flex;}.main__left {  flex: 0.7;  display: flex;  flex-direction: column;}.videos__group {  flex-grow: 1;  display: flex;  justify-content: center;  align-items: center;  padding: 1rem;  background-color: var(  main-dark);}video {  height: 300px;  border-radius: 1rem;  margin: 0.5rem;  width: 400px;  object-fit: cover;  transform: rotateY(180deg);  -webkit-transform: rotateY(180deg);  -moz-transform: rotateY(180deg);}.options {  padding: 1rem;  display: flex;  background-color: var(  main-darklg);}.options__left {    display: flex;}.options__right {    margin-left: auto;}.options__button {  display: flex;  justify-content: center;  align-items: center;  background-color: var(  primary-color);  height: 50px;  border-radius: 5px;  color: var(  main-light);  font-size: 1.2rem;  width: 50px;  margin: 0 0.5rem;}.background__red {    background-color: #f6484a;}.main__right {flex: 0.3;    background-color: #242f41;}.main__chat_window {    flex-grow: 1;}.main__message_container {  padding: 1rem;  display: flex;  align-items: center;  justify-content: center;}.main__message_container > input {  height: 50px;  flex: 1;  border-radius: 5px;  padding-left: 20px;  border: none;}#video-grid {  display: flex;  justify-content: center;  flex-wrap: wrap;}

Вот и всё! Поздравляем, вы успешно создали видеочат! Теперь вы можете развернуть его на Heroku и показать его всему миру. Демо и исходный код.

Это лишь небольшой пример того, какие вещи может делать веб-разработчик, причем в одиночку. Сделать just for fun экспорт музыки в Spotify и получить известность, пока огромная компания долго думает над решением задачи без проблем. За один вечер набросать и выкатить расширение для браузера, которое упростит жизнь миллионам пользователей тоже по силам. На что еще способна веб-разработка зависит только от фантазии программиста. Приходите учиться, чтобы освоить дзюцу веб-разработки и стать настоящим самураем интернета.

Узнайте, как прокачаться в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 462 (5 11 апреля 2021)

12.04.2021 00:19:09 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript|Браузеры


Медиа


podcast Новости 512 от CSSSR: TypeScript 4.3 beta, воркшоп по TDD c React, Husky 6, сравнение Hyperapp с React, RGB и HSL
podcast Новости 512 от CSSSR: Prototype pollution, Tailwind CSS 2.1, Ruby 3.0.1, Cypress 7.0, security-обновления Node.js
podcast Подкаст Веб-стандарты 276. Safari, static в V8, Deno, Sublime Text, HSL и LCH, F1, WordPress, SPA, гидрация и Next.js
podcast Подкаст Фронтенд Юность #181 Эйблизм открытого ПО

Веб-разработка


habr Веб-империя правительства UK: все во имя человека, для блага человека
habr DevTools для чайников
PWA для SSR приложения в 5 строк на Workbox 6.
en Руководство по SEO в Jamstack
en HTML атрибут ping для якорных элементов (ссылок)
en Обновление архитектуры DevTools: перенос DevTools на TypeScript
en Создание переключателя с поддержкой доступности
en Эффект перехода на полноразмерную страницу по клику на превью

CSS


habr 5 плохих CSS практик
en Псевдоклассы CSS :where и :is
en Устранение проблем с темным режимом Gmail с помощью CSS Blend Modes
en Современные обновления CSS для улучшения доступности
en Инспектирование элементов как способ удовлетворить свое любопытство
en Возвращение к CSS Pie таймеру

JavaScript


Спецификация ECMAScript 2021 для JavaScript приближается к финишу
en Шпаргалка по переходу с jQuery на ванильный JavaScript
en Сниппеты кода на ванильном JavaScript
en Создание умной панели навигации на ванильном JavaScript
en Лечебная сила JavaScript
en 5 способов предотвратить внедрение кода в JavaScript и Node.js
en Замена let на const
en JS классы это не просто синтаксический сахар







Браузеры


habr Шпион, выйди вон: что делают браузеры после установки?
Google Chrome заблокировал HTTP, HTTPS и FTP-доступы на порте 10080 из-за хакерской угрозы
10 малоизвестных возможностей браузера Google Chrome

Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 463 (12 18 апреля 2021)

19.04.2021 00:19:54 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript|Браузеры


Медиа


podcast Новости 512 от CSSSR: Chrome 90, Deno 1.9, анализ производительности JS, сборщики, верстка писем, pnpm 6, ESLint 7.24.0
podcast Подкаст proConf #92: GatsbyConf 2021
podcast Подкаст Сделайте мне красиво 60 Единственный фронтендер, который откладывает яйца
podcast Подкаст Фронтенд Юность #183: Путь от идеи до популярного OpenSource проекта
podcast Подкаст Да как так-то?. Выпуск 2: Тимлиды, проектные менеджеры, тестировщики кто все эти люди?

Веб-разработка


habr Малоизвестные, но крутые атрибуты в HTML
habr Микрофронтенды: разделяй и властвуй
en Полное руководство по созданию шаблонов HTML-писем
en Практическая доступность, часть 2: дайте имя (почти) всему
en Новости платформы: Использование :focus-visible, новый шрифт BBC, Declarative Shadow DOMs, A11Y иплейсхолдеры
en Медленно и осторожно: конвертация всего интерфейса Sentry на TypeScript
en Напряжение между Wix и WordPress растет




CSS


habr Нестандартные шрифты: как подключить и оптимизировать
habr Какие CSS-генераторы можно использовать в 2021 году
habr Пользовательские CSS-переменные, инверсия светлоты цветов и создание тёмной темы за 5 минут
habr CSS: работа с текстом на изображениях
Tailwind CSS: to use, или not to use?
en Tailwind UI: теперь с поддержкой React + Vue
en Проблемы с Overflow в CSS
en Как подружить стили с Fullscreen API
en Скажите привет CSS Container Queries
en CSS это строго типизированный язык
en Руководство для новичков по новым утилитам в Bootstrap 5
en Используйте Reseter.css вместо Normalize и Reset.css. Чтобы улучшить кроссбраузерность.


JavaScript


habr Типобезопасность в JavaScript: Flow и TypeScript
habr Работа с датой и часовыми поясами в JavaScript
en Изменение размера изображения в зависимости от контета с помощью JavaScript
en Работа со строками в современном JavaScript
en Генераторы JavaScript: превосходный async/await
en Другой подход к архитектуре фронтенда





Браузеры


habr Вышел Chrome 90
Включение поддержки HTTP/3 в Firefox намечено на конец мая
В Firefox 90 будет удалён код, обеспечивающий поддержку FTP
Разработчики Vivaldi и Brave отказались использовать FLoC от Google, призванный заменить сторонние cookie
В Microsoft Edge появился специальный детский режим
en В Firefox Nightly и Beta появилась поддержка QUIC и HTTP / 3
en WebKit: Представляем CSS Grid Inspector

Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 464 (19 25 апреля 2021)

26.04.2021 00:06:32 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript|Браузеры|Занимательное|


Медиа


podcast Callback Hell записи аудио-стримов о технологиях и не только от CSSSR
podcast Подкаст Фронтенд Юность #182: Не нужно платить разработчикам 200-300к
podcast Новости 512 от CSSSR: релиз Node.js 16 и Firefox 88, проектирование приложения с TypeScript и ООП, взгляд на Container Query, доклады с Я.Субботника.
podcast Новости 512 от CSSSR: Микрофронтенды в Delivery Club, JS-классы, состояние JS-фреймворков и стейт-менеджеров, минусы Dart
video IT-дебаты: JavaScript-программист vs фронтенд-разработчик
habr video Frontend Meetup 20/04
video Я.Субботник по разработке интерфейсов 2021


Веб-разработка


habr CORS для чайников: история возникновения, как устроен и оптимальные методы работы
habr Как создают и поддерживают веб-страницы tinkoff.ru
habr Трёхпроходный алгоритм рефакторинга Front End
en Что нового в DevTools (Chrome 91)
en Независимые компоненты: новые строительные блоки для веба
en Как я создал свой блог



CSS


habr Полное руководство по CSS Flex + опыт использования
CSS-нестинг больше, чем сахар
en Руководство по новым современным псевдо-селекторам CSS
en Создание (и потенциальные преимущества) CSS-шрифта
en Как добавить двойную границу к SVGShapes
en Начало работы с CSS Custom Properties
en TailwindCSS: добавляет сложности, ничего не делает.
en Работа на ошибками гибкой типографии, базирующихся на вьюпорте в Safari


JavaScript


habr Основы управления памятью в JavaScript: как это работает и какие проблемы могут возникнуть
Выпуск серверной JavaScript-платформы Node.js 16.0
en Чудесный мир Javascript бандлеров
en Улучшите управление состоянием в вашем фронтенде с помощью view models
en Шаблон для свойства отложенной загрузки в JavaScript
en Полное руководство по инкрементной статической регенерации (ISR) с Next.js
en Топ-5 самых популярных вопросов о JavaScript на Stack Overflow
en Руководство по MobX
en Понимание Array Reduce в JavaScript





Браузеры


В Firefox 88 молча удалён пункт контекстного меню Page Info
Apple, Microsoft, Opera и другие разработчики не горят желанием поддерживать технологию Google FLoC
Релиз Firefox 88
В Microsoft Edge тестируется новый режим производительности с иным принципом работы спящих вкладок


Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Перевод Как обеспечить глассморфизм с помощью HTML и CSS

27.04.2021 16:10:26 | Автор: admin

Вы, наверное, подумаете ну вот еще один тренд дизайна? Разве они у нас не каждый год появляются или около того?

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

Но давайте поговорим немного больше о глассморфизме.

Что такое глассморфизм?

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

Вот пример:

Это пример из библиотеки CSS UI, основанной на глассморфизме, называемой ui.glass.

Как видите, эффект используется для карточки, которая содержит пример кода справа, в отличие от другой карточки на заднем плане.

Другим примером является редизайн Facebook Messenger App с использованием глассморфизма для MacOS:

Редизайн был выполнен Mikoaj Gaziowski на Dribbble.

Глассморфизм также используется такими компаниями как Apple и Microsoft

Это еще одна причина, по которой эта тенденция, вероятно, не просто проходит мимо, а до сих пор здесь, чтобы остаться. С выходом обновления Big Sur для MacOS, это было первое широкомасштабное распространение этого нового дизайнерского тренда крупной компанией.

Microsoft также использует этот стиль в Fluent Design System, но они называют его "акриловым материалом", а не глассморфизмом.

Вот как это выглядит:

Итак, теперь, когда я вкратце познакомил вас с гласcморфизмом, позвольте показать, как можно применить этот эффект, используя только HTML и CSS.

Давайте начнем

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

Элемент, который мы собираемся построить, будет выглядеть так:

Начнем с создания HTML-файла со следующим кодом:

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta http-equiv="X-UA-Compatible" content="IE=edge">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>Glassmorphism effect</title></head><body>    <!-- code goes here --></body></html>

Здорово! Теперь давайте также добавим пользовательский стиль шрифта, включая Inter из Google Fonts:

<link rel="preconnect" href="http://personeltest.ru/aways/fonts.gstatic.com"><link href="http://personeltest.ru/aways/fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">

Настройка некоторых основных стилей и фона для тега body:

body {  padding: 4.5rem;  margin: 0;  background: #edc0bf;  background: linear-gradient(90deg, #edc0bf 0,#c4caef 58%);  font-family: 'Inter', sans-serif;}

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

<div class="card">    <h3 class="card-title">Glassmorphism is awesome</h3>    <p>A modern CSS UI library based on the glassmorphism design principles that will help you quickly design and build beautiful websites and applications.</p>    <a href="http://personeltest.ru/aways/ui.glass">Read more</a></div>

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

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

.card {  width: 400px;  height: auto;  padding: 2rem;  border-radius: 1rem;}.card-title {  margin-top: 0;  margin-bottom: .5rem;  font-size: 1.2rem;}p, a {  font-size: 1rem;}a {  color: #4d4ae8;  text-decoration: none;}

Здорово! Теперь, когда мы заложили основу для эффекта, давайте посмотрим, как мы сможем его применить.

Создание эффекта глассморфизма, используя HTML и CSS

Для применения эффекта нужны только два важных свойства CSS: прозрачный фон и свойства backdrop-filter: blur(10px); . Степень размытости или прозрачности может быть изменена в зависимости от ваших предпочтений.

Добавьте к элементу .card следующие стили:

.card {/* other styles */background: rgba(255, 255, 255, .7);-webkit-backdrop-filter: blur(10px);backdrop-filter: blur(10px);}

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

Давайте добавим изображение сразу после запуска тега body :

<img class="shape" src="http://personeltest.ru/aways/s3.us-east-2.amazonaws.com/ui.glass/shape.svg" alt="">

Затем примените следующие стили к элементу .shape с помощью CSS:

.shape {  position: absolute;  width: 150px;  top: .5rem;  left: .5rem;}

Потрясающе! Окончательный результат должен выглядеть так:

Если вам нужен код для этого туториала, посмотрите на этот код.

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

Одним из недостатков нового тренда дизайна является то, что Internet Explorer не поддерживает свойство backdrop-filter, а Firefox отключает его по умолчанию.

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

Вы можете проверить поддержку браузеров на сайте caniuse.com.

Заключение

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

Мы с моим другом из Themesberg работали над новой библиотекой CSS UI, которая будет использовать новое направление глассморфизма в дизайне, называемое ui.glass. Она будет иметь открытый исходный код под лицензией MIT.

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

Спасибо за чтение! Оставьте свои мысли о глассморфизме в разделе комментариев ниже.


Прямо сейчас в OTUS открыт набор на онлайн-курс "HTML/CSS".

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

- Узнать подробнее о курсе "HTML/CSS"

- Смотреть вебинар CSS Reset ненужный артефакт или спасательный круг

Подробнее..

Перевод Контейнерные запросы в CSS

28.04.2021 18:07:33 | Автор: admin

Как фронтенд-дизайнер я за последние 6 лет не был так взволнован новой CSS-функцией, как сейчас. Благодаря усилиям Мириам Сюзанны и других умных людей прототип контейнерных запросов можно включить в Chrome Canary

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


Проблема с медиазапросами

Веб-страница состоит из разных разделов и компонентов, которые мы делаем отзывчивыми при помощи медиазапросов CSS. В этом нет ничего плохого, но есть ограничения. Например, мы можем написать медиазапрос, чтобы показать минимальную версию компонента на мобильном или настольном компьютере. Часто отзывчивый веб-дизайн не имеет отношения к видовому экрану или к размеру экрана. Он касается размера контейнера. Рассмотрим такой пример:

Вот очень типичный макет с компонентом карточкой. И два варианта:

  • Стопкой (смотрите aside).

  • Горизонтально (смотрите main).

Реализовать такое на CSS можно несколькими способами, вот самый распространённый: нам нужно создать базовый компонент, а затем написать его вариации.

.c-article {  /* The default, stacked version */}.c-article > * + * {  margin-top: 1rem;}/* The horizontal version */@media (min-width: 46rem) {  .c-article--horizontal {    display: flex;    flex-wrap: wrap;  }  .c-article > * + * {    margin-top: 0;  }  .c-article__thumb {    margin-right: 1rem;  }}

Обратите внимание, что мы описали класс .c-article--horizontal для работы с горизонтальной версией компонента. Если ширина видового экрана больше 46rem, компонент должен переключаться на горизонтальную версию. Это не плохо, но каким-то образом заставляет ощущать себя ограниченным. Хочется, чтобы компонент реагировал на ширину своего родительского компонента, а не на видовой экран браузера или размер экрана.

Считайте, что мы хотим использовать стандартную .c-c-card в разделе main. Что произойдёт? Ну, карта расширится до ширины своего родителя и, следовательно, окажется слишком большой. Посмотрите на рисунок:

Это проблема, и мы можем решить её при помощи контейнерных запросов. (Есть! наконец-то!) До погружения в тему давайте взглянем на результат.

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

<div class="o-grid">  <div class="o-grid__item">    <article class="c-article">      <!-- content -->    </article>  </div>  <div class="o-grid__item">    <article class="c-article">      <!-- content -->    </article>  </div></div>
.o-grid__item {  contain: layout inline-size;}.c-article {  /* The default style */}@container (min-width: 400px) {  .c-article {    /* The styles that will make the article horizontal**        ** instead of a card style.. */  }}

Как помогут контейнерные запросы?

Предупреждение: контейнерные запросы CSS пока поддерживаются только в Chrome Canary с экспериментальным флагом.

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

Вот как я представляю себе это:

Фиолетовый контур это ширина родительского компонента. Обратите внимание, как компонент адаптируется к большему размеру своего родительского компонента. Разве это не потрясающе? Вот мощь контейнерных запросов CSS.

Как работают контейнерные запросы

С контейнерными запросами теперь можно поэкспериментировать в Chrome Canary. Чтобы включить их, перейдите в chrome://flags, найдите чекбокс "container queries" и отметьте его.

Сначала добавим свойство contain. Компонент будет адаптироваться под родительскую ширину, поэтому нужно сказать браузеру, чтобы он перерисовал не всю страницу, а только её изменяемую область. Заранее сообщить об этом браузеру мы можем при помощи свойства contain.

Значение inline-size означает, что компонент реагирует только на изменения ширины родительского элемента [прим. перев. в случае языков с вертикальным направлением, возможно, речь идёт о высоте]. Я попытался задействовать block-size, но это свойство ещё не работает. Пожалуйста, поправьте меня, если я ошибаюсь.

<div class="o-grid">  <div class="o-grid__item">    <article class="c-article">      <!-- content -->    </article>  </div>  <div class="o-grid__item">    <article class="c-article">      <!-- content -->    </article>  </div>  <!-- other articles.. --></div>
.o-grid__item {  contain: layout inline-size;}

Это первый шаг. Мы определили элемент .o-grid__item как родительский для .c-article. Следующий шаг добавить желаемые стили, чтобы контейнерные запросы работали.

.o-grid__item {  contain: layout inline-size;}@container (min-width: 400px) {  .c-article {    display: flex;    flex-wrap: wrap;  }  /* other CSS.. */}

@container это элемент .o-grid__item, а min-width: 400px его ширина. Мы даже можем пойти дальше и добавить больше стилей. На видео показано, чего можно добиться от компонентов:

У нас есть следующие стили:

  1. По умолчанию (вид карточки).

  2. Горизонтальная карточка с маленьким предпросмотром.

  3. Горизонтальная карточка с большим предпросмотром.

  4. Если родительский компонент слишком большой, стиль будет похож на стиль раздела hero, указывая, что это избранная статья.

Давайте углубимся в примеры применения контейнерных запросов.

Случаи применения контейнерных запросов CSS

Контейнерные запросы и CSS-грид с auto-fit

В некоторых случаях применение auto-fit в CSS-гриде приводит к неожиданным результатам. Например, компонент оказывается слишком широким, и его содержимое трудно читается. Дам немного контекста: вот визуальный элемент, показывающий разницу между auto-fit и auto-fill в гриде CSS:

Обратите внимание, что при использовании auto-fit элементы расширяются, чтобы заполнить доступное пространство. Однако в случае автоматического заполнения `` элементы грида не будут разрастаться, вместо этого у нас будет свободное пространство (пунктирный элемент в крайнем правом углу).

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

<div class="o-grid">  <div class="o-grid__item">    <article class="c-article"></article>  </div>  <div class="o-grid__item">    <article class="c-article"></article>  </div>  <div class="o-grid__item">    <article class="c-article"></article>  </div>  <div class="o-grid__item">    <article class="c-article"></article>  </div></div>
.o-grid {  display: grid;  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));  grid-gap: 1rem;}

Когда элемента четыре, результат должен выглядеть примерно так:

Что произойдёт, когда количество статей уменьшится? Чем меньше у нас будет элементов, тем они будут шире. Так происходит потому, что мы используем auto-fit. Первый выглядит хорошо, но последние два (2 на ряд, 1 на ряд) не очень: они слишком широкие:

Что делать, если каждый компонент статьи изменяется в зависимости от ширины родительского компонента? Если так, auto-fit будет очень хорошим преимуществом. Вот что нужно сделать: если ширина элемента грида превышает 400 пикселей, статья должна переключиться на горизонтальный стиль, а добиться этого можно так:

.o-grid__item {  contain: layout inline-size;}@container (min-width: 400px) {  .c-article {    display: flex;    flex-wrap: wrap;  }}

Кроме того, если статья единственный элемент в гриде, хочется отобразить её с разделом hero.

.o-grid__item {  contain: layout inline-size;}@container (min-width: 700px) {  .c-article {    display: flex;    justify-content: center;    align-items: center;    min-height: 350px;  }  .card__thumb {    position: absolute;    left: 0;    top: 0;    width: 100%;    height: 100%;    object-fit: cover;  }}

Вот и всё. У нас есть компонент, реагирующий на ширину родительского компонента, и он работает в любом контексте. Разве это не потрясающе? Посмотрите демо на CodePen.

aside и main

Часто нам нужно настроить компонент, чтобы он работал в контейнерах небольшой ширины, таких как <aside>. Идеальный пример раздел новостей. Когда ширина маленькая, нужно, чтобы её элементы складывались, а когда места достаточно, нужно горизонтальное расположение элементов.

Как видно на рисунке, мы работаем с разделом новостей в двух разных контекстах:

  • Раздел aside.

  • Раздел main.

Такое невозможно без контейнерных запросов, если у нас нет класса вариаций в CSS, например .newsletter--stacked или чего-то в этом роде.

Я знаю, что мы можем сказать браузеру, чтобы элементы были обтекающими в случае, когда flexbox не хватает пространства, но этого недостаточно. Мне нужно гораздо больше контроля, чтобы делать кнопку во всю ширину и прятать определённые элементы:

.newsletter-wrapper {  contain: layout inline-size;}/* The default, stacked version */.newsletter {  /* CSS styles */}.newsletter__title {  font-size: 1rem;}.newsletter__desc {  display: none;}/* The horizontal version */@container (min-width: 600px) {  .newsletter {    display: flex;    justify-content: space-between;    align-items: center;  }  .newsletter__title {    font-size: 1.5rem;  }  .newsletter__desc {    display: block;  }}

Вот видео с результатом.

Посмотрите демоверсию на CodePen.

Пагинация

Я обнаружил, что контейнерные запросы хорошо подходят для пагинации. Сначала у нас могут быть кнопки Previous и Next; если пространства достаточно, можно скрыть кнопки и показать всю нумерацию страниц. Посмотрим на рисунок ниже:

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

.wrapper {  contain: layout inline-size;}@container (min-width: 250px) {  .pagination {    display: flex;    flex-wrap: wrap;    gap: 0.5rem;  }  .pagination li:not(:last-child) {    margin-bottom: 0;  }}@container (min-width: 500px) {  .pagination {    justify-content: center;  }  .pagination__item:not(.btn) {    display: block;  }  .pagination__item.btn {    display: none;  }}

Посмотрите демоверсию на CodePen.

Карточка профиля

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

.p-card-wrapper {  contain: layout inline-size;}.p-card {  /* Default styles */}@container (min-width: 450px) {  .meta {    display: flex;    justify-content: center;    gap: 2rem;    border-top: 1px solid #e8e8e8;    background-color: #f9f9f9;    padding: 1.5rem 1rem;    margin: 1rem -1rem -1rem;  }  /* and other styles */}

При помощи этого кода теперь мы видим, как компоненты работают в разных контекстах без единого медиазапроса.

Посмотрите демоверсию на CodePen.

Элементы формы

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

.form-item {  contain: layout inline-size;}.input-group {  @container (min-width: 350px) {    display: flex;    align-items: center;    gap: 1.5rem;    input {      flex: 1;    }  }}

Посмотрите демо на CodePen.

Тестирование компонентов

Теперь, когда мы рассмотрели несколько случаев, когда контейнерные запросы могут быть полезны, возникает вопрос: как протестировать компонент? К счастью, мы можем сделать это с помощью свойства родительского компонента resize.

.parent {  contain: layout inline-size;  resize: horizontal;  overflow: auto;}

Об этом методе я узнал из этой замечательной статьи Брамуса Ван Дамма.

Легко ли отлаживать контейнерные запросы в DevTools?

Короткий ответ нет. Вы не увидите чего-то вроде @container (min-width: value). Я думаю, что это дело времени, поддержка такой отладки появится.

А запасной вариант для браузеров без контейнерных запросов?

Да! Конечно. Определённым образом можно предоставить альтернативу. Вот две замечательные статьи с объяснением, как это сделать:

Заключение

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

Если вы уже имеете некоторые навыки работы с CSS, но всё ещё не можете сказать, что разбираетесь во фронтенде можете обратить внимание на наш курс-профессию Frontend-разработчик, где вы сможете научиться создавать адаптивные веб-сайты сиспользованием CSS, Flexbox, разрабатывать интерактивные веб-сайты иприложения на JS иHTML, а также писать сложные компоненты на React и интерфейсы с авторизацией и с подключением к бэкенду.

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

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Перевод HTMHell адовая разметка

30.04.2021 00:08:01 | Автор: admin

Приветствую. Представляю вашему вниманию перевод заметок с сайтаHTMHell - коллекции плохих примеров HTML-кода, взятых из реальных проектов.

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

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

1. Кнопка, замаскированная под ссылку

Плохой код

<button role="link" title="Name of website" tabindex="0">  <img alt="Name of website" src="logo.jpg" title="Name of website"></button>

Ошибки и что следует исправить

  • Пример неправильного использования элемента <button>. Для ссылок на другую страницу или сайт следует использовать элемент <a> . Не пренебрегайте семантикой HTML-тегов, если только в этом нет явной потребности

  • Благодаря элементам <a>, на страницы можно ссылаться и без использования JavaScript

  • Атрибут title описывает содержимое элемента в виде всплывающей подсказки и для элементов <button> указывать его излишне

  • В атрибуте tabindex также нет необходимости, ведь при переключении между элементами с помощью клавиатуры кнопки получают фокус по умолчанию

Хороший код

<a href="http://personeltest.ru/aways/">  <img alt="Name of website" src="logo.jpg"></a>

2. Элемент с атрибутом role="button"

Плохой код

<div tabindex="-1">  <div role="button">    <svg width="28" height="24">  </svg>  </div></div>

Ошибки и что следует исправить

  • Нет необходимости пытаться задать семантику <div>-элемента с помощью атрибута role, ведь вместо этого достаточно просто использовать элемент <button>

  • При использовании <button> не понадобится и атрибут tabindex . HTML-кнопки по умолчанию могут получать фокус

  • На <div>-элементах событие клика вызывается только непосредственно кликом мыши. На элементах же <button> это происходит ещё и при нажатии на кнопки Enter или Space на клавиатуре

  • SVG-иконки, расположенной внутри нашей псевдокнопки не хватает текстовой альтернативы на случай, если SVG не отобразится

Хороший код

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

<button>  <span class="sr-only">Send</span>  <svg width="28" height="24" aria-hidden="true">  </svg></button>

Тексту присваивается класс .sr-only с набором свойств, делающим его скрытым только визуально

.sr-only {  position: absolute;  white-space: nowrap;  width: 1px;  height: 1px;  overflow: hidden;  border: 0;  padding: 0;  clip: rect(0 0 0 0);  clip-path: inset(50%);  margin: -1px;}

3. Картинки - кнопки

Плохой код

<img src="http://personeltest.ru/aways/habr.com/images/edit.gif" onclick="openEditDialog(123)"><img src="http://personeltest.ru/aways/habr.com/images/delete.gif" onclick="openDeleteDialog(123)">

Ошибки и что следует исправить

  • Элемент <img> предназначен отнюдь не для выполнения JavaScript, а для показа изображений

  • Как и на упомянутом ранее <div>, на элементе img> событие клика вызывается только непосредственно кликом мыши. Если бы вместо него использовался элемент <button> , это происходило бы ещё и при нажатии кнопок Enter или Space на клавиатуре

  • Для самого изображения не задана текстовая альтернатива (атрибут alt ). Из-за этого скринридеры могут озвучивать название самого файла изображения, что далеко не всегда информативно

Хороший код

Решение 1: Использовать кнопки, а к помещённым внутрь кнопок изображениям добавить атрибут alt

<button onclick="openEditDialog(123)">  <img src="http://personeltest.ru/aways/habr.com/images/edit.gif" alt="Edit product XY"></button><button onclick="openDeleteDialog(123)">  <img src="http://personeltest.ru/aways/habr.com/images/delete.gif" alt="Delete product XY"></button>

Решение 2: Использовать кнопки и вместо добавления атрибута alt к изображениям, добавить описание в текстовые элементы

<button onclick="openEditDialog(123)">  <span class="sr-only">Edit product XY</span>  <img src="http://personeltest.ru/aways/habr.com/images/edit.gif" alt=""></button><button onclick="openDeleteDialog(123)">  <span class="sr-only">Delete product XY</span>  <img src="http://personeltest.ru/aways/habr.com/images/delete.gif" alt=""></button>

Изображения с пустым атрибутом alt недоступны для пользователей скринридеров, что нам и нужно, ведь название кнопок мы добавили в виде отдельного текста, визуально скрытого с помощью набора свойств упомянутого выше класса .sr-only

.sr-only {  position: absolute;  white-space: nowrap;  width: 1px;  height: 1px;  overflow: hidden;  border: 0;  padding: 0;  clip: rect(0 0 0 0);  clip-path: inset(50%);  margin: -1px;}

4. Ссылка с кнопкой внутри

Плохой код

<a href="http://personeltest.ru/aways/example.com">  <button>Example</button></a>

Ошибки и что следует исправить

  • Вкладывая кнопку внутрь ссылки, вы подаёте сразу два сигнала: это кнопка, но также это и ссылка

  • Если вы не уверены, когда нужно использовать элемент <a>, а когда <button>, рекомендую посмотреть видео "The Links vs. Buttons Showdown" от Marcy Sutton

Хороший код

.button {  /* используйте CSS, чтобы задать ссылке вид кнопки */}
<a href="http://personeltest.ru/aways/example.com" class="button">Example</a>

5. Кнопкоподобная ссылка

Плохой код

Контекст: это ссылка, стилизованная под кнопку. Ведёт она на форму, расположенную на этой же странице

<a href="#form" role="button" aria-haspopup="true"> &nbsp;&nbsp;Register&nbsp;&nbsp; </a>

Ошибки и что следует исправить

  • Добавляя к ссылке role="button" , вы сообщаете, что это кнопка, хотя она ведёт себя как ссылка. Не меняйте семантику элементов, только если в этом нет серьезной необходимости

  • Атрибут aria-haspopup="true" призван сообщать вспомогательным устройствам, что данный элемент вызывает попап, но в нашем случае этого не происходит

  • Внутренний отступ padding следует добавлять к элементам через CSS, а не с помощью &nbsp;

Хороший код

.button {  /* с помощью CSS задайте ссылке вид кнопки  */}
<a class="button" href="#form"> Register </a>

6. Ссылка с void-оператором в значении атрибута "href"

Плохой код

<a href="javascript:void(1)" onClick='window.location="index.html"'>Link</a>

Ошибки и что следует исправить

  • Если JavaScript-код не загрузился или не может быть выполнен, использующая его ссылка просто перестанет работать

  • Да и для ссылки на другую страницу нет необходимости использовать JavaScript. Адрес можно указать в атрибуте href, который 100% поддерживается всеми браузерами и будет корректно работать

  • Такая ссылка будет работать только при клике левой кнопкой мыши. Открыть её в новой вкладке/окне щелчком скролла или через конктекстное меню не удастся

Хороший код

<a href="index.html">Link</a>

7. Дубликаты "id" и табличная раскладка

Плохой код

 <table>   <tr id="body">     <td id="body">       <table id="body">         <tr id="body_row">           <td id="body_left"></td>           <td id="body_middle"></td>           <td id="body_right"></td>         </tr>       </table>     </td>   </tr> </table>

Ошибки и что следует исправить

  • Значениея атрибутов id должны быть уникальными, независимо от того, к какому тегу они добавляются

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

  • Текущую разметку следует заменить на семантические HTML5-теги. Это существенно сократит количество тегов и сделает код более понятным

  • При стилизации следует использовать новые технологии FlexboxиCSS Grid, но никак не элементы таблиц

  • Для значений атрибута IDдолжны быть использованы более семантические термины

Хороший код

<main id="body">   <aside id="secondary_content"> </aside>   <article id="primary_content"> </article>   <aside id="tertiary_content"> </aside> </main>

8. Якорная ссылка в роли кнопки

Плохой код

<a href="#" onclick="modal.open()">Login</a>

Ошибки и что следует исправить

  • Элементы <a> следует использовать для ссылки на другие ресурсы: такие как страница или PDF-документ

  • В нашем же случае задача элемента вызвать JavaScript-действие на текущей странице. Для таких целей лучше подходит элемент <button> с атрибутом type="button" , потому что не имеет поведения по умолчанию и изначально предназначен для вызова действий в ответ на нажатие пользователем

  • Ещё одним серьёзным недочётом данного примера является то, что браузеры и устройства, которые не поддерживают JavaScript, не смогут получить доступ к содержимому модального окна

Хороший код

Решение 1: Использовать элемент <button>

<button type="button" onclick="modal.open()">Login</button>

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

Решение 2: Ссылка на отдельную страницу

<a href="http://personeltest.ru/aways/habr.com/login" onclick="modal.open()">Login</a>

Ещё одно решение - использовать элемент <a> , у которого в атрибуте hrefуказать путь на отдельную полноценную страницу, где пользователь сможет проделать те же действия, что и в модальном окне. А переход по указанному пути, который является поведением ссылки по умолчанию, заблокировать через JavaScript

Таким образом, получаем некий фолбэк для браузеров и устройств, которые не поддерживают JavaScript или если он по какой-то причине не сработал

Это пример прогрессивного улучшения при разработке

9. Запрос согласия на хранение Cookie

Плохой код

<body>  <header></header>  <main></main>  <footer></footer>  <div class="cookie_consent modal">    <p>We use cookies</p>    <div class="cookie_consent__close">      <i class="fa fa-times"></i>    </div>    <div class="cookie_consent__ok">OK</div>  </div></body>

Ошибки и что следует исправить

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

  • Кнопки данного окна, реализованные с помощью <div> элементов , а значит не получат фокус при переключении между элементами с помощью клавиатуры

  • Содержимое внутри <div>-кнопок семантически является просто текстом. Всё это не позволит вспомогательным технологиям вроде экранных читалок понять, что определённые элементы на самом деле являются кнопками

  • Как уже было указано ранее, в дополнение ко всему, на элементах <div> событие клика вызывается только непосредственно кликом мыши. Если бы всесто них использовались элементы <button>, это происходило ещё и при нажатии на кнопки Enter или Space на клавиатуре

  • Для иконки нет текстовой альтернативы, а значит её назначение можно определить только визуально

  • Font Awesome советует скрывать иконки от скринридеров, добавляя элементам <i> атрибут aria-hidden="true"

  • Font Awesome добавляет Unicode-содержимое через псевдоэлемент ::before. Некоторые вспомогательные технологии могут озвучивать его. Но в данном примере иконка будет названа "разы" (times), поскольку fa-times - это не "крестик", а "знак умножения". Обратите внимание: Talkback и VoiceOver в данном случае не озвучат вообще ничего

  • В завершение можно также добавить, что крайне полезным было бы иметь возможность закрыть модальное окно нажатием Escape

Хороший код

<body>  <div class="cookie_consent modal">    <h2 class="sr-only">Cookie notice</h2>    <p>We use cookies</p>    <button class="cookie_consent__ok">OK</button>    <button class="cookie_consent__close">      <span class="sr-only">Close notification</span>      <i class="fa fa-times" aria-hidden="true"></i>    </button>  </div>  <header></header>  <main></main>  <footer></footer></body>

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

.sr-only {  position: absolute;  white-space: nowrap;  width: 1px;  height: 1px;  overflow: hidden;  border: 0;  padding: 0;  clip: rect(0 0 0 0);  clip-path: inset(50%);  margin: -1px;}

Дополнительные материалы

10. Элемент как замена для

Плохой код

<section id="page-top">  <section data-section-id="page-top" style="display: none;"></section></section><main>  <section id="main-content">    <header id="main-header">      <h1>...</h1>      <section class="container-fluid">        <section class="row">          <article class="content col-sm-12">            <section class="content-inner">              <div class="content__body">                <article class="slider">                  <section class="slide">  </section>                </article>              </div>            </section>          </article>        </section>      </section>    </header>  </section></main>

Ошибки и что следует исправить

  • Элементы секционного содержимого (<article>, <aside>, <nav>, <section>) это разделы, которые потенциально можно как-то озаглавить

  • Секционные элементы вкладывать друг в друга можно, только это имеет смысл в ситуациях, когда содержимое внутренных элементов связано с содержимым родителя

  • В данном конкретном случае секционные элементы используются не для того, чтобы разбить содержимое на разделы, а лишь с целью стилизации. Значит большинство из них должны быть элементами <div>, не передающими семантическое значение

  • Когда пользователь переходит к элементу <section>, скринридеры могут озвучивать роль выбранного элемента region. Устройства также могут предоставлять возможность быстрой навигации по этим разделам. Использование большого количества элементов <section> (в том числе, вложенных) может сделать интерфейс излишне сложным для пользователей экранных читалок

  • Важный момент, который следует понимать элементы <section> не являются заменой <div>

  • Ещё одна ошибка в данном примере - неправильное использование элемента <header>. Обычно он содержит вводное содержимое для ближайшего родительского элемента <main> или другого секционного элемента. Если <header> не является вложенным, относится ко всей странице

  • Компонент карусели (слайдера) следует озаглавливать и связывать заголовок с главным элементом с помощью атрибута aria-labelledby чтобы позволить пользователям скринридеров легко его найти

Хороший код

<div id="page-top">  <div data-section-id="page-top" style="display: none;"></div></div><main>  <section id="main-content">    <header id="main-header">      <h1>...</h1>    </header>    <div class="container-fluid">      <div class="row">        <div class="content col-sm-12">          <div class="content-inner">            <section aria-labelledby="sliderheading" class="content__body">              <h2 id="sliderheading" hidden>New Products</h2>              <ul class="slider">                <li class="slide">  </li>              </ul>            </section>          </div>        </div>      </div>    </div>  </section></main>

Дополнительные материалы

11. Триграмма неба

Плохой код

<span class="nav-toggle">  Menu </span>

Ошибки и что следует исправить

  • Проблема данной реализации в том, что текст внутри тегов скринридеры могут озвучить как "триграмма неба меню". Причина кроется в символе "", который таким образом называется в unicode

  • Задача иконок - декоративное оформление, поэтому они должны быть скрыты от скринридеров. Рассмотрите добавление декоративных изображений с использованием CSS-свойства background

  • Так же, как на упомянутых ранее <div>, и <img>, на элементе <span> событие клика вызывается только непосредственно кликом мыши. Если вместо него использовать <button> , клик можно будет вызвать ещё и при нажатии на кнопок Enter и Space на клавиатуре.

  • Элемента <span> касаются и проблемы, связанные с невозможностью получения фокуса при переключении между элементами с помощью клавиатуры. А в нашем случае это особенно важно, ведь главная навигация открывается и скрывается именно с помощью данного элемента

  • Для улучшения доступности при открытии навигации к элементу следует добавлять атрибут aria-expanded для обозначения текущего состояния панели. Значением true если панель открыта, false если закрыта

Хороший код

<button class="nav-toggle" aria-expanded="false">  <span aria-hidden="true"></span> Menu</button>

12. Доступный опрос "Да/Нет"

Плохой код

<form role="form">  <h2>Poll title</h2>  <div id="pollQuestion">Is this accessible?</div>  <div name="pollGroup" role="radiogroup">    <div role="radiogroup" aria-label="Poll title">      <input type="radio" name="poll" aria-labelledby="pollQuestion" value="[object Object]">      <span>Yes</span>      <input type="radio" name="poll" aria-labelledby="pollQuestion" value="[object Object]">      <span>No</span>      <input type="radio" name="poll" aria-labelledby="pollQuestion" value="[object Object]">      <span>Maybe</span>      <input type="radio" name="poll" aria-labelledby="pollQuestion" value="[object Object]">      <span>Can you repeat the question?</span>    </div>    <button type="submit">Vote</button>  </div></form>

Ошибки и что следует исправить

  • Элемент <form> сам по себе семантически подразумевает форму, поэтому нет необходимости повторно указывать это с помощью атрибута role="form"

  • Форма это важный для доступности страницы ориентир. Поэтому внутрь формы полезно поместить заголовок, а в элементе <form> с помощью атрибута aria-labeledby, сослаться на него. Это существенно облегчает навигацию в документе с помощью вспомогательных технологий

  • Задавать атрибут role="radiogroup" необязательно, и уж точно не дважды. Если нужно сгруппировать элементы, просто используйте <fieldset>

  • Не используйте aria-labelledby для создания связи между радиокнопкой и вопросом. Данный атрибут предназначен для установки доступного имени. Вместо этого для текста вопроса используйте элемент <legend>

  • Чтобы радиокнопке назначить доступное имя, текст из <span> поместите в элемент <label> и свяжите с радиокнопкой с помощью атрибута for

  • Кнопку также следует поместить внутрь <fieldset> для создания одной логической группы элементов

Хороший код

<form aria-labelledby="poll-title">  <h2 id="poll-title">Poll title</h2>  <fieldset>    <legend>Is this accessible?</legend>    <input type="radio" id="radio1" name="poll" value="yes">    <label for="radio1">Yes</label>    <input type="radio" id="radio2" name="poll" value="no">    <label for="radio2">No</label>    <input type="radio" id="radio3" name="poll" value="maybe">    <label for="radio3">Maybe</label>    <input type="radio" id="radio4" name="poll" value="repeat">    <label for="radio4">Can you repeat the question?</label>    <button type="submit">Vote</button>  </fieldset></form>

13. Ссылка или

Плохой код

<input type="checkbox" id="accept" required><label for="accept">  <a href="http://personeltest.ru/aways/habr.com/legal"> I accept the confidentiality policy and data </a></label>

Ошибки и что следует исправить

  • Вкладывать элементы с запускаемым поведением (таким как клик) считается плохим решением

  • Возможность выбора чекбокса путём нажатия на его название улучшает удобство и доступность (путём увеличения области клика)

  • Но в данном случае пользователи не ожидают, что при нажатии на название чекбокса откроется новая страница

  • Размещайте ссылки за пределами элемента <label>

Хороший код

<input type="checkbox" id="accept" required><label for="accept"> I accept the confidentiality policy and data </label>(read <a href="http://personeltest.ru/aways/habr.com/legal">Terms and conditions</a>)

Источники

14. Неподходящий "type"

Плохой код

<a type="button" class="button" href="http://personeltest.ru/aways/habr.com/signup" tabindex="-1">Sign up</a>

Ошибки и что следует исправить

  • В данном примере с элементом <a> используется атрибут type , хотя он никак не влияет на семантику и можно сказать, что совсем неуместен

  • Если его и добавлять, то лишь со значением, являющимся допустимым MIME-типом. Браузеры могут учитывать его, но исключительно как рекомендацию

  • Если это ссылка, которая в атрибуте href содержит путь на какой-то ресурс (страницу или документ), следует использовать элемент <a>, а не <button>, независимо от того, как данный элемент выглядит в дизайне: как ссылка или как кнопка

  • Отрицательное значение tabindex значит, что элемент не получит фокус при переключении между элементами с помощью клавиатуры. Правда, такой элемент всё же может получить фокус с помощью JavaScript

  • Не меняйте семантику элементов, присущую им по умолчанию, если только в этом нет явной необходимости

  • Если вам нужна кнопка, просто используйте элемент <button>

Хороший код

<a href="http://personeltest.ru/aways/habr.com/signup" class="button">Sign up</a>

Источники

15. Буква за буквой

Контекст: буквы обёрнуты в <div> с целью анимирования каждой буквы через JavaScript

Плохой код

<h3>  <div style="display: block; text-align: start; position: relative;" class="title">    <div style="position: relative; display: inline-block; transform: rotateX(90deg); transform-origin: 50% 50% -30.8917px;" class="char">H</div>    <div style="position: relative; display: inline-block; transform: rotateX(90deg); transform-origin: 50% 50% -30.8917px;" class="char">e</div>    <div style="position: relative; display: inline-block; transform: rotateX(90deg); transform-origin: 50% 50% -30.8917px;" class="char">a</div>    <div style="position: relative; display: inline-block; transform: rotateX(90deg); transform-origin: 50% 50% -30.8917px;" class="char">d</div>    <div style="position: relative; display: inline-block; transform: rotateX(90deg); transform-origin: 50% 50% -30.8917px;" class="char">i</div>    <div style="position: relative; display: inline-block; transform: rotateX(90deg); transform-origin: 50% 50% -30.8917px;" class="char">n</div>    <div style="position: relative; display: inline-block; transform: rotateX(90deg); transform-origin: 50% 50% -30.8917px;" class="char">g</div>  </div></h3>

Ошибки и что следует исправить

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

Код примера, продемонстрированного на видео

  • Старайтесь избегать разрастания DOM. Слишком большое количество DOM-узлов или вложенных элементов может плохо отразиться на производительности страницы

  • Большое DOM-дерево ведёт к большому дереву доступности, которое также может плохо сказываться и на производительности вспомогательных технологий

  • Рекомендуется отделять представление от содержимого. Стили, которые не изменяются динамично, поместите в CSS-файл

Хороший код

Решение 1

<h3> Heading </h3>

Решение 2

Если в этом действительно есть необходимость, добавьте версию текста, доступную для скринридеров, а текст, предназначеный для визуального отображения, скройте с помощью aria-hidden="true"

<h3 class="title">  <span class="sr-only">Heading</span>  <div aria-hidden="true">    <div style="transform-origin: 50% 50% -30.8917px;" class="char">H</div>    <div style="transform-origin: 50% 50% -30.8917px;" class="char">e</div>    <div style="transform-origin: 50% 50% -30.8917px;" class="char">a</div>    <div style="transform-origin: 50% 50% -30.8917px;" class="char">d</div>    <div style="transform-origin: 50% 50% -30.8917px;" class="char">i</div>    <div style="transform-origin: 50% 50% -30.8917px;" class="char">n</div>    <div style="transform-origin: 50% 50% -30.8917px;" class="char">g</div>  </div></h3>

К сожалению, не существует встроенного способа скрывать содержимое только визуально. Для этого следует использовать CSS-правило, называемое классом .sr-only которое гарантирует, что контент будет скрыт визуально, но останется доступным для пользователей скринридеров.

.title {  display: block;  text-align: start;  position: relative;}.char {  position: relative;  display: inline-block;  transform: rotateX(90deg);}.sr-only {  position: absolute;  white-space: nowrap;  width: 1px;  height: 1px;  overflow: hidden;  border: 0;  padding: 0;  clip: rect(0 0 0 0);  clip-path: inset(50%);  margin: -1px;}

Источники

16. alt, хотя нет..., aria-label, хотя нет..., alt

Контекст: список изображений, которые ссылаются на страницы с товаром

Плохой код

<a tabindex="0">  <div alt="Browser Wars: The Last Engine" aria-label="Browser Wars: The Last Engine">    <div>      <img alt="Browser Wars: The Last Engine" src="thumbnail.jpg">    </div>  </div></a>

Ошибки и что следует исправить

  • Если у элемента <a> атрибут href имеет пустое значение или данного атрибута нет вообще, он представляет собой заполнитель для того места, где могла быть ссылка (HTML спецификация)

  • Если вы добавляете обработчик клика к ссылке-заглушке, то, по всей видимости, хотите, чтобы она была не заглушкой, а полноценной ссылкой с атрибутом href или кнопкой <button>, в зависимости от того, что должно происходить при клике

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

  • Атрибут alt не используется с элементами div и никак не влияет на их семантическое значение

  • Не злоупотребляйте ARIA. Атрибут aria-label лишний для элемента div, потому что img уже имеет доступное имя (значение атрибута alt)

Хороший код

Полноценная ссылка с заполненным атрибутом href и текстовая альтернатива alt для изображения

<a href="detail.html">  <div>    <img alt="Browser Wars: The Last Engine" src="thumbnail.jpg">  </div></a>

Источники

17. Недоступные карточки

Контекст: список карточек со ссылками, каждая из которых имеет заголовок, изображение и краткое описание

Плохой код

<section>  <section>    <h2>Overview</h2>    <figure class="card" data-url="image1.html" style="background: url(image1.jpg)">      <figcaption>        <h4>My heading</h4>        <article>Teasertext...</article>      </figcaption>    </figure>    <figure class="card" data-url="image2.html" style="background: url(image2.jpg)">  </figure>  </section></section>

Ошибки и что следует исправить

  • Вероятнее всего, в подобных ситуациях необходимости в таком количестве элементов <section> нет. Чтобы лучше понять почему, рекомендую прочитать статью "Why You Should Choose HTML5 <article> Over <section>" автора Bruce Lawson

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

  • Согласно спецификации, HTML5-элемент <figure> представляет собой самодостаточный элемент, который под основным содержимым опционально может содержать подпись. Но в этом примере нет содержимого, есть только подпись

  • Изображнеие карточки не является декоративным, оно несёт какую-то информацию и должно быть частью HTML-кода документа, а не добавляться через CSS-свойство background . Фоновые изображения доступны пользователям не всех устройств

  • В приведённом примере обработка клика на карточку происходит только через JavaScript. Если нет элемента ссылки с указанием пути (<a href="path/to/page">), для пользователей скринридеров переход на страницу карточки становится недоступным. Также элемент карточки не получает фокус при навигации с помощью клавиатуры

  • Элементы <h1> - <h6> представляют собой вводный заголовок для родительского элемента <section>. <h4> является потоковым содержимым и, как следствие, технически может быть потомком <figcaption>, но лучше сделать его заголовком всей карточки

  • Элемент <article> представляет собой самодостаточную композицию на странице. Это может быть газетная статья , эссе или отчёт, публикация в блоге или социальной сети. Для обычного абзаца текста лучше использовать элемент <p>

  • Сделать доступной карточку, вся область которой является кликабельной, непросто. Дополнительную информацию можно найти в разделе "источники" ниже

Хороший код

<div>  <section>    <h2>Overview</h2>    <article class="card">      <h3>        <a href="image1.html"> My heading </a>      </h3>      <img src="image1.jpg" alt="Description of image1" />      <p>Teasertext...</p>    </article>    <article class="card">  </article>  </section></div>

Источники

18. Панель div-игации

Контекст: главная панель навигации

Плохой код

<div class="nav">  <div>    <div>about</div>    <div>thoughts</div>  </div></div>

Ошибки и что следует исправить

  • <div> - это элемент, используемый в крайнем случае - когда никакие другие элементы не подходят. Использование <div> вместо более подходящих по семантике элементов ухудшает доступность

  • Для главной панели навигации лучше использовать самантический элемент <nav>. Он является важным для доступности страницы ориентиром, содержащим ссылки на внешние и внутренние страницы. Пользователи скринридеров могут сразу получить доступ к навигации или наоборот пропустить её

  • Используйте элементы <ul> или <ol> для структурирования ссылок, связанных семантически и визуально. Скринридеры обычно объявляют номер элемента в списке, что помогает ориентироваться

  • Если в навигации имеет значение последовательность элементов, используйте <ol> вместо <ul>

  • Если вместо <a> для ссылок использовать элемент <div>, событие клика будет вызываться только непосредственно кликом мыши. Это сделает их недоступными для пользователей скринридеров и тех, кто переключается между элементами с помощью клавиатуры

Хороший код

<nav>  <ul class="nav">    <li>      <a href="http://personeltest.ru/aways/habr.com/about">about</a>    </li>    <li>      <a href="http://personeltest.ru/aways/habr.com/thoughts">thoughts</a>    </li>  </ul></nav>

Источники

19. Неправильная работа с заголовками

Контекст: простая страница, которая отображает наличие товара

Плохой код

<h1>Product Status</h1><h2>Is the product available?</h2><div>  <h3>    <div>      <div>        <i>          <h3 class="message is-success">            Its <a>available</a>.          </h3>        </i>      </div>    </div>  </h3></div>

Ошибки и что следует исправить

  • Элементы <h1> - <h6> не должны использоваться для разметки подзаголовков, альтернативных заголовков и слоганов, если только они не озаглавливают новый раздел или подраздел

  • Все элементы <div> в данном примере излишни. Скорее всего, они присутствуют только потому, что фронтенд-фреймворк добавляет их по умолчанию. Используйте Fragments in React или подобные техники в других фреймворка, чтобы избежать этого

  • Старайтесь избегать разрастания DOM. Слишком большое количество DOM-узлов и вложенных элементов может отрицательно повлиять на производительность страницы

  • Большое DOM-дерево ведёт к большому дереву доступности, что может отрицательно влиять и на скорость работы вспомогательных технологий

  • Потомками <h1> - <h6> могут быть только элементы фразового содержимого. <h3> и <div> к ним не относятся

  • Элемент <i> представляет собой фрагмент, который скринридеры произносят изменённой интонацией, чтобы обозначить его отличие от остального текста. Если вам нужен просто курсивный текст, используйте CSS-свойство font-style: italic

  • Если у элемента <a> нет атрибута href, он представляет собой заглушку в том месте, где в другой ситуации может быть ссылка

  • Если вы добавляете обработчик клика к ссылке-заглушке, то, по всей видимости, хотите, чтобы она была полноценной ссылкой с атрибутом href или кнопкой <button>, в зависимости от того, что должно происходить при клике

Хороший код

 <h1>Product Status</h1> <p>Is the product available?</p> <p class="message is-success">   Its <a href="http://personeltest.ru/aways/habr.com/product.html">available</a>. </p>

Источники

20. Спецвыпуск HTMHell: кнопка "Закрыть"

В данном спецвыпуске рассматривается один из наиболее сложных и наиболее спорных шаблонов во фронтенд-разработке кнопка "Закрыть".

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

После недолгого исследования, занявшего менее 2 часов, HTMHell представляет 11 примеров плохой реализации данного элемента

Пример 1: с фоновым изображением

<div class="close"></div>    
close::after {  background: url("close.png");  content: "";}

Ошибки и что следует исправить

  • <div> - это элемент для крайнего случая, когда никакие другие элементы не подходят. Использование <div> вместо более подходящих по семантике элементов ухудшает доступность

  • На <div> событие клика вызывается только непосредственно кликом мыши. На элементах <button> это происходит ещё и при нажатии на кнопки Enter или Space на клавиатуре

  • <div> не получает фокус при переключении между элементами с помощью клавиатуры

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

  • Данный элемент скринридеры озвучат: Никак

Пример 2: с иконкой

<div class="close">  </div>

Ошибки и что следует исправить

  • Символ "" не является чем-то вроде "Закрыть" или "Перечёркнуто", это знак умножения. Например, 2 2 (два умножить на два). В кнопках "Закрыть" использовать его неуместно

  • В первом примере подробно описаны проблемы, связанные с использованием элемента <div>

  • Данный элемент скринридеры могут озвучить: как-то вроде "умножить на" или "разы" (times)

Пример 3: Иконки Font Awesome

<div class="close">  <i class="fas fa-times"></i></div>
.fa-times::before {  content: '\f00d';}

Ошибки и что следует исправить

  • Скринридеры могут озвучивать содержимое, которое генерируется через CSS

  • Font Awesome рекомендуют скрывать иконки семантически с помощью атрибута aria-hidden="true" для элемента <i>

  • Font Awesome добавляет Unicode-содержимое через псевдоэлемент ::before. Вспомогательные технологии могут озвучивать эту Unicode-альтернативу, которая в этом конкретном примере будет звучать как "разы" (times), поскольку fa-times - это не крестик, а знак умножения. Обратите внимание: Talkback и VoiceOver в данном примере не озвучат вообще ничего

  • Элемент <i> представляет собой фрагмент, который скринридеры произносят другой интонацией, что обозначить его отличие от остального текста. Если вам нужен просто курсивный текст, используйте CSS-свойство font-style: italic

  • В первом примере подробно описаны проблемы, связанные с использованием элемента <div>

  • Данный элемент скринридеры могут озвучить: "разы" (times)

Пример 4: Закрывающая ссылка

<a href="#" class="close"></a>
a::after {  font-size: 28px;  display: block;  content: "";}

Ошибки и что следует исправить

  • Если элемент <a> содержит атрибут href, он представляет собой ссылку на другой ресурс: такой как страница или PDF-документ

  • Задача элемента в нашем примере вызвать JavaScript-действие на текущей странице. Элемент <button> с атрибутом type="button" подходит лучше, потому что не имеет поведения по умолчанию и разработан для вызова действий в ответ на нажатие пользователем

  • Если вы не уверены, когда нужно использовать элемент <a>, а когда <button>, посмотрите видео "The Links vs. Buttons Showdown" от Marcy Sutton

  • Скринридеры могут озвучивать содержимое, которое генерирует CSS

  • Символ "" не является чем-то вроде "Закрыть" или "Перечёркнуто", это знак умножения. Например, 2 2 (два умножить на два). Не используейте его в кнопках "Закрыть"

  • Данный элемент скринридеры могут озвучить: "ссылка, разы" (link, times)

Пример 5: Закрывающая ссылка с текстом

<a href="#" class="close">  Close</a>
.close::before {  content: "\e028";}

Ошибки и что следует исправить

  • Хорошая попытка, но это всё ещё ссылка, а не кнопка

  • В предыдущем примере подробно написано про использование элемента <a> и генерируемое CSS содержимое

  • Данный элемент скринридеры могут озвучить: "ссылка, разы закрыть" (link, times close)

Пример 6: Закрывающая ссылка без атрибута href

<a class="close" onclick="close()"></a>

Ошибки и что следует исправить

  • Ещё одна хорошая попытка, но ссылка без атрибута href всё еще не является кнопкой

  • Если у элемента <a> нет атрибута href, он представляет собой заглушку в том месте, где в другой ситуации может быть ссылка

  • Если вы добавляете обработчик клика к ссылке-заглушке, то, по всей видимости, хотите, чтобы она была не заглушкой, а полноценной ссылкой с атрибутом href или кнопкой <button>, в зависимости от того, что должно происходить при клике

  • Ссылки-заглушки не получают фокус при переключении между элементами с помощью клавиатуры

  • Если вы не уверены, когда нужно использовать элемент <a>, а когда <button>, посмотрите видео "The Links vs. Buttons Showdown" от Marcy Sutton

  • Данный элемент скринридеры могут озвучить: "разы, кликабельно" (times, clickable)

Пример 7: Ссылка-заглушка и изображение

<a onclick="close();">   <img src="close.png"> </a>

Ошибки и что следует исправить

  • Для изображения не задана текстовая альтернатива. Скринридеры могут озвучить название файла

  • В 6 примере подробно написано про использование ссылок-заглушек

  • Скринридеры могут озвучить данный элемент: "close.png, изображение" (close.png, image)

Пример 8: Радио-кнопка

<label class="close" for="close">   <svg>  </svg> </label> <input id="close" type="radio">
[type="radio"] {  display: none;}

Ошибки и что следует исправить

  • Когда сторонники доступности говорят "Просто используйте кнопку", они имеют в виду элемент <button>, а не радио-кнопки

  • Радио-кнопки используются в группах, описывающих набор связанных вариантов (опций)

  • У SVG нет текстовой альтернативы. Чтобы больше узнать о доступности SVG, рекомендую почитать статью "Creating Accessible SVGs" автора Carie Fisher

  • Также, display: none на элементе <input> делает недоступным <label>

  • Данный элемент скринридеры озвучат: Никак

Пример 9: Кнопка с иконкой

<button class="close" type="button">  </button>

Ошибки и что следует исправить

  • Символ "" не является чем-то вроде "Закрыть" или "Перечёркнуто", это знак умножения. Например, 2 2 (два умножить на два). Не используейте его в кнопках "Закрыть"

  • Данный элемент скринридеры могут озвучить: "разы, кнопка" (times, button)

Пример 10: Кнопка с svg

<button class="close">  <svg>  </svg></button>

Ошибки и что следует исправить

  • У SVG нет текстовой альтернативы. Чтобы больше узнать о доступности SVG, рекомендую почитать статью "Creating Accessible SVGs" автора Carie Fisher

  • Данный элемент скринридеры могут озвучить: "кнопка" (button)

Пример 11: Старая добрая буква "X"

<div role="button" tabindex="0">X</div>

Ошибки и что следует исправить

  • Нет необходимости с помощью атрибута role явно задавать семантику элемента, вместо этого стоит просто использовать элемент <button

  • При использовании <button> атрибут tabindex не нужен. HTML-кнопки получают фокус по умолчанию

  • В 1 примере подробно описаны проблемы, связанные с использованием элемента <div>

  • Буква "X" не является иконкой для кнопки "Закрыть"

  • Данный элемент скринридеры могут озвучить: "икс, кнопка" (X, button)

"Использовать букву "X" для кнопок "Закрыть" это то же, что добавлять в кофе соль вместо сахара, потому что выглядит она так же"

Max Bck

Примеры правильной разметки

Решение 1: Кнопка с видимым текстом без иконки

<button type="button">  Close</button>
  • Только текст: легко в реализации и понятно для пользователей

  • Скринридеры могут озвучить данный элемент: "Закрыть, кнопка" (Close, button)

Решение 2: Кнопка с видимым текстом и только визуально доступной иконкой

<button type="button">  Close  <span aria-hidden="true"></span></button>
  • Если вы вынуждены использовать иконку "умножить", скройте её от скринритеров, обернув в элемент <span> с атрибутом aria-hidden="true"

  • Скринридеры могут озвучить данный элемент: "Закрыть, кнопка" (Close, button)

Решение 3: Кнопка со скрытым текстом и только визуально доступной иконкой

<button type="button">  <span class="sr-only">Close</span>  <span aria-hidden="true"></span></button>
.sr-only {  position: absolute;  white-space: nowrap;  width: 1px;  height: 1px;  overflow: hidden;  border: 0;  padding: 0;  clip: rect(0 0 0 0);  clip-path: inset(50%);  margin: -1px;}
  • К сожалению, не существует встроенного способа скрывать содержимое только визуально. Для этого следует использовать CSS-правило, называемое классом .sr-only которое гарантирует, что контент будет скрыт визуально, но останется доступным для пользователей скринридеров.

  • Скринридеры могут озвучить данный элемент: "Закрыть, кнопка" (Close, button)

Решение 4: Кнопка со скрытым текстом и только визуально доступной иконкой

<button type="button" aria-label="Close">  <span aria-hidden="true"></span></button>
  • Если вы не хотите показывать текст на экране, обеспечьте для иконки или SVG текстовую альтернативу, добавив к кнопке атрибут aria-label

  • Скринридеры могут озвучить данный элемент: "Закрыть, кнопка" (Close, button)

Решение 5: Font Awesome

Для полноты картины, закрывающая кнопка с иконкой Font Awesome

<button type="button" class="close" aria-label="Close">  <span class="fa fa-times" aria-hidden="true"></span></button>

Общие примечания

Порой может иметь смысл использование более описательных названий "Закрыть окно", "Закрыть галереию", "Закрыть рекламу".

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

Источники

21. Легендарный legend

Контекст: кнопка, которая разворачивает и сворачивается блок текста

Плохой код

<button class="panel-heading" tabindex="0" href="#collapse0" aria-expanded="true">  <legend> Industries Served </legend></button>

Ошибки и что следует исправить

  • <legend> не разрешается помещать в какой-нибудь другой элемент, кроме <fieldset> (HTML спецификация для legend)

  • При использовании <button> атрибут tabindex не нужен. HTML-кнопки получают фокус по умолчанию.

  • Атрибут href не может использоваться с элементом <button> (HTML спецификация для button)

Хороший код

<button class="panel-heading" aria-expanded="true">  Industries Served</button>

Источники

22. Старая добрая div-ссылка

Контекст: ссылка на другую страницу

Плохой код

<div>About us</div>
<div onClick="location.href='about.html'">  About us</div>
<div data-page="aboutus" data-url="index.php">  About us</div>

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

Ошибки и что следует исправить

  • <div> - это элемент для крайнего случая, когда никакие другие элементы не подходят. Использование <div> вместо более подходящих по семантике элементов ухудшает доступность

  • На <div> событие клика вызывается только непосредственно кликом мыши. На элементах <a> это происходит ещё и при нажатии на кнопку Enter на клавиатуре.

  • <div> не получает фокус при переключении между элементами с помощью клавиатуры

  • При нажатии правой кнопкой мыши в контекстном меню не будет пунктов "Открыть в новой вкладке/окне" или "Добавить ссылку в закладки"

  • По умолчанию скринридеры просто озвучивают текст внутри <div> (например, "О нас"). В случае использования ссылки <a> скринридеры текст и роль элемента (например, "О нас, ссылка")

  • Атрибуты наподобие aria-label у элементов <div> могут работать неправильно

  • Пользователи скринридеров могут использовать раздел со списком ссылок страницы. <div>-ссылок в этом разделе не будет, если только к элементу не будет добавлен атрибут role="link"

Хороший код

<a href="aboutus.html">  About us</a>

23. Шаблон карточки

Плохой код

<article>  <div>    <div class="sr-only">Image</div>    <img src="http://personeltest.ru/aways/habr.com/feature-teaser.png" alt="Feature teaser" />  </div></article><div>  <span>    <span>Exciting feature!</span>  </span>  <div> This text describes what the feature does! </div>  <a href="http://personeltest.ru/aways/habr.com/blog/feature">    <span>Read more</span>    <svg viewBox="0 0 9 12" xmlns="http://personeltest.ru/away/www.w3.org/2000/svg">      <path d="M.84 10.59L5.42 6 .84 1.41 2.25 0l6 6-6 6z"></path>    </svg>  </a></div>

Ошибки и что следует исправить

  • В примере выше используется <article>. Этот элемент предназначен для самодостаточного содержимого, которое может быть повторно использовано на странице. Если здесь и есть что-то повторно используемое, то вся карточка. Более подходящим является элемент <section>

  • В первом <div> присутствует доступный только для скринридеров текст "Image". Ощущение, что это своего рода определение роли следующего за ним элемента <img>. Правильно подобранные HTML-элементы сами сообщают о своей семантике. Необходимость в дополнительных "уточнениях" отпадает

  • Далее расположен <span>, который, кажется, является заголовком. Вспомогательные технологии могут использовать указанные в коде заголовки для быстрой навигации. Следовательно, более корректным будет использовать элемент заголовка корректного уровня. В данном случае <h4>

  • Основной текст внутри карточки обёрнут в <div>. Использование <p> лучше передало бы его предназначение

  • "Read more" не самый лучший текст для ссылки. Это особенно заметно пользователям скринридеров, которые используют навигацию по ссылкам. Из названия непонятно, куда именно ведёт эта ссылка

  • <svg> внутри ссылки не предоставляет дополнительную информацию и должен быть скрыт от скринридеров

Хороший код

<section>  <div>    <img src="http://personeltest.ru/aways/habr.com/feature-teaser.png" alt="" />  </div>  <div>    <h4>Exciting feature!</h4>    <p>This text describes what the feature does!</p>    <a href="http://personeltest.ru/aways/habr.com/blog/feature">      <span>Read more about our exciting feature </span>      <svg aria-hidden="true" viewBox="0 0 9 12" xmlns="http://personeltest.ru/away/www.w3.org/2000/svg">        <path d="M.84 10.59L5.42 6 .84 1.41 2.25 0l6 6-6 6z"></path>      </svg>    </a>  </div></section>

Источники

24. Placeholder - это не label

<input type="text" placeholder="First name">

Ошибки и что следует исправить

  • Каждому элементу <input> нужен <label>. Когда пользователи скринридера переходят к полям формы, озвучивается содержимое <label> и тип поля (например, "имя, поле ввода"). Если этот текст пропущен, пользователи могут не знать, какую информацию они должны указать в этом поле

  • Некоторые скринридеры всё же берут текст из атрибута placeholder, но не стоит полагаться на это

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

  • С помощью псевдоэлемента ::placeholder можно улучшить контрастность цвета. Но тут тоже нужно не переборщить, поскольку текст с очень контрастным цветом пользователи могут принять за текстовое содержимое поля

  • Использование <label> увеличивает область выбора нужного поля, что может быть очень полезным, особенно на устройствах с сенсорным экраном

  • Если placeholder является единственным местом, содержащим название поля, при вводе текста он скрывается. Это ухудшает удобство заполнения форм, особенно если содержат много полей.

  • Пользователи не могут проверить, правильно ли они заполнили форму, потому что видят только значения, но не названия полей.

  • Если браузер заполняет поле автоматически, пользователь вынужден вырезать содержимое поля, чтобы убедиться, что оно заполнилось корректно

  • Текст placeholder обрезается, если он выходит за рамки поля

  • Инструменты подобные Google Translate могут не переводить значение данного атрибута при переводе всей страницы

  • Названия полей лучше воспринимаются, когда расположены над соответствующим текстовым полем, а не внутри него

Хороший код

 <label for="firstname">First name</label> <input type="text" id="firstname">

Источники

Подробнее..

Перевод Примеры применения переменных CSS на практике

30.04.2021 14:04:59 | Автор: admin

Когда я впервые попробовал использовать переменные CSS, у меня не было полного представления об их возможностях. Правильное их использование позволяет существенно сократить время и трудозатраты на решение определённых задач в CSS.


Если вы ещё не работали с переменными CSS, рекомендую прочитать эту статью, чтобы познакомиться с ними. Как только вы их освоите, работа станет намного проще. Цель этой статьи сосредоточить внимание на разных случаях практического использования переменных CSS помимо их применения для создания маркеров дизайна, таких как цвета.

Вы готовы? Тогда вперёд!

В чём основная проблема?

Если вы используете переменные CSS так же, как и препроцессоры CSS (например в Sass), то вы не полностью реализуете все их преимущества. Рассмотрите следующий пример:

:root {    --brand-primary: #7777e9;    --brand-secondary: #c96fde;}.title {    color: var(--brand-primary);}

И тут нет никакой разницы с нижеприведённым примером в Sass:

$brand-primary: #7777e9;$brand-secondary: #c96fde;.title {    color: $brand-primary;}

Да, применение переменных CSS в качестве переменных цвета ни в коей мере не является ошибкой. Но это всё равно, что покупать Apple MacBook M1 для посещения интернет-сайтов, хотя с этим и так хорошо справляется ваш старый настольный ПК, собранный в 2012 году. Какой смысл покупать ультрасовременный компьютер и делать на нём то же самое, никак не используя весь спектр его возможностей? Как вы понимаете, примерно так я и думаю об использовании переменных CSS для сохранения информации о цветах.

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

Примеры и случаи практического использования

Полные формы записи свойств

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

<!-- Base component --><header class="page-header">    <h2>...</h2>    <p>...</p></header><!-- Component variation --><header class="page-header page-header--compact">    <h2>...</h2>    <p>...</p></header>
.page-header {    --padding-start: 2.5rem;    --padding-block: 2rem;    padding: var(--padding-block) 1rem var(--padding-block) var(--padding-start);}.page-header--compact {    --padding-start: 1.5rem;    --padding-block: 1rem;}

Обратите внимание, для изменения заполнения нам потребуется всего лишь изменить значение переменной CSS. Без неё нам было бы необходимо вручную вводить полную форму записи свойства внутреннего отступа padding, чтобы изменить только одно значение в нём.

.page-header {    padding: 2rem 1rem 2rem 1.5rem;}.page-header--compact {    padding: 1rem 1rem 1rem 2.5rem;}

Всё вышесказанное применимо и для свойства внешнего отступа margin.

Свойства CSS background

Что касается свойств CSS для работы с фоном, то и здесь переменные CSS помогают сократить объём создаваемого нами кода. И даже больше того: благодаря их применению сам код CSS легче читать.

Хранение значений URL

При работе с интерфейсом пользователя вам может потребоваться добавить какое-либо изображение в декоративных целях. В таком случае хорошим решением будет использование элементов <div> и background-image. Если интерфейс должен быть динамическим, то значения для изображения нужно подставлять с помощью JavaScript.

Без применения переменных CSS соответствующий код HTML будет выглядеть так:

<section     class="newsletter"     style="background-image: url('/assets/ui/decoraitve/newsletter-lg-aj1891101.svg')"></section>

Но вместо того, чтобы напрямую изменять свойство background-image, мы можем сделать следующее.

<section     class="newsletter"     style="--thumb:url('/assets/ui/decoraitve/newsletter-lg-aj1891101.svg')"></section>
.newsletter {    background-image: var(--thumb);    background-size: cover;    background-position: 100% 50%;}

Обратите внимание, что необходимо включить элемент url() без переменной CSS.

Положение фонового изображения

В приведённом выше примере фоновое изображение помещается справа. Для макетов с направлением текста справа налево (RTL) положение фона следует перевернуть.

.newsletter {    --pos: 100% 50%;    background-image: var(--thumb);    background-size: cover;    background-position: 100% 50%;}html[dir="rtl] .newsletter {    -background-position: 0% 50%;}

Чтобы упростить эту задачу, мы можем воспользоваться переменными CSS.

.newsletter {    /* other styles */    background-position: var(--pos);}html[dir="rtl] .newsletter {    --pos: 0% 50%;}

Угловой градиент: часть 1

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

.element {    --angle: 90deg;    background: linear-gradient(var(--angle), #4088vb, #C05858);}html[dir="rtl] .element {    --angle: -90deg;}

Угловой градиент: часть 2

В случае с угловым градиентом использование переменных CSS с радиальными градиентами действительно существенно упрощает настройку положения. В следующем примере положение градиента изменяется при помощи переменной --pos.

А здесь показано, как можно сделать то же самое без переменных CSS.

.card {    background: radial-gradient(        circle 200px at center top,        rgba(64, 136, 203, 0.5),        #f7f7f7    );}

Теперь предположим, что у нас есть какой-то вариант карточки .card-2 и он должен находиться в другом положении. Необходимо написать следующий код:

.card-2 {    background: radial-gradient(        circle 200px at center top,        rgba(64, 136, 203, 0.5),        #f7f7f7    );}

Таким образом, переписывается всё определение градиента. Можно ли сделать лучше? Определённо. Обратите внимание, что единственным настраиваемым значением для этого варианта является переменная --pos.

.card {    --pos: center top;    background: radial-gradient(        circle 200px at var(--pos),        rgba(64, 136, 203, 0.5),        #f7f7f7    );}.card-2 {    --pos: left top;}

Свойство clip-path

Весьма полезным случаем использования переменной CSS является изменение с её помощью значений clip-path: polygon() при переходе с настольных на мобильные устройства.

На показанном выше рисунке необходимо изменить положение вершин многоугольника. С помощью переменных CSS сделать это намного проще.

.hero {    --first: 4% 7%;    --second: 80% 0;    --thrid: 100% 95%;    --fourth: 10% 100%;    clip-path: polygon(var(--first), var(--second), var(--thrid), var(--fourth));}@media (min-width: 40rem) {    .hero {        --second: 96% 0;        --thrid: 92% 82%;    }}

Если вы хотите узнать больше о свойстве CSS clip-path, здесь вы найдёте статью за авторством вашего покорного слуги.

Элемент флажка

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

Первое, что я сделал, это определил значения hsla() для корневого элемента компонента.

.form-item {    --primary-h: 240;    --primary-s: 56%;    --primary-l: 63%;    --primary-alpha: 100%;}

Теперь я могу использовать эти свойства в функции определения цветов hsla().

/* The circle that appears on hover */.form-item__label:after {    --primary-alpha: 0;    background-color: hsla(        var(--primary-h),        var(--primary-s),        var(--primary-l),        var(--primary-alpha)    );}.form-item__label:hover:after {    --primary-alpha: 15%;}

Код SVG, встроенный в CSS

Когда-то я работал над пользовательским интерфейсом для проекта одного клиента и столкнулся с такой ситуацией. У нас был раздел с двумя лентами (одна сверху, а вторая снизу). Мне необходимо было изменить цвет, угол поворота и размер этих лент, не создавая отдельный код CSS для каждой из них. Спасением стали переменные CSS!

Прежде всего я подготовил требуемый код SVG в Adobe Illustrator. Я разделил каждую ленту на три слоя:

  • светлые области;

  • тёмные области;

  • базовый слой.

Затем я экспортировал этот код SVG и поместил его в элементах <defs> так, чтобы легко можно было использовать его повторно. Обратите внимание, я также добавил ключевое слово currentColor! Это и есть то самое магическое значение, которое заставит всё работать.

Наконец, теперь нам необходимо определить размер и угол поворота в CSS.

.tape {    width: var(--size);    transform: rotate(var(--angle));}
<svg class="tape" style="--angle: 10deg; color: red; --size: 120px;" aria-hidden="true" focusable="false" viewBox="0 0 123 47">  <use href="#tape"></use></svg>

Готово. Мы создали собственный код SVG, который можно настраивать по мере необходимости. Впечатляет, не правда ли?

Создание миксинов (примесей-шаблонов), таких, как в Sass

Об этой интересной возможности я узнал из рекомендуемого мною к просмотру выступления Ли Веру (Lea Verou). Идея состоит в том, чтобы установить исходные переменные CSS для определённого свойства, а затем переопределять их, когда это понадобится.

Здесь показан пример, как мы можем использовать эту возможность. Очень часто мне необходимо, чтобы враппер был центрирован, для чего можно добавить margin: 0 auto к соответствующему элементу враппера. В тех случаях, когда необходимо отцентрировать какой-то свой элемент, мы можем выполнить следующее:

<div class="featured-section u-center"></div>
.u-center {  --mx: initial;  --my: initial;  margin: var(--my) var(--mx);}.featured-section {  --mx: auto;  --my: 2rem;}

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

Использование функции calc()

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

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

.c-avatar {  width: calc(var(--size, 32) * 1px);  height: calc(var(--size, 32) * 1px);}

Обратите внимание, я использовал функцию var(--size, 32). Если переменная --size не определена, то значение 32 будет использовано как резервное значение. Важно указать 1px, чтобы добавить px к результирующему числу.

Таким образом, мы можем создавать вариации классов, просто добавляя переменную CSS --size.

.c-avatar--medium {    --size: 64;}.c-avatar--large {    --size: 128;}

Псевдоэлементы

С помощью переменных CSS можно изменять свойство псевдоэлемента, поскольку оно наследуется от родительского элемента. Рассмотрите следующий пример:

В заголовке раздела содержится декоративная фиолетовая линия, которая является псевдоэлементом. Мы можем передать переменную CSS данному заголовку, и этот псевдоэлемент унаследует её.

.section-title {    --dot-color: #829be9;}.section-title:before {    content: "";    background-color: var(--dot-color);}

Но, кроме того, мы также можем имитировать изменение цвета псевдоэлемента с помощью Javascript.

Встроенные стили

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

Рассмотрите следующий пример.

.o-grid {    display: grid;    grid-template-columns: repeat(auto-fit, minmax(var(--item-width, 200px), 1fr);    grid-gap: var(--gap);}

У нас есть настроенный грид и заданное по умолчанию для ширины элемента грида значение 200px. В коде HTML мы можем переопределить его, переустановив значение для переменной CSS.

<!-- Example 1 --><div class="o-grid" style="--item-width: 250px;">     <div></div>     <div></div>     <div></div></div><!-- Example 2 --><div class="o-grid" style="--item-width: 350px;">     <div></div>     <div></div>     <div></div></div>

Ну разве это не полезно? Если вы хотите узнать больше о переменных CSS со встроенными стилями, я уже писал об этом более подробно.

Заключение

Как вы могли убедиться, существует множество примеров использования переменных CSS помимо определения цветов с их помощью. Уверен, то что я показал, это лишь самые поверхностные варианты их применения. Если вы придумали какой-то свой уникальный вариант использования поделитесь им в комментариях!

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

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 465 (26 2 мая 2021)

02.05.2021 22:08:33 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript


Медиа


podcast Подкаст proConf #94: GraphQL Galaxy
podcast Новости 512 от CSSSR: Chrome 91 Beta, postcss-easy-z, tree-shakeable библиотеки, гайды по кастомным CSS-свойствам и CLS
podcast Подкаст Callback Hell: Падение последнего оплота Dart, визуальные ЯП на примере Enso, Lucy DSL для стейт-машин
podcast Подкаст Фронтенд Юность#184: Матрица для мешка с картошкой


Веб-разработка


История фронтенда: JavaScript как отражение новой эпохи
habr HTMHell адовая разметка
habr HTML-теги и атрибуты, о которых вы, возможно, не знали
habr Адаптивный дизайн как антипаттерн
en Скромный элемент img и Core Web Vitals
en Как реализовать выбор дейстий для выделенного текста с помощью SelectionAPI




CSS


habr Примеры применения переменных CSS на практике
habr Контейнерные запросы в CSS
habr Как обеспечить глассморфизм с помощью HTML и CSS
VDS (value definition syntax)
en fit-content и fit-content()
en Полное руководство по Custom Properties
en Первый взгляд на CQFill, полифилл для CSS Container Queries
en Изучение color-contrast() в первый раз
en GPT-3 и CSS-фреймворки
en Понимание easing-функций для анимации и переходов в CSS
en Новые возможности WebKit в Safari 14.1 (Flexbox Gap, Date & Time Inputs, CSS Individual Transform Properties)

JavaScript


habr Целительная сила JavaScript
habr Человеко-читаемый JavaScript: история о двух экспертах
Принцип мозаики, или Как мы сделали JavaScript по-настоящему модульным
en Fower утилитарная CSS in JS библиотека для быстрой разработки интерфейсов.
en Клиентские шаблоны API, о которых должен знать каждый разработчик фронтенда








Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Кастомный виджет Яндекс.Переводчикдлясайтов

02.05.2021 22:08:33 | Автор: admin

Когда-то я предложил свое решение по кастомизации виджета googleTranslate, тема оказалась действительно полезной и актуальна по сей день. Репозиторий с проектом на gitHub набрал немного звезд, а я рад тому, что мои труды не напрасны. И вот недавно мне понадобилось сделать пользовательский выпадающий список с выбором языков, но уже с виджетом яндекс переводчика. Вообще сам по себе виджет вполне устраивал заказчика, но проблема заключается в том что в нем по умолчанию находится более 90 языков и этот список нельзя никак ограничить. Нельзя выставить 2-3 или 5 необходимых вам языков, будут показаны все 90+, но проблема еще и в том, что виджет не адаптивен, он занимает 1221 пиксель в ширину и никак не подстраивается под размер экрана:

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

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

Текст обращения

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

И довольно быстро получил ответ:

Текст ответа

Здравствуйте, Виталий!

Такой возможности в нашем виджете сейчас нет.

Спасибо за желание сделать Яндекс.Переводчик удобнее! Я передал ваше предложение команде разработки.

И теперь окончательно убедившись, что готового решения нет, я принялся за дело.

Как будет выглядеть пример:

Разметка демо-страницы
<!DOCTYPE html><html>    <head>        <meta charset="utf-8" />        <title>Пользовательский виджет yatranslate для сайта на чистом js</title>        <meta name="viewport" content="width=device-width, initial-scale=1">        <!-- yatranslate -->        <script src="./js/yatranslate.js"></script>        <link rel="stylesheet" href="./css/yatranslate.css">        <!-- END yatranslate -->    </head>    <body class="body">        <div class="lang lang_fixed">            <div id="ytWidget" style="display: none;"></div>            <div class="lang__link lang__link_select" data-lang-active>                <img class="lang__img lang__img_select" src="./images/lang/lang__ru.png" alt="Ru">            </div>            <div class="lang__list" data-lang-list>                <a class="lang__link lang__link_sub" data-ya-lang="ru">                    <img class="lang__img" src="./images/lang/lang__ru.png" alt="ru">                </a>                <a class="lang__link lang__link_sub" data-ya-lang="en">                    <img class="lang__img" src="./images/lang/lang__en.png" alt="en">                </a>                <a class="lang__link lang__link_sub" data-ya-lang="de">                    <img class="lang__img" src="./images/lang/lang__de.png" alt="de">                </a>                <a class="lang__link lang__link_sub" data-ya-lang="zh">                    <img class="lang__img" src="./images/lang/lang__zh.png" alt="zh">                </a>                <a class="lang__link lang__link_sub" data-ya-lang="fr">                    <img class="lang__img" src="./images/lang/lang__fr.png" alt="fr">                </a>            </div>        </div>        <section class="content">            <h1 class="content__title">Автоматический перевод сайта</h1>            <div class="content__desc">                <p>Перевод сайта на другие языки при помощи виджета "Яндекс.Переводчик для сайтов"</p>                <p>Пример настраиваемого виджета</p>                <p>Hello World!!!</p>            </div>        </section>        <style>            /* Стили для демонстрации */            /* Styles for demonstration */            body {                display: flex;                justify-content: center;                align-items: center;                min-height: 100vh;                margin: 0;                padding: 0;                font-family: tahoma;            }            .content {                text-align: center;                margin: auto;            }        </style>    </body></html>

Для корректной работы виджета необходимо подключить файлы:

<script src="./js/yatranslate.js"></script><link rel="stylesheet" href="./css/yatranslate.css">
Содержимое yatranslate.css
/* lang */.lang {    position: relative;    z-index: 10;    text-align: center;    background: rgba(157, 157, 157, 0.3);    perspective: 700px;}.lang_fixed {    position: fixed;    right: 20px;    top: 20px;}.lang__link {    cursor: pointer;    transition: .3s all;    display: flex;    justify-content: center;    align-items: center;    flex-direction: column;    flex-shrink: 0;    box-sizing: border-box;    text-decoration: none;    border-radius: 2px;    padding: 4px;}.lang__img {    width: 30px;    height: 18px;    flex-shrink: 0;    font-size: 10px;    display: block;    transition: .3s all;}.lang__link_sub:hover {    filter: drop-shadow(0 0 3px rgb(136, 136, 136)) brightness(130%);}.lang__name {    color: #737b84;    font-size: 12px;    line-height: 12px;    flex-shrink: 0;    text-transform: uppercase;}.lang__link_sub {    width: 100%;    height: auto;    position: relative;    padding: 0;    margin-bottom: 2px;}.lang__list {    background: rgba(157, 157, 157, 0.3);    display: flex;    justify-content: center;    align-items: center;    flex-direction: column;    width: 100%;    opacity: 0;    visibility: hidden;    transition: .3s all;    transform: rotateX(-90deg);    position: absolute;    left: 0;    top: 100%;    z-index: 10;    line-height: 13px;    padding: 4px;    transform-origin: center top;    box-sizing: border-box;}.lang:hover .lang__list {    opacity: 1;    visibility: visible;    transform: rotateX(0);}.lang__link_select {    align-items: flex-start;    text-align: center;    font-size: 0;}
Содержимое yatranslate.js
/*!*************************************************** * yatranslate.js v1.0.0 * author: Vitalii P. *****************************************************/const yatranslate = {    /* Original language */    lang: "ru",    /* The language we translate into on the first visit */    /* Язык, на который переводим при первом посещении */    // langFirstVisit: 'en',};document.addEventListener('DOMContentLoaded', function () {    // Start    yaTranslateInit();})function yaTranslateInit() {    if (yatranslate.langFirstVisit && !localStorage.getItem('yt-widget')) {        /* Если установлен язык перевода для первого посещения и в localStorage нет yt-widget */        /* If the translation language is installed for the first visit and in localStorage no yt-widget */        yaTranslateSetLang(yatranslate.langFirstVisit);    }    // Подключаем виджет yandex translate    // Connecting the yandex translate widget    let script = document.createElement('script');    script.src = `https://translate.yandex.net/website-widget/v1/widget.js?widgetId=ytWidget&pageLang=${yatranslate.lang}&widgetTheme=light&autoMode=false`;    document.getElementsByTagName('head')[0].appendChild(script);    // Получаем и записываем язык на который переводим    // We get and write down the language into which we translate    let code = yaTranslateGetCode();    // Показываем текущий язык в меню    // Show the current language in the menu    yaTranslateHtmlHandler(code);    // Вешаем событие клик на флаги    // We hang the event click on the flags    yaTranslateEventHandler('click', '[data-ya-lang]', function (el) {        yaTranslateSetLang(el.getAttribute('data-ya-lang'));        // Перезагружаем страницу        // Reloading the page        window.location.reload();    })}function yaTranslateSetLang(lang) {    // Записываем выбранный язык в localStorage объект yt-widget     // Writing the selected language to localStorage     localStorage.setItem('yt-widget', JSON.stringify({        "lang": lang,        "active": true    }));}function yaTranslateGetCode() {    // Возвращаем язык на который переводим    // Returning the language to which we are translating    return (localStorage["yt-widget"] != undefined && JSON.parse(localStorage["yt-widget"]).lang != undefined) ? JSON.parse(localStorage["yt-widget"]).lang : yatranslate.lang;}function yaTranslateHtmlHandler(code) {    // Получаем язык на который переводим и производим необходимые манипуляции с DOM    // We get the language to which we translate and produce the necessary manipulations with DOM     document.querySelector('[data-lang-active]').innerHTML = `<img class="lang__img lang__img_select" src="./images/lang/lang__${code}.png" alt="${code}">`;    document.querySelector(`[data-ya-lang="${code}"]`).remove();}function yaTranslateEventHandler(event, selector, handler) {    document.addEventListener(event, function (e) {        let el = e.target.closest(selector);        if (el) handler(el);    });}
Используемые флаги

Логика виджета довольно простая. При выборе языка в локальное хранилище записывается объект с ключем yt-widget. В объекте хранится язык на который будет переведен сайт:

{"lang":"en","active":true}

К локальному хранилищу без проблем можно получить доступ и я применил ту же технику что и с гугл переводчиком. Виджет яндекса прячем, а кликая на свой кастоный флажок с атрибутом data-ya-lang назначаем записаный в нем язык свойству lang и перезагружаем страницу. После перезагрузки страницы язык, который мы сами установили, будет подхвачен виджетом и сайт будет на него переведен. В функции yaTranslateHtmlHandler проводим необходимые манипуляции с разметкой, в моем случае я показываю флаг текущего языка перевода и удаляю его из общего списка. В js каждый этап я разбил на функции и добавил описание, чтобы было легче доработать код под себя.

Репозиторий с проектом на GitHub

Демонстрация

Подробнее..

Перевод Почему стоит использовать тег ltpicturegt вместо ltimggt

05.05.2021 10:13:29 | Автор: admin
image

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

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

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

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

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

Почему тега img недостаточно для современных веб-приложений?


Как все мы знаем, тег Img в течение длительного периода времени был одним из фундаментальных элементов HTML, и никто не сомневался в его простоте и применимости.

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

Все эти вопросы можно сгруппировать в две большие категории:

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

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

Смена разрешения при помощи атрибутов srcset и sizes


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

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

Это может привести к более долгой загрузке изображений и частичной загрузке изображений сверху вниз.


Проблема загрузки изображения сверху вниз

Эту проблему можно легко решить тегом picture при помощи атрибутов srcset и sizes.

<picture>   <source      srcset="small-car-image.jpg 400w,              medium-car-image.jpg 800w,              large-car-image.jpg 1200w"      sizes="(min-width: 1280px) 1200px,             (min-width: 768px) 400px,             100vw">   <img src="medium-car-image.jpg" alt="Car"></picture>

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

Атрибут sizes задаёт пространство, которое изображение будет занимать на экране. В показанном выше примере изображение займёт до 1200px, если минимальная ширина экрана равна 1280px.

Тем не менее, рекомендуется не использовать тег Picture только для смены разрешения, потому что ту же задачу можно решить с помощью новой версии тега Img (имеющей поддержку большего количества браузеров).

<img srcset="small-car-image.jpg 400w,             medium-car-image.jpg 800w,             large-car-image.jpg 1200w"     sizes="(min-width: 1280px) 1200px,            (min-width: 768px) 400px,            100vw"          src="medium-car-image.jpg" alt="Car">

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

Поэтому давайте посмотрим, как можно решить проблему ориентации графики с помощью тега picture.

Ориентация графики при помощи атрибута media


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

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

С помощью тега picture мы можем легко обеспечить смену разрешения, воспользовавшись несколькими тегами source внутри тега picture.

<picture>      <source ....>   <source ....>   <source ....></picture>

Затем можно использовать атрибут media для задания различных условий среды, в которых будут использоваться эти источники. Также можно использовать атрибуты srcset и sizes аналогично тому, о чём мы говорили в предыдущем разделе.

В показанном ниже примере демонстрируется полная реализация ориентации графики и смены разрешения при помощи тега picture.

<picture>        <source media="(orientation: landscape)"                   srcset="land-small-car-image.jpg 200w,              land-medium-car-image.jpg 600w,              land-large-car-image.jpg 1000w"                   sizes="(min-width: 700px) 500px,             (min-width: 600px) 400px,             100vw">        <source media="(orientation: portrait)"                   srcset="port-small-car-image.jpg 700w,              port-medium-car-image.jpg 1200w,              port-large-car-image.jpg 1600w"                   sizes="(min-width: 768px) 700px,             (min-width: 1024px) 600px,             500px">        <img src="land-medium-car-image.jpg" alt="Car"></picture>

Если экран находится в альбомной ориентации, то браузер будет отображать изображения из первого набора, а если в портретной, то из второго набора. Кроме того, можно использовать атрибут media с параметрами max-width и min-width:

<picture>     <source media="(max-width: 767px)" ....>     <source media="(min-width: 768px)" ....></picture>

Последний тег img используется для обратной совместимости с браузерами, не поддерживающими теги picture.

Использование с частично поддерживаемыми типами изображений


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

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

Но мы можем с лёгкостью решить эту проблему при помощи тега Picture, поскольку он позволяет нам указать внутри несколько источников.

<picture>  <source srcset="test.avif" type="image/avif">  <source srcset="test.webp" type="image/webp">  <img src="test.png" alt="test image"></picture>

В показанный выше пример включены три типа изображений в форматах avif, webp и png. Сначала браузер попробует формат avif, если не получится, то попробует webp. Если браузер не поддерживает ни один из них, то использует изображение png.

Ситуация с тегом picture стала ещё интереснее, когда разработчики Chrome объявили о том, что во вкладке Rendering инструментов DevTools появится две новые эмуляции для эмулирования частично поддерживаемых типов изображений.

Начиная с Chrome 88 и далее можно использовать Chrome DevTools для проверки совместимости браузера с типами изображений.


Использование Chrome DevTools для эмулирования совместимости изображений

В заключение


Хоть мы и говорили о том, насколько лучше тег picture по сравнению с тегом img, я уверен, что img не умер и умрёт ещё не скоро.

Если мы будем с умом использовать имеющиеся у него атрибуты srcset и size, то можем выжать из тега img максимум. Например, можно решить проблему смены разрешения при помощи одного только тега img.

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

Среди прочих достоинств тега picture способность работать с частично поддерживаемыми типами изображений и поддержка Chrome DevTools.

Однако оба эти элемента имеют свои плюсы и минусы. Поэтому нам нужно обдумывать и выбирать наиболее подходящий к нашим требованиям элемент.



На правах рекламы


Эпичные серверы это VDS для размещения сайтов от маленького интернет-магазина на Opencart до серьёзных проектов с огромной аудиторией. Создавайте собственные конфигурации серверов в пару кликов!

Подписывайтесь на наш чат в Telegram.

Подробнее..

Перевод Вышел Bootstrap 5 оцениваем 7 главных нововведений

06.05.2021 16:19:55 | Автор: admin

Пройдя через несколько альфа- и бета-версий, наконец-то появился Bootstrap 5, на что у разработчиков ушло несколько месяцев. Новая версия претерпела серьезные изменения, включая отказ от поддержки Internet Explorer (IE) и зависимости jQuery. От IE было решено отказаться, потому что браузер занимает всего 3% рынка и его доля продолжает снижаться.

Что такое Bootstrap? Это самый популярный в мире CSS-фреймворк с открытым исходным кодом, который разработан командой Twitter. В v5 внесено сразу несколько критически важных изменений, давайте посмотрим, что там и как.

1. Отказ от поддержки jQuery


Больше Bootstrap не поддерживает библиотеку jQuery. Вместо этого команда разработчиков улучшила поддержку библиотеки JavaScript.

В целом, зависимость от jQuery не была в Bootstrap чем-то плохим. Наоборот, появление jQuery радикально изменило способ использования JavaScript. Это упростило написание задач на JavaScript, которые в противном случае требовали бы много строк кода.

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

Исходный файл уменьшился на 85 КБ, что очень важно, ведь Google считает фактор времени загрузки страницы для мобильных и веб-сайтов критическим.

При необходимости jQuery все равно можно использовать. Все плагины JavaScript при этом остаются доступными.

2. Настраиваемые свойства CSS


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

Соответственно, CSS custom properties делают CSS более гибким и программируемым. Для того, чтобы предотвратить появление конфликтов со сторонними CSS, используется префикс -bs.

Всего доступно два типа переменных: корневые и компонентные. Что касается первого класса, то доступ к ним можно получить везде, где загружен Bootstrap CSS. Эти переменные находятся в файле root.scss и являются частью скомпилированных файлов dist. Что касается второго класса, то эти переменные локальны в отдельных компонентах. Они помогают избежать случайного наследования стилей в таких компонентах, как вложенные таблицы.

3. Улучшенная система сеток (Grid)



Поскольку при переходе с 3 на 4 версию возникли некоторые проблемы, v5 сохраняет большую часть системы сеток, а не обновляет ее полностью. Вот некоторые изменения:

  • Вместо gutter ввели новые классы g* для указания отступов между ячейками.
  • Также были включены классы вертикального интервала.
  • У столбцов больше нет дефолтного значения position: relative.

4. Улучшенная документация


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

Разработчики добавили больше гибкости в настройку тем, чтобы сайты не были похожими друг на друга. Тему четвертой версии доработали, добавили контент и фрагменты кода для разработки поверх Sass (популярный препроцессор CSS). Пример стартового npm-проекта можно найти на Github.

Расширена и цветовая палитра, пользоваться которой теперь проще. Проделана дополнительная работа по улучшению цветового контраста.

5. Управление формой


Разработчики улучшили элементы управления формой, input groups и прочие компоненты.

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

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

6. Добавление API-утилит



Здесь разработчики Bootstrap не оригинальны, библиотеки утилит ранее добавили, например, создатели CSS-библиотеки Tailwind CSS.

Команда Bootstrap добавила возможность использования утилит еще в 4 версии, там это было организовано с использованием глобальных классов $ enable- *. В новой версии разработчики решили перейти на API, новый язык и синтаксис в Sass. Все это дает возможность создавать собственные утилиты, сохраняя при этом возможность удалять и или изменять дефолтные.

Для улучшения организации процесса работы некоторые утилиты из версии 4 переместили в раздел Helpers.

7. Новая библиотека иконок



Еще одно приятное нововведение добавление открытой библиотеки иконок, в которой содержится более 1300 элементов. Поскольку библиотека открыта, пользователи могут модифицировать иконки по своему вкусу.

Поскольку это изображения SVG, их можно быстро масштабировать и модифицировать, а также стилизовать с помощью CSS.

Установить иконки можно при помощи npm:

$ npm i bootstrap-icons

Кое-что еще


Кроме указанных нововведений, команда представила еще несколько:

  • Новый логотип. Иронизируя над этим достижением, сами разработчики поместили новинку на первое место в списке.
  • Новый компонент offcanvas. Он поставляется с настраиваемым фоном, body scroll и размещением. Компоненты offcanvas можно разместить с разных сторон от viewport. Настраиваются параметры посредством атрибутов данных или API JavaScript.
  • .card заменили на .accordion. Новинка все же использует плагин Collapse JavaScript, но с кастомными HTML и CSS.

Ну а загрузить Bootstrap 5 можно с официальной странички фреймворка.


Подробнее..

Модульные front-end блоки пишем свой мини фреймворк

09.05.2021 16:23:04 | Автор: admin

Доброго времени суток уважаемые читатели хабра. С каждым годом в веб разработке появляется все больше разнообразных решений которые используют модульный подход и упрощают разработку и редактирование кода. В данной статье я предлагаю вам свой взгляд на то, какими могут быть переиспользуемые front-end блоки (для проектов с бэкендом на php) и предлагаю пройти все шаги от идеи до реализации вместе со мной. Звучит интересно? Тогда добро пожаловать под кат.

Предисловие

Представлюсь - я молодой веб разработчик с опытом работы 5 лет. Крайний год я работаю на фрилансе и большая часть текущих проектов связана с WordPress. Несмотря на различую критику CMS в общем и WordPress в часности, я считаю сама архитектура WordPress это довольно удачное решение, хотя конечно не без определенных недостатков. И один из них на мой взгляд это шаблоны. В крайних обновлениях сделаны большие шаги чтобы это исправить, и Gutenberg в целом становится мощным инструментом, однако к сожалению в большинстве тем продолжается каша в шаблонах, стилях и скриптах, которая делает редактирование чего-либо крайне болезненным, а переиспользование кода зачастую невозможным. Именно эта проблема и подтолкнуло меня к идее своего мини фреймворка (читай пакета, но поскольку он будет предъявлять требования к структуре, то гордо назовем мини фреймворком), который бы организовывал структуру и позволял переиспользовать блоки.

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

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

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

Постановка задачи

Понятие блок ниже будет по сути тем же понятием что и блок в BEM методологии, т.е. это будет группа html/js/css кода которая будет представлять одну сущность.

Генерировать html и управлять зависимостями блоков мы будем через php, что говорит о том, что наш пакет будет подходить для проектов с бекендом на php. Также условимся на берегу что, не вдаваясь в споры, не будем поддаваться влиянию новомодных вещей, таких как css-in-js или bem-json и будем придерживаться эль-классико классического подхода, т.е. предполагать что html, css и js это разные файлы.

Теперь давайте сформулируем наши основные требования к будущему мини-фреймворку:

  • Обеспечить структуру блоков

  • Предоставить поддержку наследования (расширения) блоков

  • Предоставить возможность использовать блок в блоке и соответственно поддержку зависимости ресурсов одного блока от ресурсов других блоков

Структура мини фреймворка

Как условились выше, такие ресурсы как css и js всегда будут в виде обычных файлов, т.е. это будут .js и .css или .min.css и .min.js в случае использования препроцесссоров и сборщиков (как webpack например). Для вставки данных в html код мы будем использовать шаблонизатор Twig (для тех кто не знаком ссылка). Кто-то может заметить, что Php и сам по себе хороший шаблонизатор, не будем вдаваться в споры, кроме доводов указанных на главной странице проекта Twig, отмечу важный для меня пункт, то что он дисциплинирует, т.е. заставляет отделять обработку от вывода и подготавливать переменные заранее, и в данном случае мы будем использовать его.

Теперь давайте продумаем структуру нашего мини фреймворка более детально.

  1. Блок

    Каждый блок будет состоять из:

    1. Статических ресурсов (css/js/twig)

    2. Класса модели (его поля мы будет предоставлять как данные для twig шаблона)

    3. Класса контролера (он будет отвечать за наши ресурсы, их зависимости друг от друга и связывать модель с twig шаблоном)

  2. Вспомогательные классы : Класс Settings (будет содержать путь к блокам, их пространство имен и т.д.), класс обертка для Twig пакета

  3. Blocks класс

    Связующий класс, который :

    1. будет содержать вспомогательные классы (Settings, Twig)

    2. предоставлять функцию рендера блока

    3. содержать список использованных блоков, чтобы иметь возможность получить их ресурсы (css/js)

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

Требования к блокам

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

  • php 7.4+

  • Все блоки должны иметь одну родительскую директорию

  • Классы моделей и контроллеров должны иметь PSR-4 совместимое пространство имен с автозагрузчиком (PSR-4 де факто стандарт, если вы используете автозагрузчик от composer, т.е. указываете autoload/psr4 директиву в вашем composer.json то ваш проект уже соответствует этому требованию)

  • Соглашение об именах:

    • Имя контроллера должно содержать _C суффикс

    • Класс модели должен иметь то же пространство имен и то же имя (без суффикса) что и соответствующих контроллер

    • Имена ресурсов должны соответствовать имени контроллера, но с данными отличиями:

      • Без суффикса контроллера

      • Верблюжья нотация в имени должны быть заменена на тире (CamelCase = camel-case)

      • Нижнее подчеркивание в имени должно быть заменено на тире (just_block = just-block)

      • Таким образом по правилам выше имя ресурса с контроллером Block_Theme_Main_C будет blocktheme--main

Реализация

Пришло время перейти к реализации нашей идеи, т.е. к коду.

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

FIELDS_READER

Все наша магия при работе с моделями и контроллерами будет строится на функции get_class_vars которая предоставит нам имена полей класса и на ReflectionProperty классе, который предоставит нам информацию об этих полях, такую как видимость поля (protected/public) и его тип. Мы будем собирать информацию только о protected полях.

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

FIELDS_READER.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework;use Exception;use ReflectionProperty;abstract class FIELDS_READER {private array $_fieldsInfo;public function __construct() {$this->_fieldsInfo = [];$this->_readFieldsInfo();$this->_autoInitFields();}final protected function _getFieldsInfo(): array {return $this->_fieldsInfo;}protected function _getFieldType( string $fieldName ): ?string {$fieldType = null;try {// used static for child support$property = new ReflectionProperty( static::class, $fieldName );} catch ( Exception $ex ) {return $fieldType;}if ( ! $property->isProtected() ) {return $fieldType;}return $property->getType() ?$property->getType()->getName() :'';}private function _readFieldsInfo(): void {// get protected fields without the '__' prefix$fieldNames = array_keys( get_class_vars( static::class ) );$fieldNames = array_filter( $fieldNames, function ( $fieldName ) {$prefix = substr( $fieldName, 0, 2 );return '__' !== $prefix;} );foreach ( $fieldNames as $fieldName ) {$fieldType = $this->_getFieldType( $fieldName );// only protected fieldsif ( is_null( $fieldType ) ) {continue;}$this->_fieldsInfo[ $fieldName ] = $fieldType;}}private function _autoInitFields(): void {foreach ( $this->_fieldsInfo as $fieldName => $fieldType ) {// ignore fields without a typeif ( ! $fieldType ) {continue;}$defaultValue = null;switch ( $fieldType ) {case 'int':case 'float':$defaultValue = 0;break;case 'bool':$defaultValue = false;break;case 'string':$defaultValue = '';break;case 'array':$defaultValue = [];break;}try {if ( is_subclass_of( $fieldType, MODEL::class ) ||     is_subclass_of( $fieldType, CONTROLLER::class ) ) {$defaultValue = new $fieldType();}} catch ( Exception $ex ) {$defaultValue = null;}// ignore fields with a custom type (null by default)if ( is_null( $defaultValue ) ) {continue;}$this->{$fieldName} = $defaultValue;}}}
FIELDS_READERTest.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocksFramework\CONTROLLER;use LightSource\FrontBlocksFramework\FIELDS_READER;use LightSource\FrontBlocksFramework\MODEL;class FIELDS_READERTest extends Unit {public function testReadProtectedField() {$fieldsReader = new class extends FIELDS_READER {protected $_loadedField;public function __construct() {parent::__construct();}public function getFields() {return $this->_getFieldsInfo();}};$this->assertEquals( ['_loadedField' => '',], $fieldsReader->getFields() );}public function testIgnoreReadProtectedPrefixedField() {$fieldsReader = new class extends FIELDS_READER {protected $__unloadedField;public function __construct() {parent::__construct();}public function getFields() {return $this->_getFieldsInfo();}};$this->assertEquals( [], $fieldsReader->getFields() );}public function testIgnoreReadPublicField() {$fieldsReader = new class extends FIELDS_READER {public $unloadedField;public function __construct() {parent::__construct();}public function getFields() {return $this->_getFieldsInfo();}};$this->assertEquals( [], $fieldsReader->getFields() );}public function testIgnoreReadPrivateField() {$fieldsReader = new class extends FIELDS_READER {private $unloadedField;public function __construct() {parent::__construct();}public function getFields() {return $this->_getFieldsInfo();}};$this->assertEquals( [], $fieldsReader->getFields() );}public function testReadFieldWithType() {$fieldsReader = new class extends FIELDS_READER {protected string $_loadedField;public function __construct() {parent::__construct();}public function getFields() {return $this->_getFieldsInfo();}};$this->assertEquals( ['_loadedField' => 'string',], $fieldsReader->getFields() );}public function testReadFieldWithoutType() {$fieldsReader = new class extends FIELDS_READER {protected $_loadedField;public function __construct() {parent::__construct();}public function getFields() {return $this->_getFieldsInfo();}};$this->assertEquals( ['_loadedField' => '',], $fieldsReader->getFields() );}////public function testAutoInitIntField() {$fieldsReader = new class extends FIELDS_READER {protected int $_int;public function __construct() {parent::__construct();}public function getInt() {return $this->_int;}};$this->assertTrue( 0 === $fieldsReader->getInt() );}public function testAutoInitFloatField() {$fieldsReader = new class extends FIELDS_READER {protected float $_float;public function __construct() {parent::__construct();}public function getFloat() {return $this->_float;}};$this->assertTrue( 0.0 === $fieldsReader->getFloat() );}public function testAutoInitStringField() {$fieldsReader = new class extends FIELDS_READER {protected string $_string;public function __construct() {parent::__construct();}public function getString() {return $this->_string;}};$this->assertTrue( '' === $fieldsReader->getString() );}public function testAutoInitBoolField() {$fieldsReader = new class extends FIELDS_READER {protected bool $_bool;public function __construct() {parent::__construct();}public function getBool() {return $this->_bool;}};$this->assertTrue( false === $fieldsReader->getBool() );}public function testAutoInitArrayField() {$fieldsReader = new class extends FIELDS_READER {protected array $_array;public function __construct() {parent::__construct();}public function getArray() {return $this->_array;}};$this->assertTrue( [] === $fieldsReader->getArray() );}public function testAutoInitModelField() {$testModel        = new class extends MODEL {};$testModelClass   = get_class( $testModel );$fieldsReader     = new class ( $testModelClass ) extends FIELDS_READER {protected $_model;private $_testClass;public function __construct( $testClass ) {$this->_testClass = $testClass;parent::__construct();}public function _getFieldType( string $fieldName ): ?string {return ( '_model' === $fieldName ?$this->_testClass :parent::_getFieldType( $fieldName ) );}public function getModel() {return $this->_model;}};$actualModelClass = $fieldsReader->getModel() ?get_class( $fieldsReader->getModel() ) :'';$this->assertEquals( $actualModelClass, $testModelClass );}public function testAutoInitControllerField() {$testController      = new class extends CONTROLLER {};$testControllerClass = get_class( $testController );$fieldsReader        = new class ( $testControllerClass ) extends FIELDS_READER {protected $_controller;private $_testClass;public function __construct( $testControllerClass ) {$this->_testClass = $testControllerClass;parent::__construct();}public function _getFieldType( string $fieldName ): ?string {return ( '_controller' === $fieldName ?$this->_testClass :parent::_getFieldType( $fieldName ) );}public function getController() {return $this->_controller;}};$actualModelClass    = $fieldsReader->getController() ?get_class( $fieldsReader->getController() ) :'';$this->assertEquals( $actualModelClass, $testControllerClass );}public function testIgnoreInitFieldWithoutType() {$fieldsReader = new class extends FIELDS_READER {protected $_default;public function __construct() {parent::__construct();}public function getDefault() {return $this->_default;}};$this->assertTrue( null === $fieldsReader->getDefault() );}}

MODEL

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

MODEL.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework;abstract class MODEL extends FIELDS_READER {private bool $_isLoaded;public function __construct() {parent::__construct();$this->_isLoaded = false;}final public function isLoaded(): bool {return $this->_isLoaded;}public function getFields(): array {$args = [];$fieldsInfo = $this->_getFieldsInfo();foreach ( $fieldsInfo as $fieldName => $fieldType ) {$args[ $fieldName ] = $this->{$fieldName};}return $args;}final protected function _load(): void {$this->_isLoaded = true;}}
MODELTest.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocksFramework\MODEL;class MODELTest extends Unit {public function testGetFields() {$model = new class extends MODEL {protected string $_field1;public function __construct() {parent::__construct();}public function update() {$this->_field1 = 'just string';}};$model->update();$this->assertEquals( ['_field1'   => 'just string',], $model->getFields() );}}

CONTROLLER

Данный класс также как и MODEL наследует класс FIELDS_READER, однако имеет и другие важные задачи. Содержит два поля модель и массив __external, который пригодится нам далее при работе с twig шаблоном.

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

Метод getTemplateArgs будет возвращать данные для twig шаблона, это все protected поля соответствующей модели (без префикса _ если есть) и два дополнительных поля, _template и _isLoaded, первое будет содержать путь к шаблону, а второе отображать состояние модели. Также в этом методе мы реализуем возможность использовать блок в блоке (т.е. иметь класс Model в другом классе Model как поле) - мы соединяем поля контроллера и поля соответствующей модели по имени : т.е. если каждому полю с типом контроллер мы находим соответствующее поле в модели (с типом модель), то мы инициализируем поле контроллер моделью и вызываем метод getTemplateArgs у этого контроллера, получая таким образом все необходимую информацию для отображения этого вложенного блока.

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

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

CONTROLLER.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework;use Exception;abstract class CONTROLLER extends FIELDS_READER {const TEMPLATE_KEY__TEMPLATE = '_template';const TEMPLATE_KEY__IS_LOADED = '_isLoaded';private ?MODEL $_model;// using the prefix to prevent load this fieldprotected array $__external;public function __construct( ?MODEL $model = null ) {parent::__construct();$this->_model     = $model;$this->__external = [];$this->_autoInitModel();}final public static function GetResourceInfo( Settings $settings, string $controllerClass = '' ): array {// using static for children support$controllerClass = ! $controllerClass ?static::class :$controllerClass;// e.g. $controllerClass = Example/Theme/Main/Example_Theme_Main_C$resourceInfo = ['resourceName'         => '',// e.g. example--theme--main'relativePath'         => '',// e.g. Example/Theme/Main'relativeResourcePath' => '', // e.g. Example/Theme/Main/example--theme--main];$controllerSuffix = Settings::$ControllerSuffix;//  e.g. Example/Theme/Main/Example_Theme_Main$relativeControllerNamespace = $settings->getBlocksDirNamespace() ?str_replace( $settings->getBlocksDirNamespace() . '\\', '', $controllerClass ) :$controllerClass;$relativeControllerNamespace = substr( $relativeControllerNamespace, 0, mb_strlen( $relativeControllerNamespace ) - mb_strlen( $controllerSuffix ) );// e.g. Example_Theme_Main$phpBlockName = explode( '\\', $relativeControllerNamespace );$phpBlockName = $phpBlockName[ count( $phpBlockName ) - 1 ];// e.g. example--theme--main (from Example_Theme_Main)$blockNameParts    = preg_split( '/(?=[A-Z])/', $phpBlockName, - 1, PREG_SPLIT_NO_EMPTY );$blockResourceName = [];foreach ( $blockNameParts as $blockNamePart ) {$blockResourceName[] = strtolower( $blockNamePart );}$blockResourceName = implode( '-', $blockResourceName );$blockResourceName = str_replace( '_', '-', $blockResourceName );// e.g. Example/Theme/Main$relativePath = explode( '\\', $relativeControllerNamespace );$relativePath = array_slice( $relativePath, 0, count( $relativePath ) - 1 );$relativePath = implode( DIRECTORY_SEPARATOR, $relativePath );$resourceInfo['resourceName']         = $blockResourceName;$resourceInfo['relativePath']         = $relativePath;$resourceInfo['relativeResourcePath'] = $relativePath . DIRECTORY_SEPARATOR . $blockResourceName;return $resourceInfo;}// can be overridden if Controller doesn't have own twig (uses parents)public static function GetPathToTwigTemplate( Settings $settings, string $controllerClass = '' ): string {return self::GetResourceInfo( $settings, $controllerClass )['relativeResourcePath'] . $settings->getTwigExtension();}// can be overridden if Controller doesn't have own model (uses parents)public static function GetModelClass(): string {$controllerClass = static::class;$modelClass      = rtrim( $controllerClass, Settings::$ControllerSuffix );return ( $modelClass !== $controllerClass &&         class_exists( $modelClass, true ) &&         is_subclass_of( $modelClass, MODEL::class ) ?$modelClass :'' );}public static function OnLoad() {}final public function setModel( MODEL $model ): void {$this->_model = $model;}private function _getControllerField( string $fieldName ): ?CONTROLLER {$controller = null;$fieldsInfo = $this->_getFieldsInfo();if ( key_exists( $fieldName, $fieldsInfo ) ) {$controller = $this->{$fieldName};// prevent possible recursion by a mistake (if someone will create a field with self)// using static for children support$controller = ( $controller &&                $controller instanceof CONTROLLER ||                get_class( $controller ) !== static::class ) ?$controller :null;}return $controller;}public function getTemplateArgs( Settings $settings ): array {$modelFields  = $this->_model ?$this->_model->getFields() :[];$templateArgs = [];foreach ( $modelFields as $modelFieldName => $modelFieldValue ) {$templateFieldName = ltrim( $modelFieldName, '_' );if ( ! $modelFieldValue instanceof MODEL ) {$templateArgs[ $templateFieldName ] = $modelFieldValue;continue;}$modelFieldController = $this->_getControllerField( $modelFieldName );$modelFieldArgs       = [];$externalFieldArgs    = $this->__external[ $modelFieldName ] ?? [];if ( $modelFieldController ) {$modelFieldController->setModel( $modelFieldValue );$modelFieldArgs = $modelFieldController->getTemplateArgs( $settings );}$templateArgs[ $templateFieldName ] = HELPER::ArrayMergeRecursive( $modelFieldArgs, $externalFieldArgs );}// using static for children supportreturn array_merge( $templateArgs, [self::TEMPLATE_KEY__TEMPLATE  => static::GetPathToTwigTemplate( $settings ),self::TEMPLATE_KEY__IS_LOADED => ( $this->_model && $this->_model->isLoaded() ),] );}public function getDependencies( string $sourceClass = '' ): array {$dependencyClasses = [];$controllerFields  = $this->_getFieldsInfo();foreach ( $controllerFields as $fieldName => $fieldType ) {$dependencyController = $this->_getControllerField( $fieldName );if ( ! $dependencyController ) {continue;}$dependencyClass = get_class( $dependencyController );// 1. prevent the possible permanent recursion// 2. add only unique elements, because several fields can have the same typeif ( ( $sourceClass && $dependencyClass === $sourceClass ) ||     in_array( $dependencyClass, $dependencyClasses, true ) ) {continue;}// used static for child support$subDependencies = $dependencyController->getDependencies( static::class );// only unique elements$subDependencies = array_diff( $subDependencies, $dependencyClasses );// sub dependencies are before the main dependency$dependencyClasses = array_merge( $dependencyClasses, $subDependencies, [ $dependencyClass, ] );}return $dependencyClasses;}// Can be overridden for declare a target model class and provide an IDE supportpublic function getModel(): ?MODEL {return $this->_model;}private function _autoInitModel() {if ( $this->_model ) {return;}$modelClass = static::GetModelClass();try {$this->_model = $modelClass ?new $modelClass() :$this->_model;} catch ( Exception $ex ) {$this->_model = null;}}}
CONTROLLERTest.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocksFramework\{CONTROLLER,MODEL,Settings};class CONTROLLERTest extends Unit {private function _getModel( array $fields, bool $isLoaded = false ): MODEL {return new class ( $fields, $isLoaded ) extends MODEL {private array $_fields;public function __construct( array $fields, bool $isLoaded ) {parent::__construct();$this->_fields = $fields;if ( $isLoaded ) {$this->_load();}}public function getFields(): array {return $this->_fields;}};}private function _getController( ?MODEL $model ): CONTROLLER {return new class ( $model ) extends CONTROLLER {public function __construct( ?MODEL $model = null ) {parent::__construct( $model );}};}private function _getTemplateArgsWithoutAdditional( array $templateArgs ) {$templateArgs = array_diff_key( $templateArgs, [CONTROLLER::TEMPLATE_KEY__TEMPLATE  => '',CONTROLLER::TEMPLATE_KEY__IS_LOADED => '',] );foreach ( $templateArgs as $templateKey => $templateValue ) {if ( ! is_array( $templateValue ) ) {continue;}$templateArgs[ $templateKey ] = $this->_getTemplateArgsWithoutAdditional( $templateValue );}return $templateArgs;}////public function testGetResourceInfoWithoutCamelCaseInBlockName() {$settings = new Settings();$settings->setControllerSuffix( '_C' );$settings->setBlocksDirNamespace( 'Namespace' );$this->assertEquals( ['resourceName'         => 'block','relativePath'         => 'Block','relativeResourcePath' => 'Block/block',], CONTROLLER::GetResourceInfo( $settings, 'Namespace\\Block\\Block_C' ) );}public function testGetResourceInfoWithCamelCaseInBlockName() {$settings = new Settings();$settings->setControllerSuffix( '_C' );$settings->setBlocksDirNamespace( 'Namespace' );$this->assertEquals( ['resourceName'         => 'block-name','relativePath'         => 'BlockName','relativeResourcePath' => 'BlockName/block-name',], CONTROLLER::GetResourceInfo( $settings, 'Namespace\\BlockName\\BlockName_C' ) );}public function testGetResourceInfoWithoutCamelCaseInTheme() {$settings = new Settings();$settings->setControllerSuffix( '_C' );$settings->setBlocksDirNamespace( 'Namespace' );$this->assertEquals( ['resourceName'         => 'block--theme--main','relativePath'         => 'Block/Theme/Main','relativeResourcePath' => 'Block/Theme/Main/block--theme--main',], CONTROLLER::GetResourceInfo( $settings, 'Namespace\\Block\\Theme\\Main\\Block_Theme_Main_C' ) );}public function testGetResourceInfoWithCamelCaseInTheme() {$settings = new Settings();$settings->setControllerSuffix( '_C' );$settings->setBlocksDirNamespace( 'Namespace' );$this->assertEquals( ['resourceName'         => 'block--theme--just-main','relativePath'         => 'Block/Theme/JustMain','relativeResourcePath' => 'Block/Theme/JustMain/block--theme--just-main',], CONTROLLER::GetResourceInfo( $settings, 'Namespace\\Block\\Theme\\JustMain\\Block_Theme_JustMain_C' ) );}////public function testGetTemplateArgsWhenModelContainsBuiltInTypes() {$settings   = new Settings();$model      = $this->_getModel( ['stringVariable' => 'just string',] );$controller = $this->_getController( $model );$this->assertEquals( ['stringVariable' => 'just string',], $this->_getTemplateArgsWithoutAdditional( $controller->getTemplateArgs( $settings ) ) );}public function testGetTemplateArgsWhenModelContainsAnotherModel() {$settings = new Settings();$modelA              = $this->_getModel( ['_modelA' => 'just string from model a',] );$modelB              = $this->_getModel( ['_modelA' => $modelA,'_modelB' => 'just string from model b',] );$controllerForModelA = $this->_getController( null );$controllerForModelB = new class ( $modelB, $controllerForModelA ) extends CONTROLLER {protected $_modelA;public function __construct( ?MODEL $model = null, $controllerForModelA ) {parent::__construct( $model );$this->_modelA = $controllerForModelA;}};$this->assertEquals( ['modelA' => ['modelA' => 'just string from model a',],'modelB' => 'just string from model b',], $this->_getTemplateArgsWithoutAdditional( $controllerForModelB->getTemplateArgs( $settings ) ) );}public function testGetTemplateArgsWhenControllerContainsExternalArgs() {$settings = new Settings();$modelA              = $this->_getModel( ['_additionalField' => '','_modelA'          => 'just string from model a',] );$modelB              = $this->_getModel( ['_modelA' => $modelA,'_modelB' => 'just string from model b',] );$controllerForModelA = $this->_getController( null );$controllerForModelB = new class ( $modelB, $controllerForModelA ) extends CONTROLLER {protected $_modelA;public function __construct( ?MODEL $model = null, $controllerForModelA ) {parent::__construct( $model );$this->_modelA               = $controllerForModelA;$this->__external['_modelA'] = ['additionalField' => 'additionalValue',];}};$this->assertEquals( ['modelA' => ['additionalField' => 'additionalValue','modelA'          => 'just string from model a',],'modelB' => 'just string from model b',], $this->_getTemplateArgsWithoutAdditional( $controllerForModelB->getTemplateArgs( $settings ) ) );}public function testGetTemplateArgsContainsAdditionalFields() {$settings   = new Settings();$model      = $this->_getModel( [] );$controller = $this->_getController( $model );$this->assertEquals( [CONTROLLER::TEMPLATE_KEY__TEMPLATE,CONTROLLER::TEMPLATE_KEY__IS_LOADED,], array_keys( $controller->getTemplateArgs( $settings ) ) );}public function testGetTemplateArgsWhenAdditionalIsLoadedIsFalse() {$settings   = new Settings();$model      = $this->_getModel( [] );$controller = $this->_getController( $model );$actual = array_intersect_key( $controller->getTemplateArgs( $settings ), [ CONTROLLER::TEMPLATE_KEY__IS_LOADED => '', ] );$this->assertEquals( [ CONTROLLER::TEMPLATE_KEY__IS_LOADED => false, ], $actual );}public function testGetTemplateArgsWhenAdditionalIsLoadedIsTrue() {$settings   = new Settings();$model      = $this->_getModel( [], true );$controller = $this->_getController( $model );$actual = array_intersect_key( $controller->getTemplateArgs( $settings ), [ CONTROLLER::TEMPLATE_KEY__IS_LOADED => '', ] );$this->assertEquals( [ CONTROLLER::TEMPLATE_KEY__IS_LOADED => true, ], $actual );}public function testGetTemplateArgsAdditionalTemplateIsRight() {$settings   = new Settings();$model      = $this->_getModel( [] );$controller = $this->_getController( $model );$actual = array_intersect_key( $controller->getTemplateArgs( $settings ), [ CONTROLLER::TEMPLATE_KEY__TEMPLATE => '', ] );$this->assertEquals( [CONTROLLER::TEMPLATE_KEY__TEMPLATE => $controller::GetPathToTwigTemplate( $settings ),], $actual );}////public function testGetDependencies() {$controllerA = $this->_getController( null );$controllerB = new class ( null, $controllerA ) extends CONTROLLER {protected $_controllerA;public function __construct( ?MODEL $model = null, $controllerA ) {parent::__construct( $model );$this->_controllerA = $controllerA;}};$this->assertEquals( [get_class( $controllerA ),], $controllerB->getDependencies() );}public function testGetDependenciesWithSubDependencies() {$controllerA = new class extends CONTROLLER {public function getDependencies( string $sourceClass = '' ): array {return ['A',];}};$controllerB = new class ( null, $controllerA ) extends CONTROLLER {protected $_controllerA;public function __construct( ?MODEL $model = null, $controllerA ) {parent::__construct( $model );$this->_controllerA = $controllerA;}};$this->assertEquals( ['A',get_class( $controllerA ),], $controllerB->getDependencies() );}public function testGetDependenciesWithSubDependenciesRecursively() {$controllerA = new class extends CONTROLLER {public function getDependencies( string $sourceClass = '' ): array {return ['A',];}};$controllerB = new class ( null, $controllerA ) extends CONTROLLER {protected $_controllerA;public function __construct( ?MODEL $model = null, $controllerA ) {parent::__construct( $model );$this->_controllerA = $controllerA;}};$controllerC = new class ( null, $controllerB ) extends CONTROLLER {protected $_controllerB;public function __construct( ?MODEL $model = null, $controllerB ) {parent::__construct( $model );$this->_controllerB = $controllerB;}};$this->assertEquals( ['A',get_class( $controllerA ),get_class( $controllerB ),], $controllerC->getDependencies() );}public function testGetDependenciesWithSubDependenciesInOrderWhenSubBeforeMainDependency() {$controllerA = new class extends CONTROLLER {public function getDependencies( string $sourceClass = '' ): array {return ['A',];}};$controllerB = new class ( null, $controllerA ) extends CONTROLLER {protected $_controllerA;public function __construct( ?MODEL $model = null, $controllerA ) {parent::__construct( $model );$this->_controllerA = $controllerA;}};$this->assertEquals( ['A',get_class( $controllerA ),], $controllerB->getDependencies() );}public function testGetDependenciesWithSubDependenciesWhenBlocksAreDependentFromEachOther() {$controllerA = new class extends CONTROLLER {protected $_controllerB;public function setControllerB( $controllerB ) {$this->_controllerB = $controllerB;}};$controllerB = new class ( null, $controllerA ) extends CONTROLLER {protected $_controllerA;public function __construct( ?MODEL $model = null, $controllerA ) {parent::__construct( $model );$this->_controllerA = $controllerA;}};$controllerA->setControllerB( $controllerB );$this->assertEquals( [get_class( $controllerA ),], $controllerB->getDependencies() );}public function testGetDependenciesWithoutDuplicatesWhenSeveralWithOneType() {$controllerA = $this->_getController( null );$controllerB = new class ( null, $controllerA ) extends CONTROLLER {protected $_controllerA;protected $_controllerAA;protected $_controllerAAA;public function __construct( ?MODEL $model = null, $controllerA ) {parent::__construct( $model );$this->_controllerA   = $controllerA;$this->_controllerAA  = $controllerA;$this->_controllerAAA = $controllerA;}};$this->assertEquals( [get_class( $controllerA ),], $controllerB->getDependencies() );}////public function testAutoInitModel() {$modelClass      = str_replace( [ '::', '\\' ], '_', __METHOD__ );$controllerClass = $modelClass . Settings::$ControllerSuffix;eval( 'class ' . $modelClass . ' extends ' . MODEL::class . ' {}' );eval( 'class ' . $controllerClass . ' extends ' . CONTROLLER::class . ' {}' );$controller = new $controllerClass();$actualModelClass = $controller->getModel() ?get_class( $controller->getModel() ) :'';$this->assertEquals( $modelClass, $actualModelClass );}public function testAutoInitModelWhenModelHasWrongClass() {$modelClass      = str_replace( [ '::', '\\' ], '_', __METHOD__ );$controllerClass = $modelClass . Settings::$ControllerSuffix;eval( 'class ' . $modelClass . ' {}' );eval( 'class ' . $controllerClass . ' extends ' . CONTROLLER::class . ' {}' );$controller = new $controllerClass();$this->assertEquals( null, $controller->getModel() );}}

Settings

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

Settings.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework;class Settings {public static string $ControllerSuffix = '_C';private string $_blocksDirPath;private string $_blocksDirNamespace;private array $_twigArgs;private string $_twigExtension;private $_errorCallback;public function __construct() {$this->_blocksDirPath      = '';$this->_blocksDirNamespace = '';$this->_twigArgs           = [// will generate exception if a var doesn't exist instead of replace to NULL'strict_variables' => true,// disable autoescape to prevent break data'autoescape'       => false,];$this->_twigExtension      = '.twig';$this->_errorCallback      = null;}public function setBlocksDirPath( string $blocksDirPath ): void {$this->_blocksDirPath = $blocksDirPath;}public function setBlocksDirNamespace( string $blocksDirNamespace ): void {$this->_blocksDirNamespace = $blocksDirNamespace;}public function setTwigArgs( array $twigArgs ): void {$this->_twigArgs = array_merge( $this->_twigArgs, $twigArgs );}public function setErrorCallback( ?callable $errorCallback ): void {$this->_errorCallback = $errorCallback;}public function setTwigExtension( string $twigExtension ): void {$this->_twigExtension = $twigExtension;}public function setControllerSuffix( string $controllerSuffix ): void {$this->_controllerSuffix = $controllerSuffix;}public function getBlocksDirPath(): string {return $this->_blocksDirPath;}public function getBlocksDirNamespace(): string {return $this->_blocksDirNamespace;}public function getTwigArgs(): array {return $this->_twigArgs;}public function getTwigExtension(): string {return $this->_twigExtension;}public function callErrorCallback( array $errors ): void {if ( ! is_callable( $this->_errorCallback ) ) {return;}call_user_func_array( $this->_errorCallback, [ $errors, ] );}}

Twig

Также вспомогательный класс, лишь уточню что мы расширили twig своей функцией _include (которая является оберткой для встроенного и использует наши поля _isLoaded и _template из метода CONROLLER->getTemplateArgs выше) и фильтр _merge (который отличается тем, что рекурсивно сливает массивы).

Twig.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework;use Exception;use Twig\Environment;use Twig\Loader\FilesystemLoader;use Twig\Loader\LoaderInterface;use Twig\TwigFilter;use Twig\TwigFunction;class Twig {private ?LoaderInterface $_twigLoader;private ?Environment $_twigEnvironment;private Settings $_settings;public function __construct( Settings $settings, ?LoaderInterface $twigLoader = null ) {$this->_twigEnvironment = null;$this->_settings        = $settings;$this->_twigLoader      = $twigLoader;$this->_init();}// e.g for extend a twig with adding a new filterpublic function getEnvironment(): ?Environment {return $this->_twigEnvironment;}private function _extendTwig(): void {$this->_twigEnvironment->addFilter( new TwigFilter( '_merge', function ( $source, $additional ) {return HELPER::ArrayMergeRecursive( $source, $additional );} ) );$this->_twigEnvironment->addFunction( new TwigFunction( '_include', function ( $block, $args = [] ) {$block = HELPER::ArrayMergeRecursive( $block, $args );return $block[ CONTROLLER::TEMPLATE_KEY__IS_LOADED ] ?$this->render( $block[ CONTROLLER::TEMPLATE_KEY__TEMPLATE ], $block ) :'';} ) );}private function _init(): void {try {$this->_twigLoader      = ! $this->_twigLoader ?new FilesystemLoader( $this->_settings->getBlocksDirPath() ) :$this->_twigLoader;$this->_twigEnvironment = new Environment( $this->_twigLoader, $this->_settings->getTwigArgs() );} catch ( Exception $ex ) {$this->_twigEnvironment = null;$this->_settings->callErrorCallback( ['message' => $ex->getMessage(),'file'    => $ex->getFile(),'line'    => $ex->getLine(),'trace'   => $ex->getTraceAsString(),] );return;}$this->_extendTwig();}public function render( string $template, array $args = [], bool $isPrint = false ): string {$html = '';// twig isn't loadedif ( is_null( $this->_twigEnvironment ) ) {return $html;}try {// will generate ean exception if a template doesn't exist OR broken// also if a var doesn't exist (if using a 'strict_variables' flag, see Twig_Environment->__construct)$html .= $this->_twigEnvironment->render( $template, $args );} catch ( Exception $ex ) {$html = '';$this->_settings->callErrorCallback( ['message'  => $ex->getMessage(),'file'     => $ex->getFile(),'line'     => $ex->getLine(),'trace'    => $ex->getTraceAsString(),'template' => $template,] );}if ( $isPrint ) {echo $html;}return $html;}}
TwigTest.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework\Tests\unit;use Codeception\Test\Unit;use Exception;use LightSource\FrontBlocksFramework\CONTROLLER;use LightSource\FrontBlocksFramework\Settings;use LightSource\FrontBlocksFramework\Twig;use Twig\Loader\ArrayLoader;class TwigTest extends Unit {private function _renderBlock( array $blocks, string $renderBlock, array $renderArgs = [] ): string {$twigLoader = new ArrayLoader( $blocks );$settings   = new Settings();$twig    = new Twig( $settings, $twigLoader );$content = '';try {$content = $twig->render( $renderBlock, $renderArgs );} catch ( Exception $ex ) {$this->fail( 'Twig render exception, ' . $ex->getMessage() );}return $content;}public function testExtendTwigIncludeFunctionWhenBlockIsLoaded() {$blocks      = ['block-a.twig' => '{{ _include(blockB) }}','block-b.twig' => 'block-b content',];$renderBlock = 'block-a.twig';$renderArgs  = ['blockB' => [CONTROLLER::TEMPLATE_KEY__TEMPLATE  => 'block-b.twig',CONTROLLER::TEMPLATE_KEY__IS_LOADED => true,],];$this->assertEquals( 'block-b content', $this->_renderBlock( $blocks, $renderBlock, $renderArgs ) );}public function testExtendTwigIncludeFunctionWhenBlockNotLoaded() {$blocks      = ['block-a.twig' => '{{ _include(blockB) }}','block-b.twig' => 'block-b content',];$renderBlock = 'block-a.twig';$renderArgs  = ['blockB' => [CONTROLLER::TEMPLATE_KEY__TEMPLATE  => 'block-b.twig',CONTROLLER::TEMPLATE_KEY__IS_LOADED => false,],];$this->assertEquals( '', $this->_renderBlock( $blocks, $renderBlock, $renderArgs ) );}public function testExtendTwigIncludeFunctionWhenArgsPassed() {$blocks      = ['block-a.twig' => '{{ _include(blockB, {classes:["test-class",],}) }}','block-b.twig' => '{{ classes|join(" ") }}',];$renderBlock = 'block-a.twig';$renderArgs  = ['blockB' => [CONTROLLER::TEMPLATE_KEY__TEMPLATE  => 'block-b.twig',CONTROLLER::TEMPLATE_KEY__IS_LOADED => true,'classes'                           => [ 'own-class', ],],];$this->assertEquals( 'own-class test-class', $this->_renderBlock( $blocks, $renderBlock, $renderArgs ) );}public function testExtendTwigMergeFilter() {$blocks      = ['block-a.twig' => '{{ {"array":["a",],}|_merge({"array":["b",],}).array|join(" ") }}',];$renderBlock = 'block-a.twig';$renderArgs  = [];$this->assertEquals( 'a b', $this->_renderBlock( $blocks, $renderBlock, $renderArgs ) );}}

Blocks

Это наш объединяющий класс.

Статический метод LoadAll загружает все контроллеры и вызывает статический метод OnLoad у каждого контроллера (в нашем случае это не используется, как было указано выше это необходимо для расширения и выходит за рамки статьи).

Метод renderBlock принимает объект контроллера и производит рендер блока, передавая в twig шаблон аргументы из метода CONROLLER->getTemplateArgs выше. Также добавляет класс используемого контроллера и классы всех его зависимостей в список использованных блоков, что позволит нам далее получить используемый css и js.

Ну и наконец метод getUsedResources используя список выше и статический метод CONTROLLER::GetResourceInfo позволяет нам после рендера блоков получить используемый css и js код, объединенный в правильной последовательности, т.е. с учетом всех зависимостей./

Blocks.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework;class Blocks {private array $_loadedControllerClasses;private array $_usedControllerClasses;private Settings $_settings;private Twig $_twig;public function __construct( Settings $settings ) {$this->_loadedControllerClasses = [];$this->_usedControllerClasses   = [];$this->_settings                = $settings;$this->_twig                    = new Twig( $settings );}final public function getLoadedControllerClasses(): array {return $this->_loadedControllerClasses;}final public function getUsedControllerClasses(): array {return $this->_usedControllerClasses;}final public function getSettings(): Settings {return $this->_settings;}final public function getTwig(): Twig {return $this->_twig;}final public function getUsedResources( string $extension, bool $isIncludeSource = false ): string {$resourcesContent = '';foreach ( $this->_usedControllerClasses as $usedControllerClass ) {$getResourcesInfoCallback = [ $usedControllerClass, 'GetResourceInfo' ];if ( ! is_callable( $getResourcesInfoCallback ) ) {$this->_settings->callErrorCallback( ['message' => "Controller class doesn't exist",'class'   => $usedControllerClass,] );continue;}$resourceInfo = call_user_func_array( $getResourcesInfoCallback, [$this->_settings,] );$pathToResourceFile = $this->_settings->getBlocksDirPath() . DIRECTORY_SEPARATOR . $resourceInfo['relativeResourcePath'] . $extension;if ( ! is_file( $pathToResourceFile ) ) {continue;}$resourcesContent .= $isIncludeSource ?"\n/* " . $resourceInfo['resourceName'] . " */\n" :'';$resourcesContent .= file_get_contents( $pathToResourceFile );}return $resourcesContent;}private function _loadController( string $phpClass, array $debugArgs ): bool {$isLoaded = false;if ( ! class_exists( $phpClass, true ) ||     ! is_subclass_of( $phpClass, CONTROLLER::class ) ) {$this->_settings->callErrorCallback( ['message' => "Class doesn't exist or doesn't child",'args'    => $debugArgs,] );return $isLoaded;}call_user_func( [ $phpClass, 'OnLoad' ] );return true;}private function _loadControllers( string $directory, string $namespace, array $controllerFileNames ): void {foreach ( $controllerFileNames as $controllerFileName ) {$phpFile   = implode( DIRECTORY_SEPARATOR, [ $directory, $controllerFileName ] );$phpClass  = implode( '\\', [ $namespace, str_replace( '.php', '', $controllerFileName ), ] );$debugArgs = ['directory' => $directory,'namespace' => $namespace,'phpFile'   => $phpFile,'phpClass'  => $phpClass,];if ( ! $this->_loadController( $phpClass, $debugArgs ) ) {continue;}$this->_loadedControllerClasses[] = $phpClass;}}private function _loadDirectory( string $directory, string $namespace ): void {// exclude ., ..$fs = array_diff( scandir( $directory ), [ '.', '..' ] );$controllerFilePreg = '/' . Settings::$ControllerSuffix . '.php$/';$controllerFileNames = HELPER::ArrayFilter( $fs, function ( $f ) use ( $controllerFilePreg ) {return ( 1 === preg_match( $controllerFilePreg, $f ) );}, false );$subDirectoryNames   = HELPER::ArrayFilter( $fs, function ( $f ) {return false === strpos( $f, '.' );}, false );foreach ( $subDirectoryNames as $subDirectoryName ) {$subDirectory = implode( DIRECTORY_SEPARATOR, [ $directory, $subDirectoryName ] );$subNamespace = implode( '\\', [ $namespace, $subDirectoryName ] );$this->_loadDirectory( $subDirectory, $subNamespace );}$this->_loadControllers( $directory, $namespace, $controllerFileNames );}final public function loadAll(): void {$directory = $this->_settings->getBlocksDirPath();$namespace = $this->_settings->getBlocksDirNamespace();$this->_loadDirectory( $directory, $namespace );}final public function renderBlock( CONTROLLER $controller, array $args = [], bool $isPrint = false ): string {$dependencies                 = array_merge( $controller->getDependencies(), [ get_class( $controller ), ] );$newDependencies              = array_diff( $dependencies, $this->_usedControllerClasses );$this->_usedControllerClasses = array_merge( $this->_usedControllerClasses, $newDependencies );$templateArgs = $controller->getTemplateArgs( $this->_settings );$templateArgs = HELPER::ArrayMergeRecursive( $templateArgs, $args );return $this->_twig->render( $templateArgs[ CONTROLLER::TEMPLATE_KEY__TEMPLATE ], $templateArgs, $isPrint );}}
BlocksTest.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework\Tests\unit;use Codeception\Test\Unit;use Exception;use LightSource\FrontBlocksFramework\Blocks;use LightSource\FrontBlocksFramework\CONTROLLER;use LightSource\FrontBlocksFramework\MODEL;use LightSource\FrontBlocksFramework\Settings;use LightSource\FrontBlocksFramework\Twig;use org\bovigo\vfs\vfsStream;use org\bovigo\vfs\vfsStreamDirectory;class BlocksTest extends Unit {private function _getBlocks( string $namespace, vfsStreamDirectory $rootDirectory, array $structure, array $usedControllerClasses = [] ): ?Blocks {vfsStream::create( $structure, $rootDirectory );$settings = new Settings();$settings->setBlocksDirNamespace( $namespace );$settings->setBlocksDirPath( $rootDirectory->url() );$twig = $this->make( Twig::class, ['render' => function ( string $template, array $args = [], bool $isPrint = false ): string {return '';},] );try {$blocks = $this->make( Blocks::class, ['_loadedControllerClasses' => [],'_usedControllerClasses'   => $usedControllerClasses,'_twig'                    => $twig,'_settings'                => $settings,] );} catch ( Exception $ex ) {$this->fail( "Can't make Blocks stub, " . $ex->getMessage() );}$blocks->loadAll();return $blocks;}// get a unique namespace depending on a test method to prevent affect other testsprivate function _getUniqueControllerNamespaceWithAutoloader( string $methodConstant, vfsStreamDirectory $rootDirectory ): string {$namespace = str_replace( '::', '_', $methodConstant );spl_autoload_register( function ( $class ) use ( $rootDirectory, $namespace ) {$targetNamespace = $namespace . '\\';if ( 0 !== strpos( $class, $targetNamespace ) ) {return;}$relativePathToFile = str_replace( $targetNamespace, '', $class );$relativePathToFile = str_replace( '\\', '/', $relativePathToFile );$absPathToFile = $rootDirectory->url() . DIRECTORY_SEPARATOR . $relativePathToFile . '.php';include_once $absPathToFile;} );return $namespace;}// get a unique directory name depending on a test method to prevent affect other testsprivate function _getUniqueDirectory( string $methodConstant ): vfsStreamDirectory {$dirName = str_replace( [ ':', '\\' ], '_', $methodConstant );return vfsStream::setup( $dirName );}private function _getControllerClassFile( string $namespace, string $class ): string {$vendorControllerClass = '\LightSource\FrontBlocksFramework\CONTROLLER';return '<?php namespace ' . $namespace . '; class ' . $class . ' extends ' . $vendorControllerClass . ' {}';}private function _getController( array $dependencies = [] ) {return new class ( null, $dependencies ) extends CONTROLLER {private array $_dependencies;public function __construct( ?MODEL $model = null, array $dependencies ) {parent::__construct( $model );$this->_dependencies = $dependencies;}function getDependencies( string $sourceClass = '' ): array {return $this->_dependencies;}function getTemplateArgs( Settings $settings ): array {return [CONTROLLER::TEMPLATE_KEY__TEMPLATE => '',];}};}////public function testLoadAllControllersWithPrefix() {// fixme$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, ['Block' => ['Block_C.php' => $this->_getControllerClassFile( "{$namespace}\Block", 'Block_C' ),],] );$this->assertEquals( ["{$namespace}\Block\Block_C",], $blocks->getLoadedControllerClasses() );}public function testLoadAllIgnoreControllersWithoutPrefix() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, ['Block' => ['Block.php' => $this->_getControllerClassFile( "{$namespace}\Block", 'Block' ),],] );$this->assertEquals( [], $blocks->getLoadedControllerClasses() );}public function testLoadAllIgnoreWrongControllers() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, ['Block' => ['Block_C.php' => $this->_getControllerClassFile( "{$namespace}\Block", 'WrongBlock_C' ),],] );$this->assertEquals( [], $blocks->getLoadedControllerClasses() );}////public function testRenderBlockAddsControllerToUsedList() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, [] );$controller    = $this->_getController();$blocks->renderBlock( $controller );$this->assertEquals( [get_class( $controller ),], $blocks->getUsedControllerClasses() );}public function testRenderBlockAddsControllerDependenciesToUsedList() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, [] );$controller    = $this->_getController( [ 'A', ] );$blocks->renderBlock( $controller );$this->assertEquals( ['A',get_class( $controller ),], $blocks->getUsedControllerClasses() );}public function testRenderBlockAddsDependenciesBeforeControllerToUsedList() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, [] );$controller    = $this->_getController( [ 'A', ] );$blocks->renderBlock( $controller );$this->assertEquals( ['A',get_class( $controller ),], $blocks->getUsedControllerClasses() );}public function testRenderBlockIgnoreDuplicateControllerWhenAddsToUsedList() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, [] );$controllerA   = $this->_getController();$blocks->renderBlock( $controllerA );$blocks->renderBlock( $controllerA );$this->assertEquals( [get_class( $controllerA ),], $blocks->getUsedControllerClasses() );}public function testRenderBlockIgnoreDuplicateControllerDependenciesWhenAddsToUsedList() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, [] );$controllerA   = $this->_getController( [ 'A', ] );$controllerB   = $this->_getController( [ 'A', ] );$blocks->renderBlock( $controllerA );$blocks->renderBlock( $controllerB );$this->assertEquals( ['A',get_class( $controllerA ),// $controllerB has the same class], $blocks->getUsedControllerClasses() );}////public function testGetUsedResourcesWhenBlockWithResources() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, ['Block' => ['Block_C.php' => $this->_getControllerClassFile( "{$namespace}\Block", 'Block_C' ),'block.css'   => 'just css code',],], ["{$namespace}\Block\Block_C",] );$this->assertEquals( 'just css code',$blocks->getUsedResources( '.css', false ) );}public function testGetUsedResourcesWhenBlockWithoutResources() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, ['Block' => ['Block_C.php' => $this->_getControllerClassFile( "{$namespace}\Block", 'Block_C' ),],], ["{$namespace}\Block\Block_C",] );$this->assertEquals( '',$blocks->getUsedResources( '.css', false ) );}public function testGetUsedResourcesWhenSeveralBlocks() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, ['BlockA' => ['BlockA_C.php' => $this->_getControllerClassFile( "{$namespace}\BlockA", 'BlockA_C' ),'block-a.css'  => 'css code for a',],'BlockB' => ['BlockB_C.php' => $this->_getControllerClassFile( "{$namespace}\BlockB", 'BlockB_C' ),'block-b.css'  => 'css code for b',],], ["{$namespace}\BlockA\BlockA_C","{$namespace}\BlockB\BlockB_C",] );$this->assertEquals( 'css code for acss code for b',$blocks->getUsedResources( '.css', false ) );}public function testGetUsedResourcesWithIncludedSource() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, ['SimpleBlock' => ['SimpleBlock_C.php' => $this->_getControllerClassFile( "{$namespace}\SimpleBlock", 'SimpleBlock_C' ),'simple-block.css'  => 'css code',],], ["{$namespace}\SimpleBlock\SimpleBlock_C",] );$this->assertEquals( "\n/* simple-block */\ncss code",$blocks->getUsedResources( '.css', true ) );}}

Вот и все, теперь осталось объединить эти части и наш мини-фреймворк готов. Момент публикации composer пакета я опущу, т.к. это довольно простая задача и если вам будет интересно то вы без труда найдете информацию по этому поводу.

Демонстрационный пример

Ниже приведу демонстрационный пример использования пакета, с одним чистым css для наглядности, если кому-то интересен пример с scss/webpack смотрите ссылки в конце статьи.

Создаем блоки для теста, BlockA и BlockC будут независимыми блоками, BlockB будет содержкать BlockC.

BlockA

BlockA.php
<?phpnamespace LightSource\FrontBlocksExample\BlockA;use LightSource\FrontBlocksFramework\MODEL;class BlockA extends MODEL {protected string $_name;public function load() {parent::_load();$this->_name = 'I\'m BlockA';}}
BlockA_C.php

/sp

<?phpnamespace LightSource\FrontBlocksExample\BlockA;use LightSource\FrontBlocksFramework\Blocks;use LightSource\FrontBlocksFramework\CONTROLLER;class BlockA_C extends CONTROLLER {public function getModel(): ?BlockA {/** @noinspection PhpIncompatibleReturnTypeInspection */return parent::getModel();}}
block-a.twig

/

<div class="block-a">    {{ name }}</div>
block-a.css

Bl

.block-a {    color: green;    border:1px solid green;    padding: 10px;}

BlockB

BlockB.php
<?phpnamespace LightSource\FrontBlocksExample\BlockB;use LightSource\FrontBlocksExample\BlockC\BlockC;use LightSource\FrontBlocksFramework\MODEL;class BlockB extends MODEL {protected string $_name;protected BlockC $_blockC;public function __construct() {parent::__construct();$this->_blockC = new BlockC();}public function load() {parent::_load();$this->_name = 'I\'m BlockB, I contain another block';$this->_blockC->load();}}
BlockB_C.php
<?phpnamespace LightSource\FrontBlocksExample\BlockB;use LightSource\FrontBlocksExample\BlockC\BlockC_C;use LightSource\FrontBlocksFramework\CONTROLLER;class BlockB_C extends CONTROLLER {protected BlockC_C $_blockC;public function getModel(): ?BlockB {/** @noinspection PhpIncompatibleReturnTypeInspection */return parent::getModel();}}
block-b.twig
<div class="block-b">    <p class="block-b__name">{{ name }}</p>    {{ _include(blockC) }}</div>
block-b.css

Blo

.block-b {    color: orange;    border: 1px solid orange;    padding: 10px;}.block-b__name {    margin: 0 0 10px;    line-height: 1.5;}

BlocksC

BlockC.php
<?phpnamespace LightSource\FrontBlocksExample\BlockC;use LightSource\FrontBlocksFramework\MODEL;class BlockC extends MODEL {protected string $_name;public function load() {parent::_load();$this->_name = 'I\'m BlockC';}}
BlockC_C.php

/

<?phpnamespace LightSource\FrontBlocksExample\BlockC;use LightSource\FrontBlocksFramework\CONTROLLER;class BlockC_C extends CONTROLLER {public function getModel(): ?BlockC {/** @noinspection PhpIncompatibleReturnTypeInspection */return parent::getModel();}}

Подключаем наш пакет и рендерим блоки

block-c.twig
<div class="block-c">    {{ name }}</div>
block-c.css
.block-c {    color: black;    border: 1px solid black;    padding: 10px;}

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

example.php
<?phpuse LightSource\FrontBlocksExample\{BlockA\BlockA_C,BlockB\BlockB_C,};use LightSource\FrontBlocksFramework\{Blocks,Settings};require_once __DIR__ . '/vendors/vendor/autoload.php';//// settings$settings = new Settings();$settings->setBlocksDirNamespace( 'LightSource\FrontBlocksExample' );$settings->setBlocksDirPath( __DIR__ . '/Blocks' );$settings->setErrorCallback( function ( array $errors ) {// todo log or any other actionsecho '<pre>' . print_r( $errors, true ) . '</pre>';});$blocks = new Blocks( $settings );//// usage$blockA_Controller = new BlockA_C();$blockA_Controller->getModel()->load();$blockB_Controller = new BlockB_C();$blockB_Controller->getModel()->load();$content = $blocks->renderBlock( $blockA_Controller );$content .= $blocks->renderBlock( $blockB_Controller );$css     = $blocks->getUsedResources( '.css', true );//// html?><html><head>    <title>Example</title>    <style>        <?= $css ?>    </style>    <style>        .block-b {            margin-top: 10px;        }    </style></head><body><?= $content ?></body></html>

в результате вывод будет примерно таким

example.png

Послесловие

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

Вот и все, спасибо за внимание.

Ссылки:

репозиторий с мини фреймворком

репозиторий с демонстрационным примером

репозиторий с примером использования scss и js в блоках (webpack сборщик)

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

Подробнее..

От студента до учителя как разобраться в веб-разработке, если это не твой профиль

21.04.2021 20:21:36 | Автор: admin

Хоть кому-то и может показаться, что веб-разработчик это суровый технарь (айтишник же!), вход в эту профессию не сложнее, чем вPython. В неё часто переходят бывшие педагоги, юристы, бухгалтеры и другие гуманитарии. О том, с чего начать обучение, какие ошибки допускают новички, как освоиться в профессии и стоит ли самостоятельно учиться, рассказывает преподаватель веб-разработки в GeekBrains Алексей Кадочников.

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

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

Никогда не откладывайте обучение на завтра: заведите будильник, например, на восемь вечера. Большинство людей к этому времени успевают освободиться от работы, попасть домой и даже поужинать. Зазвенит садитесь и занимайтесь до девяти-десяти. И так каждый день. Пропустили? Добавьте пропущенный час к занятию завтра.

Конечно же, 8 вечера это не панацея. Главное оптимизировать график, чтобы эффективно совмещать обучение и работу. Когда я переучивался на разработчика (об этом расскажу дальше), ставил будильник на шесть утра, занимался, завтракал и ехал на работу.

Кто переучивается на разработчика

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

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

Мне самому пришлось сменить специальность. Восемь лет назад, когда я окончил университет, оказалось, что на рынке по специальности Вычислительные машины, комплексы, системы и сети всего 8 вакансий. Для четырех из них мне не хватало опыта, а по ещё четырём мне не перезвонили. В результате устроился инженером на завод и через несколько месяцев работы понял, что это не то, чему я хочу посвятить жизнь. Тогда яс нуляпрошелкурсы веб-разработкии нашёл работу по их окончанию. СейчасяFront-end developerвMail.ru GroupипреподаювGeekBrains.

Еще один пример мой студент Павел Литвин. Он не доучился в ВУЗе на безопасника, работал менеджером по продажам, потом в SEO, в конце концов выучился фронтенд-разработке и стал зарабатывать в4 раза больше, чем до курсов.И таких историй множество.

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

Чтобы понять, подойдет ли тебе специальность, надо просто попробовать. В онлайн-школах, где учат разработке, есть бесплатныеинтенсивына несколько часов. За это время студентам предлагается сделать небольшой проект: так можно понять, подходит ли им эта сфера. Если вы попробовали пару таких интенсивов и ничего не получается или не нравится процесс, возможно, это не ваше призвание и стоит продолжить поиски. Профессия веб-разработчика подходит многим, но это не универсальный рецепт. Наверняка вы найдете для себя что-то другое.

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

Самостоятельное обучение

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

В момент, когда я пошёл на курсы, мой знакомый решил учиться самостоятельно три года что-то читал, смотрел в интернете. В итоге за это время он вышел на зарплату в 50 000 рублей. Пойди он на курсы, достиг бы этого результата уже через полгода-год. Думая, что экономит, он потерял деньги.

Еще одна частая проблема самостоятельногообучения веб-разработке с нуляосвоение устаревших технологий. Мне приходилось переучивать студентов, выучивших неактуальную информацию. Есть вещи, которые уже не применяются, оптимизировались и их нужно удалить из памяти или перенастроить. Например, раньше в верстке для перемещения элемента использовалась командаfloat left, но это довольно громоздкое и сложное решение. Затем вместо него начали использоватьdisplay: flex. Теперь и этот метод успел устареть и теперь актуаленdisplay: grid. Внешний вид от всех этих способов будет одинаковым, но последнее решение изящнее и быстрее в реализации.

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

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

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

Чтобы сохранить мотивацию, когда я делаю что-то новое, использую такой приём: записываю видео, в котором озвучиваю цели и мотивы текущих задач. Если через условные три месяца наступает момент, когда опускаются руки, просматриваю его. Если идея с видео вам не нравится, попробуйте написать вдохновляющее письмо.

Высшее образование и курсы

Я устраивался на работу много раз, переезжал между городами и даже странами СНГ. За всё время про диплом о высшем образовании у меня спросили всего дважды. Поэтому я бы не сказал, что это дает какое-то преимущество перед другими соискателями. На собеседованиях проверяют технические знания. Если их нет, никакой диплом не спасет.

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

Чтобы учиться на курсах эффективно, нужно смотреть вебинары, выполнять практические задания и (как минимум) читать первыессылкив Google или Яндекс по теме урока: это помогает немного расширить понимание пройденного.

Читать учебники перед началом обучения не стоит: они очень быстро устаревают. С момента написания книги до её попадания на полки проходит не один месяц. Её нужно отредактировать, сверстать, напечатать, выпустить на рынок. Лучше перед учебой (или одновременно с ней) читать официальную документацию выбранного языка или технологии. Например, вотсайт, на котором собран максимум информации для старта в JavaScript.

У нас на курсах есть задания для командной работы с несколькими разработчиками. Мы стараемся сделать программу максимально адаптивной. Каждый, кто прошел курс, может написать о нём и своём преподавателеотзыв. То, что присылают выпускники, не модерируется и всегда рассматривается отделом качества. Если его специалисты видят проблемы по какому-то предмету, они отправляют заявку, чтобы курс обновили.

Вариантов обучения много можно выучиться на джуна, а можно сразу на миддла. Можно быть фронтендером, бекэндером или освоить обе технологии, став фулстек-специалистом. Разница в освоенных технологиях:

  • Junior-frontendдолжензнатьhtml + css + js + react.

  • Junior-fullstack: html + css + js + php +базыданных.

  • Middle frontendразработчик: html + css + js + react + vue + node.js +команднаяразработка.

  • Middle-fullstack: html + css + js + react + php + laravel +базыданных+команднаяразработка.

Обучение веб-разработкена наших курсах длится от 5 месяцев. За это время можно получить базу junior-фронтенда. Чтобы изучить технологии, нужные для миддл-фронтенда, понадобится год. Освоение навыков миддла и для фронтенда, и для бэкенда требует 1,5 года. А дальше нужно идти в бой и набираться опыта, чтобы подтвердить этот статус в реальной работе.

Я преподаю фронтенд-разработку. Особенность курса в постоянной демонстрации практической составляющей, в каждом уроке показываю, как применять изученное. Мы даём прикладные знания, а все домашние задания связаны друг с другом. С 1 по 8 урок студенты постепенно разрабатывают готовый проект, и в итоге они успевают сделать пятистраничный проект интернет-магазина. Например, вот такого:

Как устроиться на работу и что от нее ожидать

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

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

Джуну на первой работе можно рассчитывать на 4060 тысяч, миддл зарабатывает от 100150 тысяч. По сути, зарплата может быть и 200250 тысяч, но чтобы знаний хватило на зарплату миддла, нужно прилежно учиться не меньше полугода-года.

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

Самая частая ошибка на собеседованиях говорить лишнее. В разработке на болтовне не выедешь: отвечать нужно уверенно и по сути. Если где-то не уверен или не знаешь, стоит показать логику действий в такой ситуации, но ничего не выдумывать и не заговаривать интервьюера. Веб-разработка это работа в команде. Поэтому важно показать, что ты умеешь работать с другими и есть релевантный опыт. У нас на курсах есть задания для командной работы с несколькими разработчиками.

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

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

Нужно помнить, что на работе (зачастую) никто никого ничему учить не будет. Разработчику дают конкретную задачу и просят её сделать. Я думаю, что если есть желание быстро развиваться в профессии, лучше выбирать место, где много сотрудников и есть старший наставник. Работая в одиночку развиться очень сложно.

Подробнее..

Нестандартные шрифты как подключить и оптимизировать

15.04.2021 10:21:55 | Автор: admin

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

Сразу стоит отметить, что самый простой вариант вообще не подключать сторонние шрифты и пользоваться стандартными, которые предустановлены в большинстве операционных систем. Это хорошо знакомые Arial, Times New Roman и так далее эти шрифты называются веб-безопасными, достаточно просто указать название одного из таких шрифтов в коде, и всё будет работать.

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

Выбираем формат шрифта

Все слышали про TTF и OTF. Но это форматы, которые предоставляются с минимальным сжатием или совсем без него. Их стоит использовать только в том случае, если нужна поддержка очень старых браузеров.

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

Подключение шрифтов с помощью Google Fonts

Есть простой и удобный способ подключить нестандартный шрифт использовать Google Fonts. Это бесплатный сервис, с помощью которого можно подключать шрифты, но не хранить их на своём сервере. Чтобы им воспользоваться, необходимо выбрать шрифт и добавить одно или несколько нужных начертаний, а затем вставить в <head> ссылку, которую сгенерирует Google Fonts во вкладке Embed.

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

body {font-family: "Roboto", "Arial", sans-serif;}

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

Плюсы этого способа простота использования и всегда актуальные версии шрифтов, Google Fonts их часто обновляет. Но у него есть и минус запросы к сторонним серверам могут негативно повлиять на скорость загрузки страницы. Если выбираете этот способ, стоит обратить внимание на оптимизацию.

Подключение шрифтов с помощью правила @font-face

Есть альтернативный способ, при котором файлы со шрифтами хранятся вместе с остальными ресурсами сайта. Для них принято заводить отдельную директорию в корне проекта например, fonts. В неё следует поместить файлы для каждого начертания в нужных форматах в большинстве случаев, если не требуется поддержка старых браузеров, подойдут .woff и .woff2, о которых мы говорили ранее. Шрифты можно скачать на различных ресурсах. При этом всегда нужно обращать внимание на лицензию некоторые шрифты могут быть недоступны для коммерческого использования.

После того, как шрифты добавлены в проект, их нужно подключить в CSS-файле. Для этого используется правило @font-face. В самом базовом варианте оно будет включать:

  1. Название шрифта, которое затем нужно использовать, чтобы задать элементам подключённый шрифт.

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

  3. Начертания: жирное, курсивное и так далее. Для каждого начертания нужно отдельное правило @font-face.

Базовый вариант правила:

@font-face {font-family: "Roboto";font-style: normal;font-weight: 400;/* Браузер сначала попробует найти шрифт локально */src: local("Roboto"),/* Если не получилось, загрузит woff2 */url("/fonts/roboto.woff2") format("woff2"),/* Если браузер не поддерживает woff2, загрузит woff */url("/fonts/roboto.woff") format("woff");}/* Теперь можно использовать шрифт */body {font-family: "Roboto", "Arial", sans-serif;}

Для улучшения производительности правило @font-face лучше всего прописывать в самом начале CSS-файла. Так браузер сможет раньше начать обработку шрифта.

Оптимизация

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

FOIT, FOUT и FOFT

Пока шрифт загружается, при рендеринге можно наблюдать разное поведение текста.

FOIT (Flash of Invisible Text) с англ. мелькание невидимого текста. При таком поведении, пока шрифт не загрузится, текст не отображается и появляется только после загрузки шрифта. Значительная проблема во время загрузки нет доступа к текстовому контенту.

FOUT (Flash of Unstyled Text) с англ. мелькание неоформленного текста. Во время загрузки используется шрифт, заданный по умолчанию (системный, например), а после загрузки страница перерисовывается с использованием загрузившегося шрифта. Эта перерисовка довольно заметна и может быть нежелательна.

FOFT (Flash of Faux Text) с англ. мелькание синтезированного текста. Это поведение можно наблюдать в промежутке, когда основное начертание уже загрузилось, а дополнительные (жирное, курсивное и так далее) нет. Браузер имитирует нужное начертание до загрузки настоящей версии. В этом случае страница может перерисовываться несколько раз по мере загрузки начертаний.

В разных браузерах логика рендеринга текста во время загрузки шрифта отличается. Например, Chrome и Firefox в течение трёх секунд не отрисовывают ничего, затем используют веб-безопасный шрифт, а после окончания загрузки текст перерисовывается. IE поступает похоже, но при этом не ждёт три секунды. Подобное поведение в разных браузерах можно унифицировать, используя свойство fontdisplay.

Свойство font-display

У свойства есть несколько значений, которые определяют поведение текста во время загрузки шрифта:

  • auto поведение по умолчанию, зависит от браузера.

  • block текст не отображается в течение короткого периода (3 секунды), затем отрисовывается запасной шрифт, если основной ещё не загрузился. Как только загрузка завершается, текст перерисовывается снова.

  • swap сразу же отрисовывается запасной шрифт, после загрузки шрифта повторный рендеринг.

  • fallback в течение очень короткого периода (100 миллисекунд) не отображается ничего, затем браузер использует запасной шрифт и ждёт 3 секунды если шрифт всё ещё не загрузился, остаётся запасной шрифт. Далее не важно, загрузился шрифт или нет, замена не произойдёт. Если шрифт загрузится, то он применится только при обновлении страницы.

  • optional текст не отображается в течение 100 миллисекунд, а затем отрисовывается запасным шрифтом. Даже если шрифт загрузится после этого, замена произойдёт только при обновлении страницы.

Оптимальное значение swap, его можно использовать в большинстве случаев, оно удобно для пользователей. При подключении шрифта с помощью Google Fonts это значение установлено по умолчанию. Если же есть необходимость избежать мелькания текста (например, для вдумчивого чтения), подойдёт optional.

Предзагрузка шрифтов

Ещё один способ оптимизации предварительная загрузка шрифтов. С её помощью можно изменить обычную приоритизацию загрузки ресурсов, тем самым сказав браузеру, что важно загрузить шрифт в первую очередь.

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

Для того, чтобы предзагрузка сработала, нужно поместить в <head> ссылку на шрифт и задать атрибуту rel значение preload:

<link rel="preload" href="http://personeltest.ru/aways/habr.com/fonts/roboto.woff2" as="font">

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

Уменьшение количества глифов шрифта

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

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

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

Google Fonts по умолчанию использует сабсеты. Это можно увидеть, открыв CSS-файл, который подключается в <head> при использовании сервиса. Для каждого языка есть отдельный сабсет. Пример для латиницы и кириллицы:

/* latin */@font-face {font-family: "Roboto";font-style: normal;font-weight: 400;font-display: swap;src: local("Roboto"),local("Roboto-Regular"),url(http://personeltest.ru/aways/fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu4mxK.woff2)format("woff2");unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,U+2212, U+2215, U+FEFF, U+FFFD;}/* cyrillic */@font-face {font-family: "Roboto";font-style: normal;font-weight: 400;font-display: swap;src: local("Roboto"),local("Roboto-Regular"),url(http://personeltest.ru/aways/fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu5mxKOzY.woff2)format("woff2");unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}

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

Полезности

Подробнее..

Перевод CSS работа с текстом на изображениях

15.04.2021 14:13:15 | Автор: admin

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


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

Вступление

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

Слева без оверлея, справа с оверлеем.Слева без оверлея, справа с оверлеем.

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

Обзор возможных решений

Давайте посмотрим на возможные решения.

Есть решения, требующие большего внимания, это градиентные решения. Почему? Потому что добавить слой градиента это очень просто, но текст не будет удобным.

Решения

Наложение градиента

Вообще говоря, наложение градиента это наиболее распространённое решение, позволяющее сделать текст на изображении более чётким. Учитывая это, я остановлюсь на нём немного подробнее.

При реализации наложения градиента у нас есть два варианта:

  • Использовать отдельный элемент для градиента (псевдоэлемент или пустой <div>)

  • Применить градиент как фоновое изображение.

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

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

.card__content {  position: absolute;  /* other styles (left, top, right, and padding) */  background: linear-gradient(to top, rgba(0, 0, 0, 0.85), transparent);}

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

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

Причина в том, что градиент должен покрывать больше пространства по вертикали, поэтому должен быть выше. Если градиент равен размеру контента, он будет работать не во всех случаях. Чтобы решить эту проблему, мы можем использовать min-height, как показано ниже:

min-height к элементу .card__content.

Flexbox для перемещения содержимого вниз.

.card__content {  position: absolute;  /* other styles (left, top, right, and padding) */  display: flex;  flex-direction: column;  justify-content: flex-end;  background: linear-gradient(to top, rgba(0, 0, 0, 0.85), transparent);}

Другое решение большой padding-top, с ним не нужны min-height и flexbox.

.card__content {  position: absolute;  padding-top: 60px;  background: linear-gradient(to top, rgba(0, 0, 0, 0.85), transparent);}

Обратите внимание на разницу между левой и правой карточками. Градиент больше по высоте.

Выглядит хорошо. Можем ли мы сделать лучше? Определённо да!

Смягчение градиента

Присмотревшись, вы заметите, где заканчивается градиент, то есть у него резкая граница.

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

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

К счастью, г-н Андреас Ларсен создал удобные плагины PostCSS и Sketch, которые помогают преобразовывать резкий градиент в более мягкий.

Вот градиент CSS для примера выше:

.card__content {  background-image: linear-gradient(    0deg,    hsla(0, 0%, 35.29%, 0) 0%,    hsla(0, 0%, 34.53%, 0.034375) 16.36%,    hsla(0, 0%, 32.42%, 0.125) 33.34%,    hsla(0, 0%, 29.18%, 0.253125) 50.1%,    hsla(0, 0%, 24.96%, 0.4) 65.75%,    hsla(0, 0%, 19.85%, 0.546875) 79.43%,    hsla(0, 0%, 13.95%, 0.675) 90.28%,    hsla(0, 0%, 7.32%, 0.765625) 97.43%,    hsla(0, 0%, 0%, 0.8) 100%  );}

Сравните карточки со смягчением градиента и без него.

Горизонтальные градиенты

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

Вот CSS градиента для раздела выше. Чтобы добиться смягчения градиента, я использовал упомянутый ранее инструмент.

background: linear-gradient(  to right,  hsl(0, 0%, 0%) 0%,  hsla(0, 0%, 0%, 0.964) 7.4%,  hsla(0, 0%, 0%, 0.918) 15.3%,  hsla(0, 0%, 0%, 0.862) 23.4%,  hsla(0, 0%, 0%, 0.799) 31.6%,  hsla(0, 0%, 0%, 0.73) 39.9%,  hsla(0, 0%, 0%, 0.655) 48.2%,  hsla(0, 0%, 0%, 0.577) 56.2%,  hsla(0, 0%, 0%, 0.497) 64%,  hsla(0, 0%, 0%, 0.417) 71.3%,  hsla(0, 0%, 0%, 0.337) 78.1%,  hsla(0, 0%, 0%, 0.259) 84.2%,  hsla(0, 0%, 0%, 0.186) 89.6%,  hsla(0, 0%, 0%, 0.117) 94.1%,  hsla(0, 0%, 0%, 0.054) 97.6%,  hsla(0, 0%, 0%, 0) 100%);

Смешивание сплошного цвета и градиента

Я узнал об этом приёме на сайте Netflix. На домашней странице для незарегистрированного пользователя есть заголовок с большим фоновым изображением.

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

<div class="hero">  <img src="cover.jpg" alt="" />  <div class="hero__content">    <h2>Unlimited movies, TV shows, and more.</h2>    <p>Watch anywhere. Cancel anytime.</p>  </div></div>
.hero:after {  content: "";  position: absolute;  left: 0;  top: 0;  width: 100%;  height: 100%;  background-color: rgba(0, 0, 0, 0.4);  background-image: linear-gradient(    to top,    rgba(0, 0, 0, 0.8),    rgba(0, 0, 0, 0) 60%,    rgba(0, 0, 0, 0.8) 100%  );}

Вот наглядное объяснение того, как работает этот паттерн.

Наложение градиента и тень текста

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

.whatever-text {  text-shadow: 0 2px 3px rgba(0, 0, 0, 0.3);}

Наложение градиента, тень текста и непрозрачность

Это закономерность, которую я заметил в проигрывателе видео на Facebook. Мне понравилось, что они использовали несколько техник, чтобы сделать текст и другие элементы пользовательского интерфейса чёткими. При работе с проигрывателем очень важно гарантировать, что элементы поверх него заметны.

.player__icon {  opacity: 0.9;}.player__time {  color: #fff;  text-shadow: 0 0 5px #fff;}

Что в этом нового? Значки и проигрыватель имеют непрозрачность в 90 %. Это помогает им смешаться с фоном под ними. Создаётся ощущение, что элементы управления вмешаны в изображение.

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

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

Youtube делает то же самое со своими видео.

Вот что мне понравилось в подходе Youtube:

  • Тёмная рамка для каждого значка, чтобы он лучше выделялся.

  • Чёрная тень вместо белой для времени видео.

Радиальный градиент

Интересное решение, о котором я узнал от Netflix, радиальный градиент. Вот как он работает:

  1. Установите основной цвет заднего фона.

  2. Поместите изображение в верхний правый угол с шириной 75 %.

  3. Наложение соответствует размеру и положению изображения.

.hero {  background-color: #000;  min-height: 300px;}.hero__image {  position: absolute;  right: 0;  top: 0;  width: 75%;  height: 100%;  object-fit: cover;}.hero:after {  content: "";  position: absolute;  right: 0;  top: 0;  width: 75%;  height: 100%;  background: radial-gradient(    ellipse 100% 100% at right center,    transparent 80%,    #000  );}

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

Выбор удобного пользователю цвета наложения

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

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

Тестирование

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

В приведённом выше примере я выбрал сплошной цвет под заголовком, а коэффициент контрастности составляет 4,74, такой коэффициент считается хорошим.

Работа с Firefox DevTools

Спасибо Гийсу Вейфейкену: он рассказал мне, что Firefox может тестировать цветовой контраст на градиентах. Это отличная функция.

Узнайте, как прокачаться в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Перевод Какие CSS-генераторы можно использовать в 2021 году

16.04.2021 12:14:51 | Автор: admin
image

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

Схема работы таких генераторов проста: достаточно ввести через простой визуальный интерфейс нужные вам параметры и на выходе получить готовые CSS-стили. Далее полученный код нужно скопировать и добавить в свой проект.

Известный full-stack разработчик Марко Денич предлагает свою подборку лучших CSS-генераторов. Возможно, о каких-то из них вы не знали, и, познакомившись с ними сейчас, начнёте использовать в работе.

1. Neumorphism/Soft UI generator


Neumorphism позволяет создавать градиенты и тени. Кроме того, с ним легко подобрать и сгенерировать цвета для вашего сайта.

Подробнее


2. Smooth Shadow generator


Это генератор теней, позволяющий через удобный UI менять параметры свойства box-shadow. В CSS3 свойство box-shadow отвечает за эффекты тени, которые возможны для большинства элементов веб-страницы. Инструмент, в частности, позволяет настраивать такие параметры, как вертикальное смещение, размытие, растяжение и прозрачность.

Подробнее


3. Fancy Border Radius Generator


Генератор позволяет рисовать сложные фигуры с закруглёнными углами. На выходе мы получаем набор значений свойства border-radius.

Подробнее


4. Easing Gradients


Позволяет создавать нелинейные градиенты и использовать модифицированные цветовые пространства.

Подробнее


5. Data Viz Color Palette Generator


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

Подробнее


6. CSS Grid Generator


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

Подробнее


7. CSS Accordion Slider Generator


Бесплатный онлайн-сервис, позволяющий настроить через UI и сгенерировать горизонтальные и вертикальные аккордеон-слайдеры на HTML и CSS, без использования javaScript.

Подробнее


8. CSS clip-path maker


Простой онлайн-инструмент для обрезки картинок по заданному трафарету.

Подробнее


9. Get Waves


Генератор создает svg-вектор с заливкой волнообразной формы. Форму волны можно выбирать из трёх вариантов синусоидальная, прямоугольная и пилообразная.

Также можно указывать размер волны и направление. В инструмент встроен рандомайзер, который выдаёт волны со случайными размерами, но с сохранением выбранной формы.

Подробнее


Согласны ли вы с выбором автора? Какие CSS-генераторы нравятся вам?




На правах рекламы


Эпичные серверы для размещения сайтов и не только! Быстрые VDS на базе новейших процессоров AMD EPYC и NVMe хранилища для размещения проектов любой сложности, от корпоративных сетей и игровых проектов до лендингов и VPN.

Подробнее..

Полное руководство по CSS Flex опыт использования

20.04.2021 00:18:08 | Автор: admin

Как и CSS Grid, Flex Box довольно сложен, потому что состоит из двух составляющих: контейнера и элементов внутри него.

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

Это все, на что способен Flex.Это все, на что способен Flex.

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

CSS Flex или Flex Box

Flex это набор правил для автоматического растягивания нескольких столбцов и строк внутри родительского контейнера.

display:flex

В отличие от многих других свойств CSS, в Flex есть основной контейнер и вложенные в него элементы. Некоторые свойства CSS-Flex относятся только к контейнеру. А другие можно применить только к элементам внутри него.

Вы можете думать о flex-элементе как о родительском контейнере со свойством display: flex. Элемент, помещенный в контейнер, называется item. Каждый контейнер имеет границы начала(flex-start) и конца гибкости(flex-end), как показано на этой диаграмме.

Горизонтальная (main) и вертикальная (cross) оси

Хотя список элементов представлен линейно, необходимо обращать внимание на строки и столбцы. По этой причине Flex включает в себя координатные оси. Горизонтальная ось называется main-axis, а вертикальная cross-axis.

Чтобы управлять шириной содержимого контейнера и промежутками между элементами, которые растягиваются вдоль main-axis, необходимо использовать Justify-content. Для управления вертикальными изменениями элементов необходимо использовать align-items.

Если у вас есть 3 столбца и 6 элементов, Flex автоматически создаст вторую строку для размещения оставшихся элементов. Если у вас в списке более 6 элементов, будут созданы дополнительные строки.

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

Вы можете определять количество столбцов.Вы можете определять количество столбцов.

Распределение строк и столбцов внутри родительского элемента определяется свойствами CSS Flex flex-direction, flex-wrap и некоторыми другими, которые будут продемонстрированы дальше.

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

Direction

Можно задать направление движения элементов (по умолчанию слева направо).

flex-direction: row-reverse изменяет направление движения списка элементов. По умолчанию стоит значение row, что означает движение слева направо.

Wrap

flex-wrap: wrap определяет перенос элементов на другую строку, когда в родительском контейнере заканчивается место.

Flow

flex-flow включает в себя flex-direction и flex-wrap, что позволяет определять их с помощью одного свойства.

Примеры:

  • flex-flow: row wrap определяет значения flex-direction как row и flex-wrap как wrap.

  • flex-flow:row wrap-reverse (перенос элементов вверх)

  • flex-flow:row wrap (стандартный перенос элементов); justify-content: space-between (пробел между элементами);

  • flex-flow: row-reverse wrap (направление движения справа налево со стандартным переносом сверху вниз)

  • flex-flow: row-reverse wrap-reverse (направление движения справа налево и обратный перенос элементов);

  • flex-flow: row wrap; justify-content: space-between; (стандартный перенос и направление; расстояние между элементами)

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

Когда мы меняем flex-direction на column, свойство flex-flow ведет себя точно так же, как и в предыдущих примерах. За исключением wrap-reverse, когда элементы переносятся снизу вверх.

justify-content

Я получил много просьб прояснить приведенный выше пример. Для этого я создал анимацию:

Анимированные возможности justify-content. Анимированные возможности justify-content.

Надеюсь, эта CSS-анимация поможет лучше понять работу justify content.

Свойства следующие:flex-direction:row; justify-content: flex-start | flex-end | center | space-between | space-around | stretch | space-evenly. В этом примере мы используем только 3 элемента в строке.

Нет никаких ограничений на количество элементов, которые можно использовать в Flex. Эти диаграммы демонстрируют только поведение элементов, когда одно из перечисленных значений применяется к свойству justify-content .

То же свойство justify-content используется для выравнивания элементов, когда flex-direction: column. То же свойство justify-content используется для выравнивания элементов, когда flex-direction: column.

Packing Flex Lines (согласно спецификации Flex)

Я не уверен, реализовано ли это в каком-либо браузере, но когда-то это было частью спецификации CSS-flex и, вероятно, стоит упомянуть об этом для полноты картины.

В спецификации Flex это называется Packing Flex Lines. По сути, это работает так же, как в примерах выше. Однако стоит обратить внимание, что интервал расположен между целыми группами элементов. Это полезно, когда вы хотите создать зазоры между группами.

Packing Flex Lines, но теперь с flex-direction: column Packing Flex Lines, но теперь с flex-direction: column

align-items

align-items контролирует выравнивание элементов по горизонтали относительно родительского контейнера.

flex-basis

flex-basis работает аналогично другому свойству CSS: min-width. Оно увеличивает размер элемента в зависимости от содержимого. Если свойство не задействуется, то используется значение по умолчанию.

flex-grow

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

flex-shrink

flex-shrink это противоположность flex-grow. В примере значение flex-shrink равно 7. При таком значении размер элемента равен 1/7 размера окружающих его элементов (размер которых автоматически скорректирован).

При работе с отдельными элементами можно использовать только одно свойство из трёх: flex-grow , flex-shrink или flex-base.

order

Используя свойство order , можно изменить естественный порядок элементов.

justify-items

И последнее, что нужно для тех, кто хочет использовать CSS Grid вместе с Flex Box... justify-items в CSS Grid похожи на justify-content в Flex. Свойства, описанные на приведенной выше диаграмме, не будут работать в Flex, но в значительной степени эквивалентны сетке для выравнивания содержимого ячеек.

Опыт веб-студии ITSOFT

Мы в веб-студии ITSOFT перешли на Flex в 2010 году. В основе всех наших новых проектов лежит именно эта технология. Наш опыт показал, что она позволяет создавать адаптивные и кроссбраузерные сайты с действительно красивым дизайном.

При использовании Flex нужно учитывать, что:

  • Flex-элементы по умолчанию ужимаются под свое содержимое; Это может пригодиться, когда есть блоки, размер которых изначально неизвестен и зависит от количества контента.

    Примеры: пункты меню в https://ketoplus.ru/

  • Внешние отступы flex-элементов не схлопываются и не выпадают, в отличие от блочной модели.

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

    Пример: блоки в http://velpharm.ru/about/veltrade/

  • Flex-элементы умеют перераспределять свободное пространство вокруг себя, таким образом меняя свои размеры;

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

    Пример: главная страница http://oflomil.ru/

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

    Бывают макеты страниц, в которых порядок следования элементов отличается на мобильной и десктопной версиях. Flex справится с задачей.

    Пример: разный порядок следования логотипа и навигации в шапке в мобильной и десктопной версиях (http://elmucin.ru/).

  • Flex-элементу можно задать не только горизонтальные автоматические отступы, но и вертикальные; к тому же есть специальные свойства, с помощью которых очень просто выравнивать элементы внутри флекс-контейнера по горизонтали или вертикали.

    Пример: кнопка всегда находится в нижней части карточки статьи (https://rabiet.ru/experts/), блок продукция (https://stomatofit.ru/#products).

  • Flex-элементы могут переноситься на следующую строку, если указано соответствующее свойство.

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

    Пример: каталог Велфарм (http://velpharm.ru/catalog/), блок Где купить (http://valosemid/#partners, https://gopantomid.ru/)

Стоит отметить, что Flexbox поддерживается в Internet Explorer 10-11, хоть и с некоторыми оговорками.

Некоторые интересные фишки Flex мы реализовали на сайте oflomil.ru. На разводящей странице есть 3 столбца на всю высоту видимой области экрана. На мобильном они трансформируются в 3 строки, каждая из которых равномерно занимает место в видимой части экрана. На продуктовых страницах без единого скрипта реализована бегущая строка. Текстовые элементы расположены в ряд благодаря inline-flex. Все свободное пространство в ряду распределено между этими элементами равномерно. Наш собственный сайт также свёрстан с использованием Flex.

Реализация бегущей строки с Flex Реализация бегущей строки с Flex

Из недостатков можно отметить то, что Flex не хватает при верстке писем. Не во всех почтовых клиентах он работает корректно.

В скором времени большую распространённость получит технология Grid. Однако Grid не замена Flexbox. Флексы в связке с гридами помогут решать задачи, которые раньше были сложными или вовсе невозможными. К примеру, Masonry-раскладка одна из тех вещей, которую нельзя полноценно реализовать на данный момент. После появления возможностей спецификации Grid Layout 3 уровня в браузерах, реализация такой раскладки станет возможной и к тому же простой.

Источники: оригинал руководства с картинками


Дата-центр ITSOFT размещение и аренда серверов и стоек в двух дата-центрах в Москве. За последние годы UPTIME 100%. Размещение GPU-ферм и ASIC-майнеров, аренда GPU-серверов, лицензии связи, SSL-сертификаты, администрирование серверов и поддержка сайтов.

Подробнее..

Перевод Адаптивный дизайн как антипаттерн

27.04.2021 16:10:26 | Автор: admin


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

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

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

Пространственный газлайтинг


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

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

Именно такова проблема, которая возникла у меня из-за адаптивного дизайна. Большинство реализаций адаптивного дизайна по сути напоминают редизайн вашего сайта всякий раз, когда пользователь меняет размер окна. Самый вопиющий пример такого рода когда горизонтальная панель меню превращается в вертикальную. В трубу вылетает все то, что пользователь успел зрительно запомнить о расположении ссылок на панели! Думали, эта опция спрятана здесь? Нет же, она там!

Межсайтовая непоследовательность


Но даже если пользователям не нравится адаптивный дизайн, от него особо никуда не денешься. Масса примеров так называемого адаптивного дизайна реализуется при помощи медиа-запросов с применением CSS по сути, это условные операторы, приказывающие браузеру ввести в дело набор правил CSS, если выполняются определенные условия, например, если ширина окна браузера превышает или не превышает заданное количество пикселов. Точное число в данном случае выбирает веб-дизайнер; соответственно, два разных веб-дизайнера почти никогда не выбирают одинаковую ширину.

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

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

Казалось бы, почему пользователь просто не отрегулирует размер окна, если попадет на веб-страницу, для просмотра которой требуется окно пошире. В конце концов, именно так приходилось действовать пользователю до появления адаптивного дизайна, когда была возможность просто переключаться между веб-страницами с заданной фиксированной шириной. Но на самом деле не все так просто: на неадаптивных страницах совершенно ясно, нужно ли изменить размер окна, чтобы в нем поместилась вся страница. А на адаптивной веб-странице пользователю ничто не подсказывает, эй! В такое окно, как у тебя сейчас, вся страница не влезет. Нет ни полосы прокрутки, ни обрезки содержимого; в лучшем (или, полагаю, в худшем) случае есть меню-гамбургер или какой-нибудь другой странный как бы мобильный интерфейс.

Что делать пользователю?


Напрашивается мысль, что пользователь мог бы решить эту проблему, отключив поддержку медиа-запросов в браузере. Если бы все было так просто! Корень проблемы в том, что среди веб-дизайнеров распространена практика закреплять за медиа-запросом полноширинный вариант дизайна и по умолчанию выдавать мобильную версию. Отключив медиа-запросы, мы получим на таких страницах результат, ровно противоположный желаемому.

Рациональнее было бы каким-то образом заставить браузер придавать единую задаваемую пользователем ширину таблицам стилей и скриптам на всех веб-страницах. Это желательно хотя бы из соображений приватности.

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

Что делать веб-дизайнеру?


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

Правда, на более высоком уровне следует задаться вопросом, а в чем вообще смысл адаптивного дизайна. Определенно, такой дизайн очень полезен: современные пользователи выходят в Интернет с самых разных устройств с различной шириной экрана, и эта проблема решается при помощи адаптивного дизайна. Но, возможно, проблема адаптивного дизайна в чрезмерной обобщенности такого решения. Он призван поддерживать работу с любыми мыслимыми размерами экрана, но на самом ли деле экраны столь разнообразны? Скорее, они, напротив, стандартизируются и подразделяются на немногочисленные, легко выделяемые категории. Вот такие:

  1. Настольный ПК/ноутбук
  2. Планшет
  3. Смартфон

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

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

let mobile = navigator.userAgent.match(/Mobi/);let ipad = navigator.userAgent.match(/iPad/);let android = navigator.userAgent.match(/Android/);if (mobile && !ipad)this is a phoneelse if (ipad || android)this is a tablet

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



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

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Категории

Последние комментарии

© 2006-2021, personeltest.ru