Как стать автором
Поиск
Написать публикацию
Обновить
1942.33
Timeweb Cloud
То самое облако

Как я полюбил LESS, избавился от копипасты в CSS-коде, сделал его безопаснее, а разметку семантической (часть 2)

Уровень сложностиСредний
Время на прочтение25 мин
Количество просмотров1.2K

В первой части я рассказывал об основах LESS: переменных, миксинах, и некоторых приёмах. А сегодня мы поговорим о вещах, оставшихся в прошлый раз нераскрытыми:

  • Как автоматически проверять графические файлы, подготовленные художником для сайта или приложения, в процессе компиляции LESS-кода в CSS;

  • Как из картинок генерировать CSS для контролов;

  • Как сделать интерфейс более адаптивным при помощи автоматически масштабируемых изображений;

  • Как использовать вложенность классов совместно с семантической разметкой, чтобы не путаться в структуре HTML и CSS.

А в процессе затронем чисто технические моменты:

  • Организация LESS-кода в своём проекте;

  • Расширение базовых возможностей LESS при помощи плагинов на Javascript'е;

  • Использование миксинов в роли функций (а не классов);

  • Стандартная библиотека LESS.

Добро пожаловать под кат!

❯ Структура проекта

Всё от ужаса рыдает
И дрожит как банный лист!
Компилятор запускает
Структуральнейший LESS'ист.

— Почти АБС

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

❯ main.less (прикладная стилизация)

Как я обещал, далее речь пойдёт о том, как использовать вложенность классов совместно с семантической разметкой. Пока же остановимся на том, что из этих соображений нам нужно будет на каждый файл с разметкой (HTML) создавать по одному файлу с кодом прикладной стилизации на LESS.

Возьмём за основу простой случай: у нас есть ровно один файл с разметкой (index.html). В этом случае, файл с прикладной стилизацией можно назвать main.less. В него войдут все классы, стилизующие элементы из index.html, которые раньше мы писали на CSS.

Вам нужно будет настроить вашу IDE так, чтобы файл main.less компилировался при каждом сохранении. Возможно, для этого придётся поставить в IDE расширение — например, я использую Visual Studio 2022 и мне потребовалось расширение Web Compiler 2022+. Вы же, скорее всего, пользуетесь VS Code, для которого рекомендуют Easy LESS. В настройках я советую явно указать имя целевого файла (main.less → main.css) и дополнительно включить минификатор, не забыв попросить генерацию source map минифицированного файла (что удобно для отладки в браузере).

В результате, при каждом изменении и сохранении main.less должна происходить генерация трёх файлов: результирующего main.css, его минифицированной версии main.min.css и отладочного main.min.css.map. Файл main.css нужен нам для того, чтобы заглядывать внутрь и проверять, что у нас получилось в результате компиляции. main.min.css можно выкладывать в продакшен (сайт или билд программы на основе WebView/Chromium), ссылаясь на него из HTML обычным способом:

<link rel="stylesheet" href="css/main.min.css">

Ну а main.min.css.map пригодится, если вы пользуетесь DevTools. Как минимум, он будет правильно показывать номера строк в исходном файле (а не вечное 1 для минифицированного варианта).

В начало main.less я вставляю три секции:

  1. Глобальные переменные для управления компиляцией.

  2. Ссылки на библиотеки LESS.

  3. Ссылки на библиотеки CSS.

Вот как это примерно выглядит:

// Глобальные переменные для управления компиляцией
@debug: true;

// Ссылки на библиотеки LESS
@import "lib";

// Ссылки на библиотеки CSS
@import "animations.css";

Для чего нужно управлять компиляцией при помощи глобальных переменных? Вспомните пример из самого начала первой части:

.debugA when (@debug = true)
{
	background-color: aliceblue;
}

Генерация кода .debugA (и создание класса .debugA, и подстановка свойства background-color при вызове .debugA() как миксина) произойдёт только тогда, когда включен режим отладки.

