Parcel & Pug – особенности работы с изображениями
Привет. Когда в обучении фронту я дошел до картиночек и вставки их на страницу я был слегка удивлён. Когда дошел до их адаптивности уже был не слегка.
Казалось бы, тег <img> ему атрибутом src='котик.png' да и всё, ан нет. Естественно, нужен как минимум alt, а то валидацию не пройдет и дядя SEO-шник даст пизды, потом еще обязательно width / height, а то контент-манагер еще вставит 10мегапикселов фото и всё поедет как твоя кукуха.
И это только статика, через CSS "адаптивно" ты можешь только тянуть картинку передавая привет всем шакалам, койотам и всем остальным безумно можно быть первым. Можно все пофиксить object-fit, но тогда либо у тебя махонькая картинка на десктопе, либо FullHD на телефоне, что само по себе слегка похоже на выбор стула из... нутыпоэл.
Ах, да! Еще же эти ваши ретина-дисплеи и, конечно же, новые модные-молодёжные реп-дискета-дискотека лёгкие форматы WebP и AVIF.
Итого у нас получается, что одна картинка - это не одна картинка, а в очень минимум варианте две-три, а по хорошему... хм... Восемнадцать! И все их надо верно разметить в <picture>.
Готовить всё это ручками, конечно, дело не боярское.
Автоматизируй
Естественно, мыжпрограммисты. На это есть всякие бандлеры, например - Gulp, Webpack или сегодняшний пациент - Parcel. Их вообще дофига, но самые популярные для фронта, насколько я понимаю, эти трое.
На самом деле все они, так или иначе, работают через один и тот же инструмент – плагин или модуль Node.js (я еще не до конца просёк терминологию) под названием Sharp. Работает просто - даёшь ему картинку, говоришь чего с ней сделать, отдаёт тебе картинку.
Parcel сам по себе мощная штука, хотя и специфическая, но об этом в другой раз. Самое главное - он умеет из коробки работать с Sharp, а как следствие с картинками и более того, позволяет без всяких конфигов прямо в HTML и CSS указать что с картинкой делать.
<picture> <source srcset="image.jpeg?as=avif&width=800" type="image/avif" /> <source srcset="image.jpeg?as=webp&width=800" type="image/webp" /> <source srcset="image.jpeg?width=800" type="image/jpeg" /> <img src="image.jpeg?width=200" alt="test image" /> </picture>
Задача для Sharp формируется с помощью query-параметра после знака ? в пути к картинке. Если параметра, читай задачи, два - их соединяем символом &. В нашем случае у первого <source> мы "попросили" Parcel сконвертировать image.jpeg в формат AVIF - as=avif, и изменить размер на 800px по ширине - width=800. По умолчанию, высота ресайзится пропорционально.
Кроме конвертации as= и размеров width=/height=, можно еще задать уровень качества, то бишь степень сжатия через quality=, но я никогда не трогал её, так как дефолтные 75 меня устраивают.
То есть, по сути нам нужна только одна jpeg/png картинка-исходник, самая большая - всё остальное можно сделать через Parcel и прям в разметке указать чего куда подставить. Примерный пример - картинка-исходник 960px в ширину для ретина 2х, остальное делаем через параметры:
<picture>
<!-- desktop: avif, webp, png с ретиной -->
<source
type="image/avif" media="(min-width: 1280px)"
srcset="pic.png?as=avif&width=480, pic.png?as=avif 2x" />
<source
type="image/webp" media="(min-width: 1280px)"
srcset="pic.png?as=webp&width=480, pic.png?as=webp 2x" />
<source
type="image/png" media="(min-width: 1280px)"
srcset="pic.png?width=480, pic.png 2x" />
<!-- tablet, ресайз условный -->
<source
type="image/avif" media="(min-width: 768px)"
srcset="pic.png?as=avif&width=320, pic.png?as=avif&width=640 2x" />
<source
type="image/webp" media="(min-width: 768px)"
srcset="pic.png?as=webp&width=320, pic.png?as=webp&width=640 2x" />
<source
type="image/png" media="(min-width: 768px)"
srcset="pic.png?width=320, pic.png?width=640 2x" />
<!-- mobile -->
<source
type="image/avif"
srcset="pic.png?as=avif&width=260, pic.png?as=avif&width=520 2x" />
<source
type="image/webp"
srcset="pic.png?as=webp&width=260, pic.png?as=webp&width=520 2x" />
<img
alt="picture" width="260" height="180"
src="pic.png?width=260" srcset="pic.png?width=520 2x" />
</picture>В CSS, кстати, тоже работает, но функцию image-set() пока хреновенько поддерживают браузеры.
В прошлой статье я частично использовал этот инструмент, но не до конца разобрался, плюс нюансы есть с Pug'ом, о чем дальше.
Штош. Миллион картинок нам теперь не нужны, но верстать всё еще надо весь десяток тегов для каждой контентной картинки. А что если это карточки в каталоге или галерея? Прокачивай десятипалый набор или...
Шаблонизируй
Наверное, надо было наоборот, но на примере простого HTML проще понять как происходит конвертация в Parcel/Sharp.
Я использую Pug.js так как просто захотел выучить новенькое и он попался первым. О самом Pug'е я в прошлой статье писал, для текущей задачи можно много чего придумать - я решил воспользоваться mixin'ом.
Мы создаём шаблон какого-то куска разметки и передавая в него параметры расставляем их в теле шаблона:
mixin item(name, path, desc)
li
h3 name
img(src=path, alt=name)
p desc
+item('Mr. Puggy', './mr-puggy.png', 'Distinguished gentleman')Сами вызовы миксинов, читай создание элемента по шаблону, можно воткнуть, например, в местный цикл, а данные передавать из объекта:
each item in gallery-items +item(item.name, item.path, item.desc)
Объект, кстати, можно подключить внешний, правда, немного через жопу, то есть через конфиг-файл Pug'а. У конфига .pugrc есть зарезервированный объект locals в который можно пихать свои данные, а в .pug файлах обращаться к самому вложенному объекту, так как locals работает для всей страницы.
// пример .pugrc (или pug.config.js)
{
"locals": {
"gallery-items": [
{
"name": "Mr. Puggy",
"path": "./mr-puggy.png",
"desc": "Distinguished gentleman"
},
{
"name": "Don Fluffy",
"path": "./don-fluffy.png",
"desc": "Mafioso"
}
]
}
}Ах, да! Особенности!
Остались нюансы. С первым я столкнулся в прошлый раз - Pug'овская интерполяция, которая #{foo}, действительно не работает в атрибутах, о чем сказано в документации. Там же в документации, написаны варианты выхода из ситуации – конкатенация прям в атрибуте или использовать ES-ную интерполяцию с бэктиками - `${foo}`.
Я выбрал второй вариант и пока не пожалел - получилось лаконичнее и, на мой взгляд, даже читабельнее чем с ворохом конкатенаций.
mixin item(name, path, desc)
//- контент шаблона
...
picture
source(
srcset=`${path}?as=avif&width=480, ${path}?as=avif 2x`,
media='(min-width: 1280px)',
type='image/avif'
)
...
// остальные sources аналогичноВторой нюанс это экранирование специальных символов в атрибутах. В Pug символы типа <, > и нужный нам амперсанд & по умолчанию экранируются и преобразуются в мнемоники типа >, < и так далее.
Чтобы символы не экранировались достаточно просто поставить ! перед равно при написании атрибута:
source(
srcset!=`${path}?as=avif&width=480, ${path}?as=avif 2x`,
...
)Да, даже подсветка говорит нам, мол это же "НЕравно", но такой вот синтаксис.
По итогу, пару дней покопавшись оказалось, что не так уж всё и страшно с этими картиночками. Вот боевой пример с одной странице, где плитка с портфолио:
Массив portfolio я вынес в locals, получается там такая база данных на минималках. В конкретном примере 12 картинок, так как нет необходимости делать под таблет - по дизайну они просто смещаются сверху вбок от текста.