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 картинок, так как нет необходимости делать под таблет - по дизайну они просто смещаются сверху вбок от текста.