Директива @import (напоминающая #include из C/C++) позволяет вставлять как библиотечные LESS-файлы, так и обычные CSS'ные. При вставке файлов, написанных на LESS, расширение указывать необязательно (в случае с @import "lib"; это значит, что мы ссылаемся на библиотечный файл lib.less). Исходный код из файла будет сначала подставлен, а уже потом скомпилирован (иначе было бы глупо, не так ли?).

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

@keyframes sys-rotate-from-0-to-360
{
	from
	{
		transform: rotate(0deg);
	}
	to
	{
		transform: rotate(360deg);
	}
}

@keyframes sys-opacity-from-0-to-1
{
	from
	{
		opacity: 0;
	}
	to
	{
		opacity: 1;
	}
}

@keyframes sys-opacity-from-1-to-0
{
	from
	{
		opacity: 1;
	}
	to
	{
		opacity: 0;
	}
}

/* …и другие подобные анимации. */

Там же, в main.less, я пишу все прикладные миксины (то есть, такие, которые имеют смысл только в контексте генерации main.css). При этом я стараюсь объявлять каждый миксин непосредственно перед использованием:

// Mixin for a button with a nested SVG icon.
.icon-button(@size)
{
	.size-and-font(@size);
	.center-center();

	background-color: transparent;
	border: none;
}

.code-toolbar
{
…
	& > button
	{
		.icon-button(rp(32));

		opacity: 0;
		…
	}
}

.btn-close-dialog
{
	.icon-button(rp(36));
	…
}

❯ lib.less (библиотека миксинов, списков селекторов и т.п.)

В отличие от прикладных, универсальные миксины есть смысл выносить в библиотеки, чтобы затем использовать их в нескольких LESS-файлах или даже нескольких проектах.

В нашем примере мы не будем дробить библиотечный код на разные библиотеки, а ограничимся одной — lib.less.

Вставим в начало lib.less ссылку на используемые плагины:

@plugin "less-lib.js";

Это позволит нам расширять базовые возможности LESS, дописывая свои кастомные функции в дополнение к функциям стандартной библиотеки LESS, и пользоваться ими как в библиотечных миксинах, так и прикладном коде (ведь эта ссылка попадёт в main.less вместе со всем остальным кодом). Помните «макрос» rp() из первой части, который позволял для пущей наглядности указывать размеры в «относительных» пикселях вместо rem'ов? Это одна из таких кастомных функций.

Что ещё из описанного в первой части вынесено в lib.less?

Списки браузерозависимых селекторов, таких как @range-track и @range-thumb.

Реализации псевдоклассов, таких как @text-input.

Системно-синтаксические миксины, такие как .any.

Геометрические миксины.

Примеры геометрических миксинов
// Sets width and height.
.size(@w, @h)
{
	width: @w;
	height: @h;
}

// Sets the same width and height.
.size(@size)
{
	.size(@size, @size);
}

// Sets the same width, height and 1em.
.size-and-font(@size)
{
	.size(@size);

	font-size: @size;
}

// Sets left and top. Makes an element absolutely positioned.
.pos(@l, @t)
{
	left: @l;
	top: @t;

	position: absolute;
}

// Sets the same left and top. Makes an element absolutely positioned.
.pos(@offset)
{
	.pos(@offset, @offset);
}

// Sets right and top. Makes an element absolutely positioned.
.r-pos(@r, @t)
{
	right: @r;
	top: @t;

	position: absolute;
}

// Sets the same right and top. <akes an element absolutely positioned.
.r-pos(@offset)
{
	.r-pos(@offset, @offset);
}

// Sets right and bottom. Makes an element absolutely positioned.
.rb-pos(@r, @b)
{
	right: @r;
	bottom: @b;

	position: absolute;
}

// Sets the same right and bottom. Makes an element absolutely positioned.
.rb-pos(@offset)
{
	.rb-pos(@offset, @offset);
}

// Sets left and bottom. Makes an element absolutely positioned.
.lb-pos(@l, @b)
{
	left: @l;
	bottom: @b;

	position: absolute;
}

// Sets the same left and bottom. Makes an element absolutely positioned.
.lb-pos(@offset)
{
	.lb-pos(@offset, @offset);
}

// Sets all four: left, top, right and bottom. Makes an element absolutely positioned.
.l-t-r-b(@l, @t, @r, @b)
{
	left: @l;
	top: @t;
	right: @r;
	bottom: @b;

	position: absolute;
}

Вспомогательные миксины.

Примеры вспомогательных миксинов
// Makes an element round.
.circle()
{
	border-radius: 100%;
}

// Flips an element vertically.
.flip-vertically()
{
	transform: scaleY(-1);
}

// Clears out any default style of a control.
.reset-control()
{
	appearance: none;
	background-color: transparent;
}

// Makes a control vertical.
.vertical-control()
{
	writing-mode: vertical-lr;
	direction: rtl;
}

Все остальные более-менее универсальные миксины, о которых мы будем говорить дальше.

❯ less-lib.js (плагины LESS, т.е. кастомные функции)

Как понятно из расширения, реализация написана на Javascript. Несмотря на это, файл с плагинами имеет смысл только в контексте генерации CSS, поэтому я держу его в той же папке, где и весь остальной LESS/CSS.

Пишем плагины LESS

Почему, выбирая между LESS и SASS, я выбрал именно LESS? Потому, что его компилятор изначально был написан на Javascript, с прицелом на юзерские JS-расширения. Я знал, что рано или поздно (причём, скорее, рано) мне придётся перейти и на эту дрянь начать дописывать недостающие части, отсутствующие в языке «из коробки».

Так и получилось, когда мне понадобились «относительные пиксели» на замену rem'ам, но ими дело не ограничилось.

Вот как выглядит структура файла less-lib.js:

// 1. Вспомогательные функции, например генерация гуидов:

// 'crypto' interface is not available in some LESS-compiling environments.
function getGuid()
{
	return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c)
	{
		var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
		return v.toString(16);
	});
}

