Веб-разработка
May 30, 2023

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 символы типа <, > и нужный нам амперсанд & по умолчанию экранируются и преобразуются в мнемоники типа &gt;, &lt; и так далее.

Чтобы символы не экранировались достаточно просто поставить ! перед равно при написании атрибута:

source(
  srcset!=`${path}?as=avif&width=480, ${path}?as=avif 2x`, 
  ...
)

Да, даже подсветка говорит нам, мол это же "НЕравно", но такой вот синтаксис.


По итогу, пару дней покопавшись оказалось, что не так уж всё и страшно с этими картиночками. Вот боевой пример с одной странице, где плитка с портфолио:

Массив portfolio я вынес в locals, получается там такая база данных на минималках. В конкретном примере 12 картинок, так как нет необходимости делать под таблет - по дизайну они просто смещаются сверху вбок от текста.

Всё, спасибо за внимание!