…

registerPlugin
(
	{
		install: function (less, pluginManager, functions)
		{

			// 2. Кастомные функции для вызова из LESS:

			functions.add('myFunction1', function (parameters)
			{
				return …;
			});

			functions.add('myFunction2', function (parameters)
			{
				return …;
			});

			…
		}
	}
)

Функции, которые мы таким образом регистрируем (myFunction1(), myFunction2() и т.д.), могут быть после этого вызваны из LESS наравне с функциями стандартной библиотеки (такими, как e(), each() и range(), с которыми мы встречались в первой части).

Функция может принимать любое число параметров, каждый из которых представляет собой некий объект с полями. Во всех описанных далее случаях нас интересует только поле value, из которого мы извлекаем значение. Кстати, документации по написанию плагинов нет, поэтому если вдруг вас заинтересует структура этого объекта (для какого-нибудь особого случая, например для работы с единицами измерения), у вас есть два пути: путь настоящего самурая — читать исходный код компилятора, и путь, которым воспользовался я — я называю это «алерт-отладка» 😎 — печатать объект со всеми именами полей и значениями в строку, а затем выводить её как сообщение об ошибке в IDE для изучения (как именно, я покажу отдельно).

Возвращать из функции можно обычную джаваскриптовскую строку ('red'), универсальное значение LESS (new tree.Value(myArray);) и число с единицами измерения (new tree.Dimension(42, 'px')).

Вот как реализована ранее многократно упоминавшаяся функция rp(), которая позволяет писать width: rp(3); вместо глазоломающего width: 0.1875rem;:

functions.add('rp', function (rpx)
{
	return new tree.Dimension(rpx.value / 16, 'rem');
});

❯ Падающего подтолкни

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

Признаюсь: я вообще не очень люблю ни компиляцию как процесс, ни статическую типизацию как способ провериться на ошибки. Любая компиляция — всегда дополнительный шаг на пути от замысла к продукту, и если она не обусловлена объективной необходимостью (например, необходимостью получения бинарного запускаемого файла из высокоуровнего языка), то лучше обойтись без неё. Кроме того, стоит разок поотлаживаться «в полях», т.е. на площадке заказчика, где из всех средств разработки есть только Блокнот, понимаешь, насколько лучше, когда можно просто сохранить файл. Что касается статической типизации, я предпочитаю тесты, которые обычно гибче.

Но раз уж вы тут и читаете этот опус, значит в первой части я вас убедил, что в случае с CSS игра стоит свеч, и компиляция LESS позволяет добиваться лучших результатов, чем голый CSS. А ошибки компиляции, которые мы можем генерировать, в случае с LESS примерно эквивалентны написанию тестов. (На C++ или C# вы не можете в процессе компиляции проверить ресурсы проекта на валидность, а в LESS — можете).

Итак, как выдать свою ошибку при компиляции?

Всё просто — можно выкинуть исключение с сообщением: throw new Error('Ну, привет');. Ошибка обрабатывается так же, как и любая ошибка с невалидным LESS-кодом, что гарантирует нам попадание 'Ну, привет' вместе с номером строки и именем LESS-файла в окно IDE с ошибками компиляции.

(Сюда же, как я уже говорил, можно выводить отладочную информацию о типе объектов и вообще — о контексте компиляции, если лень читать исходники компилятора LESS.js).

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

❯ “Die, Bart, die” (что-то на немецком)

Для остановки компиляции я добавил функцию die():

functions.add('die', function (message)
{
	throw new Error(`Compiling LESS error: ${message.value}`);
});

Использовать её можно как макрос TODO, когда описываешь целое семейство миксинов, и не успеваешь их все реализовать в очередном билде:

.frame-based-animation(@src, @speed)
{
	die('Not implemented');
}

Или так:

.set-aspect-ratio-and-background-from-img(@src, @inverse: false)
{
	& when (@debug = false)
	{
		die('Not implemented');
	}

	… черновая имплементация …
}

Это хороший способ не дать собраться (опционально — релизной) версии при случайном вызове недописанного и недоотлаженного миксина. (Если мы просто добавим выражение when (@debug = true) в конце миксина, как в случае с .debugA, сборка не сломается — просто тело миксина будет проигнорировано).

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

.my-mixin(@param)
{
	& when not (@param = 'none') and not (@param = 'fill') and not (@param = 'ignore')
	{
		die('Invalid @param value');
	}

	a: b;
}

.test
{
	// Скомпилируется и сгенерирует .test { a : b; }
	.my-mixin('none');

	// Выдаст ошибку при компиляции (с указанием проекта, файла, строки):
	// SyntaxError: Error evaluating function `die`:
	// Compiling LESS error: Invalid @param value in \css\main.less
	.my-mixin(0);
}

В следующей части, если я увижу, что тема остаётся для вас интересной, я разберу, как добавить в синтаксис языка более-менее полноценный enum.

Ещё при помощи die() можно гарантировать, что переданные параметры не противоречат друг другу (например, что при вызове миксина задана только высота или только ширина) и проверить другие сценарии.

Может показаться, что это уже излишняя перестраховка, и я просто «глюкалу полирую». Во всяком случае, мне самому иногда так кажется. Но я каждый раз напоминаю себе: браузер ничего не знает о семантике стилизации и не может показать логические ошибки, даже если страница очевидно (для каждого хумана) сломана. И проверки + die() — единственный способ конвертировать логическую ошибку в ошибку компиляции. Ведь если у вас счёт пользователей идёт на десятки или сотни тысяч (тем более, если больше), будет очень неприятно таким образом облажаться.

(А знаете, кто так постоянно лажает? Мобильный Firefox! Я, правда, пользуюсь Nightly-версией из-за наличия в ней about:config, но за то, как временами перекашивает интерфейс, который тоже написан на CSS, мне было бы стыдно даже в Nightly-версии).

Один неловкий поворот, и… э-э-э… меню контейнер рвёт.
Один неловкий поворот, и… э-э-э… меню контейнер рвёт.

❯ Как говорил один мой знакомый… покойник… доверять нельзя никому, даже себе

Второй юз-кейс для самопроверки — сравнение значений. (По правде говоря, на практике оказалось нужно только проверять ожидаемое равенство). Подобные проверочные функции я решил назвать assert'ами (учитывая, что это тест, выполняемый в процессе компиляции, термин, возможно, не самый удачный — но как назвал, так назвал). Итак, вот как выглядит плагинная функция assert'а равенства:

functions.add('assertEq', function (a, b)
{
	// ВНИМАНИЕ! Для сложных типов сравниваем только числовую часть.
	if (a.value != b.value)
		throw new Error(`Assertion failed: ${a.value} != ${b.value}`);

	return '';
});

Чтобы ею воспользоваться в LESS, достаточно написать:

.my-mixin()
{
	// Инициализируем две переменных результатами расчётов.
	@var-1: .get-var-1()[];
	@var-2: .get-var-2()[];

	// Они должны быть равны.
	assertEq(@var-1, @var-2); // Не компилируется, если это не так.

	…
}

❯ Скажите, Малевич, как художник художнику: а в присланном вами файле длина точно равна ширине?

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

Но мы можем проверить это условие автоматически! Помните, я говорил, что хотя стандартная стандартная библиотека LESS не даст вам в процессе компиляции поднять веб-сервер (что, безусловно, должно огорчать некоторых зумеров, отвыкших от работы с файлами), зато она поддерживает кое-какой файловый ввод-вывод? Так вот, получить длину и ширину файла с изображением можно в процессе компиляции. И сразу их сравнить:

// Checks, whether an image is square. Breaks compilation, if not.
// Out params: cached @width, @height.
.assert-image-is-square(@src)
{
	@width: image-width(@src);
	@height: image-height(@src);

	assertEq(@width, @height);
}

Это assert-миксин из библиотеки миксинов lib.less, который вызывает плагинную функцию assertEq, чтобы проверить равенство размеров и сломать компиляцию, если они не равны.

Пользоваться им можно так:

.assert-image-is-square('../images/black-square.png');

Или так:

@src: '../images/lens.png';
@size: unit(.assert-image-is-square(@src)[@width]);

Что здесь происходит?

Во-первых, мы теперь можем нашему гуманитарному другу, вооружённому Фотошопом, дать доступ к репозиторию. Пусть сам коммитит новые версии картинки с линзой, а мы не будем мешать творческому процессу. Если файл вдруг станет неквадратным, билд-сервер просто не соберёт проект, и при грамотной настройке continious integration все заинтересованные лица получат по голове письму. Проверять размеры можно не только на квадратность, но и сравнивая с конкретными значениями, однако я так жёстко стараюсь не действовать — пусть лучше размеры картинки будут задавать описание стилей (к чему мы ещё вернёмся).

Во-вторых… я в первой части упоминал, что миксины могут выступать в роли не только классов, но и функций, и возвращать значения. Так вот, для этого даже не нужно писать никакой return — все локальные переменные, объявленные в миксине, автоматически превращаются в элементы именованного списка, который этот миксин неявно возвращает. В нашем миксине мы объявили две локальных переменных: @width и @height. Соответственно, после вызова миксина он возвращает список из этих двух элементов.

Получить доступ к конкретному элементу именованного списка можно стандартным способом — указав имя в квадратных скобках. Поэтому, если наш миксин не упал из-за неравенства размеров, такая запись: .assert-image-is-square(@src)[@width] вернёт вычисленное значение переменной @width. Если нам нужен размер картинки для чего-то ещё, помимо проверки, зачем получать его из файла дважды? Если компилятор LESS не кеширует чтение метаданных из файла, это сильно замедлит компиляцию. Если кеширует — это замедлит компиляцию несильно, но всё равно желательно, чтобы она происходила как можно быстрее, в идеале — мгновенно. (И можно было видеть результат в браузере, не дожидаясь подолгу, пока код скомпилируется, особенно если у вас в проекте сотни картинок, а поменять вам нужно один символ, не имеющий к ним отношения).

Но стандартные функции image-width() и image-height() возвращают не числа, а длины, то есть включают в себя единицы измерения. К счастью, мы знаем, что эти единицы — всегда px (это же нативные размеры растрового изображения!), поэтому так смело отдаём их в assertEq(), который проверяет только числовую часть переданных значений (поле value). Однако, если нам потребуется сложить размер с другим числом, мы получим ошибку компиляции. Именно для того, чтобы её предотвратить, выражение .assert-image-is-square(@src)[@width] (значение ширины, полученное из объекта, который вернул миксин) передаётся в ещё одну стандартную функцию LESS: unit(). Эта функция переводит одни единицы в другие, а если не указать требуемые единицы — просто возвращает число. Это число, равное и длине, и ширине картинки линзы в пикселях, попадает в переменную @size, которую можно использовать в последующих расчётах:

@margin: (@size * 0.3); // Отступы 30% от размера.
margin: unit(@margin, px);

Чтобы проверить квадратность набора изображений, добавим версию assert'а, поддерживающего списки:

// Checks, whether all images are square. Breaks compilation, if not.
// No out params.
.assert-images-are-square(@images)
{
	each(@images,
	{
		.assert-image-is-square(@value);
	});
}

На всякий случай, если вы запутались: это тоже миксин из библиотеки миксинов lib.less. Пользоваться им можно так (пример из main.less):

@cell-images:
	'../images/1.png',
	'../images/2.png',
	'../images/3.png',
	'../images/4.png',
	'../images/5.png',
	'../images/6.png',
	'../images/7.png',
	'../images/8.png',
	'../images/9.png';

.assert-images-are-square(@cell-images);

❯ Все ли животные равны, или некоторые равнее?

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

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

// Checks, whether two images are the same size. Breaks compilation, if not.
// Out params: cached @width, @height.
.assert-images-are-the-same-size(@image-1, @image-2)
{
	@width: image-width(@image-1);
	@height: image-height(@image-1);

	assertEq(@width, image-width(@image-2));
	assertEq(@height, image-height(@image-2));
}

// Checks, whether all images are the same size. Breaks compilation, if not.
// Out params: cached @width, @height.
.assert-images-are-the-same-size(@images)
{
	@first-image: extract(@images, 1);
	@width: image-width(@first-image);
	@height: image-height(@first-image);

	each(@images,
	{
		assertEq(image-width(@value), @width);
		assertEq(image-height(@value), @height);
	});
}

Опять же, на всякий случай: @image-1 это не «имидж минус один», как может показаться сослепу, а имя переменной «первое изображение». Эх, люблю LESS и CSS: хотите написать «минус», ставьте вокруг пробелы, не лепите всё выражение в слитную кучку!

Из интересного: во второй перегрузке нам встречается стандартная функция extract, которая извлекает элемент из списка по индексу (аналог квадратных скобок в Javascript). И, как вы видите, нумерация списков ведётся от единицы. Эх, а вот за это не люблю LESS и CSS.

Теперь мы можем даже одновременно сравнить квадратность каждого изображения в списке и одинаковость их размеров, да ещё и записать этот размер в переменную:

@cell-images:
	'../images/1.png',
	'../images/2.png',
	'../images/3.png',
	'../images/4.png',
	'../images/5.png',
	'../images/6.png',
	'../images/7.png',
	'../images/8.png',
	'../images/9.png';

@cell-size: .assert-images-are-the-same-size(@cell-images)[@width];
            .assert-images-are-square(@cell-images);

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

Дэмьен из Mean Girls плохого не посоветует
Дэмьен из Mean Girls плохого не посоветует

Какую ещё пользу может принести извлечение размера картинок?

При помощи этих двух функций LESS (image-width() и image-height()) можно добиться от HTML/CSS дополнительных удобств при сработе с изображениями. О них и поговорим ниже.

❯ Размеры контролов

Для одного из проектов мне потребовалось реализовать контрол, в котором совмещались компас (показывающий направление при помощи спутниковой навигации), уровень («пузырёк», G-sensor, показывающий отклонение от горизонтальной плоскости) и ещё кое-какие полезняшки. Вот как это выглядело в итоге:

Извините за артефакты компрессии
Извините за артефакты компрессии

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

Зачем тут так много слоёв? Во-первых, в зависимости от режима контрола слои приходилось перемещать в соответствии с приоритетом (отображать стрелку поверх пузырька или наоборот). Во-вторых, нужна была настройка «уровня детализации» — одно дело, когда сейлзы показывают в прохладном полутёмном кабинете оборудование директорам компаний-клиентов, и совсем другое — когда подчинённые директоров пользуются потом этим оборудованием под палящим солнцем (они предпочитают отключить блики — хватает природных). Запомним этот момент, позже я покажу, как LESS помогает управлять слоями.

При запуске приложения данные с датчиков превращались в значения CSS-переменных, управляющих слоями: слой со стрелкой вращался, слой с пузырьком перемещался и т.д. Естественно, делая это плавно за счёт свойства transition.

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

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

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

// Makes an element the same size in px as the given image.
// Out params: cached @width, @height.
.fixed-element-size-of-img(@src, @set-height: true)
{
	@width: image-width(@src);
	@height: image-height(@src);

	width: @width;

	& when (@set-height)
	{
		height: @height;
	}
}

// Adds a background image and sets background size in px.
.fixed-background-size-of-img(@src)
{
	@width: image-width(@src);
	@height: image-height(@src);

	background-size: @width @height;
	background-image: url(@src);
}

// Makes an element the same size in px as the given image.
// Adds the image as background and sets background size in px.
.fixed-background-and-element-size-of-img(@src)
{
	.fixed-element-size-of-img(@src);
	.fixed-background-size-of-img(@src);
}

После чего и размер, и фон слоёв контрола (а стало быть, и всего контрола целиком) стало можно устанавливать одним-единственным вызовом миксина .fixed-background-and-element-size-of-img() с именем файла. И при сборке разных версий с разными файлами картинок генерировать из них разный CSS.

ℹ️ img vs. background-image
Может возникнуть вопрос: почему бы в таких случаях просто не использовать <img>, элемент которого автоматически будет иметь размер, определяемый файлом? Во-первых, адаптивные изображения всё равно потребуют CSS, а во-вторых, и главное, у <img> просто другая семантика — семантика контента, а не оформления. Использовать <img> ради особенностей его реализации означает закладывать мину под свой код. Где она взорвётся — неизвестно, но ногу оторвёт запросто. Самый простой пример — accessibility, которая часто делает больно тем, кто не соблюдает семантику: скринридер может попытаться озвучить кусок компаса, приняв его за иллюстрацию, а браузер при определённых условиях заботливо сделает картинку фокусабельной (например, в Chromium для этого было достаточно подписаться на событие focusin у контейнера, и, что хуже всего, разработчик не мог извне отключить такое поведение… а искать его в коде Chromium, если вы сами собираете браузер, поверьте, задача не для слабонервных).

Из интересного — в первом миксине мы впервые встречаемся с такой штукой, как дефолтное значение параметра миксина: @set-height: true. Под таким углом миксины всё более походят на функции, не правда ли? Этот параметр управляет генерацией высоты (ширина генерируется всегда). Проверка его значения осуществляется при помощи конструкции & when (@set-height), которая заменяет в LESS условный оператор if.

❯ Адаптивные контролы и изображения

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

Если мы верстаем интерфейс адаптивно, размер текста должен меняться пропорционально базовому размеру шрифта, а для этого размеры всех текстов надо прямо или косвенно (через em'ы) указывать в rem'ах. При отображении интерфейса в браузере пользователь может сам поменять эту настройку (подробности ищите в этой статье). А при встраивании движка браузера в своё приложение удобно использовать базовый размер шрифта как основу для скейлинга интерфейса (масштаб всего UI можно задать, например, в десктопной версии Telegram).

Однако, некрасиво, когда текст меняет свой размер, а контролы — нет. Хуже того, при этом нарушаются пропорции, что может привести к разъезжаниям и наползаниям элементов друг на друга.

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

// Makes an element the same size in rem as the given image.
.adaptive-element-size-of-img(@src, @set-height: true)
{
	@width: image-width(@src);
	@height: image-height(@src);

	width: rp(unit(@width));

	& when (@set-height)
	{
		height: rp(unit(@height));
	}
}

// Adds a background image and sets background size in rem.
.adaptive-background-size-of-img(@src)
{
	@width: image-width(@src);
	@height: image-height(@src);

	background-size: rp(unit(@width)) rp(unit(@height));
	background-image: url(@src);
}

// Makes an element the same size in rem as the given image.
// Adds the image as background and sets background size in rem.
.adaptive-background-and-element-size-of-img(@src)
{
	.adaptive-element-size-of-img(@src);
	.adaptive-background-size-of-img(@src);
}

Помните, что image-width() и image-height() возвращают не числа, они возвращают размеры в пикселях. Поэтому их приходится очищать от единиц измерения при помощи функции unit(). Затем в дело вступает функция rp(), которая генерирует из числа пикселей rem'ы, соответствующие размерам изображения при дефолтном размере шрифта.

И готово: поменяв размер базового шрифта, мы получим пропорциональное изменение контролов вместе с текстом.

Кроме того, ничто не мешает нам использовать этот миксин и для обычного <img>. Да, если нам нужны картинки как иллюстрации, в соответствии с семантикой мы должны пользоваться именно этим тегом. Ну а после, в main.less, можно построить список адаптивных иллюстраций, масштабируемых вместе с текстом:

// <img src="">
@adaptive-imgs:
	"images/shop/logo.png",
	"images/thumbnails/aliexpress.com.png",
	"images/boost.png",
	… перечисляем все изображения …
	"images/cloud-core.png";

each(@adaptive-imgs,
{
	img[src=@{value}]
	{
		.adaptive-element-size-of-img('../@{value}', false); // Don't set height to avoid conflicts with fluid images.
	}
});

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

Обратите внимание, что тут-то нам как раз и пригодился параметр @set-height. Как видите, для <img> может быть лучше положиться на пропорциональность длины и ширины файла, обеспечиваемую автоматически: например, это пригодится, если мы хотим уменьшать иллюстрации, не влезающие в контейнер или пользоваться min-width/max-width в других целях.

❯ Сведение слоёв

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

@images:
	'../images/image1.png',
	'../images/image2.png',
	'../images/image3.png';

// Теперь можно проверять:
.assert-images-are-square(@images);
.assert-images-are-the-same-size(@images);

Но работать со списками вообще удобно. Их можно соединять, разъединять, формировать разные конфигурации (например, конфигурацию «детализированный контрол» со всеми слоями и конфигурацию «минимум деталей» только с необходимыми). Естественно, совершенно незачем создавать под каждый слой отдельный элемент DOM — как вы помните, свойство background-image уже поддерживает слои — списки изображений, комбинируемых при рендеринге.

И всё бы ничего, но каждый элемент списка для background-image должен быть завёрнут в url():

background-image: url(../images/image1.png), url(../images/image2.png), url(../images/image3.png);

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

В то же время url() не нужен нам при работе с изображением (при извлечении его размеров и т.п.).

Так что, пришлось для преобразования списков изображений к строке, которую браузеры сочтут валидным значением background-image, состряпать следующую плагинную функцию (файл less-lib.js):

functions.add('multipleBackgrounds', function (list)
{
	if (Array.isArray(list.value))
		return list.value.reduce((a, v) => (a ? a + ', ' : a) + `url(${v.value})`, '');

	return `url(${list.value})`; // For single element lists.
});

Пользоваться ей можно так:

@images:
	'../images/image1.png',
	'../images/image2.png',
	'../images/image3.png';

background-image: multipleBackgrounds(@images);

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

А вообще, конечно, чувствуется, чувствуется что функциональщина пока ещё не проникла во все слои программирования. Если стандартная библиотека языка поддерживает списки (а в LESS для этого был специально введён набор функций), то так и напрашиваются какие-нибудь аналоги лямбд и map/reduce. К счастью, всё это «есть у нас дома», то бишь в Javascript'е, и легко становится доступным в LESS.

❯ Конкатенация списков

К вопросу о недостающих вещах при работе со списками. Какая уж там функциональщина, какие лямбды! Увы, из коробки в LESS нет даже банальной конкатенации списков. Пришлось решать и этот вопрос, путём добавления в less-lib.js такой плагинной функции:

functions.add('concatLists', function (...args)
{
	const values = [];

	for (const arg of args)
	{
		if (arg.value instanceof Array)
		{
			values.push(...arg.value);
		}
		else
		{
			values.push(arg);
		}
	}

	return new tree.Value(values);
});

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

@compass-dial-layer-images:
	'../images/compass/layer9.png',
	'../images/compass/layer8.png',
	'../images/compass/layer7.png',
	'../images/compass/layer6.png',
	'../images/compass/layer5.png',
	'../images/compass/layer4.png',
	'../images/compass/layer3.png',
	'../images/compass/layer2.png',
	'../images/compass/layer1.png';

@compass-needle-layer-images:
	'../images/compass/layer11.png',
	'../images/compass/layer10.png';

@compass-glass-layer-images:
	'../images/compass/layer14.png',
	'../images/compass/layer13.png',
	'../images/compass/layer12.png';

@compass-bezel-layer-images:
	'../images/compass/layer15.png';

// Объединяем несколько списков в один:

@compass-layer-images: concatLists(@compass-bezel-layer-images,
	@compass-glass-layer-images,
	@compass-needle-layer-images,
	@compass-dial-layer-images);

// Теперь, например, можно каждую группу назначать элементу
// при помощи background-image: multipleBackgrounds(),
// а общий список отдавать на проверки в assert'ы.

Две параллельные иерархии

Теперь вернёмся к тому, с чего я начал первую часть: переход от утилит к миксинам позволяет сделать разметку более семантической.

Действительно, раз больше нет необходимости указывать все эти .p-0 и .rounded-1, маркировать элементы можно строго по смыслу, например, .product-card. В результате, в HTML останется голый скелет, навешивать на который мясо оформления можно в LESS (в нашем примере — main.less), комбинируя внутри этих классов (.product-card) разные миксины.

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

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

<nav aria-label="Main menu">
	<svg>
		<use href="#svg-lib_menu-icon"></use>
	</svg>

	<ul>
		<!-- Пункты меню приложения… -->
		<li><a class="menu-file" href="#">File</a></li>
		<li><a class="menu-edit" href="#">Edit</a></li>
		<li><a class="menu-view" href="#">View</a></li>

		<!-- …или меню страницы -->
		<li><a href="#home">Home</a></li>
		<li><a href="#about">About</a></li>
		<li><a href="#contact">Contact</a></li>
	</ul>
</nav>

<main>

	<section class="about">
		…
	</section>

	<section class="contact">
		…
	</section>

	…остальные секции…
</main>

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

Иконка в начале меню содержит <svg> и <use>, без которых просто не получится сослаться на нужную запись в библиотеке иконок <svg><defs></defs></svg>. (Про то, как я пробовал разные варианты организации библиотеки векторных изображений SVG, если кому-то интересно, могу как-нибудь написать отдельно. Спойлер: идеального способа я не нашёл).

Список (<ul>, <li>) нужен чисто семантически. (Перефразируя знаменитого преподавателя с военной кафедры: «Список пунктов меню должен быть списком, ведь это же, как-никак, список!»).

Наконец, если у ссылок явно не указан адрес href, для адресации и привязки обработчиков они размечаются классами. (Я почти никогда не использую id в прикладном коде, чего и вам советую. Почему так, как навязывание стандартами использования id приходится обходить в системных библиотеках, и как выглядит аналог этих библиотек для CSS (написанный, конечно, на LESS) — про всё это читайте в следующей части).

Теперь перейдём к оформлению. Вот как выглядит соответствующая часть прикладного main.less:

body
{
	nav
	{
		…описание контейнера меню…

		svg
		{
			.svg-inline();
			…
		}

		ul > li
		{
			…геометрия пункта меню…

			a
			{
				…цвета пункта меню…
			}
		}

		&.collapsed
		{
			…описание свёрнутого состояния меню…
		}
	}

	main
	{
		section.about
		{
			…
		}

		section.contact
		{
			…
		}
	}
}

Что мы здесь наблюдаем?

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

.svg-inline
{
	fill: currentColor;
	display: inline;
	height: 1em;
	vertical-align: middle;
}

Кстати, если вы до сих пор используете иконочные шрифты только потому, что не знаете, как ещё иконки можно помещать в текст в виде символов, чтобы они отображались цветом текста и имели ту же высоту, то с классом svg-inline вы можете просто копировать SVG-код откуда-нибудь с Bootstrap Icons, что, по-моему, гораздо удобнее. (Как минимум, тем, что не надо тащить внешний файл в шрифтовом формате).

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

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


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

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

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

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


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud - в нашем Telegram-канале

Перед оплатой в разделе «Бонусы и промокоды» в панели управления активируйте промокод и получите кэшбэк на баланс.

Теги:
Хабы:
+9
Комментарии2

Публикации

Информация

Сайт
timeweb.cloud
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель
Timeweb Cloud