
Всем привет! Сегодня мы рассмотрим один из вариантов интеграции svg иконок в наш фронтенд проект используя веб-компоненты. Основная идея компонента заключается в том, чтобы лениво подгружать в SVG спрайт иконки и переиспользовать уже загруженные иконки при необходимости. Сами иконки будем вставлять в разметке в виде <svg-icon name="arrow-angle-down"> нам понадобится всего сотня строк кода! Кому интересна реализация, прошу под кат!
Для тех, кому лень читать и хочется сразу посмотреть на весь листинг кода — прошу в репозиторий svg-icon. Для начала просто проговорим логику работы веб-компонента:
-
Получаем атрибут name у компонента и проверяем его наличие в уже существующем спрайте иконок на странице
-
Если такой иконки нету, то проверяем не загружается ли эта иконка в данный момент
-
Если такая иконка не загружается, загружаем ее и добавляем в спрайт.
Все достаточно просто. Для начала добавим в наш шаблон проекта пустой спрайт
<div id="SVG_SPRITE" style="display: none;"> <svg><defs></defs></svg> </div>
На этом этапе, можно сделать ремарку, что для еще большей оптимизации процесса, мы могли бы сюда отрендерить в defs сразу наши иконки, что позволит нам не подгружать их по сети уже на клиенте. Реализация этого остается на плечах разработчика, как пример могу предложить использовать vite vite-plugin-svg-prite.
Теперь рассмотрим саму реализацию по частям
class SVGIcon extends HTMLElement { //ID спрайт элемента #SPRITE_ID = 'SVG_SPRITE'; //Публичный путь по которому будем загружать иконки #ICONS_PATH = '/icons'; constructor() { super(); } //Проверяем наличие атрибута name connectedCallback() { const iconName = this.getAttribute('name'); if(iconName) { this.#loadIcon(iconName); } else { console.error('svg-icon undefined attr name'); } } ...
Здесь все очень просто, мы выносим в приватные свойства ID элемента для спрайта и публичный путь к иконкам на сервере, при срабатывании хука connectedCallback проверяем атбрут name и запускаем приватный метод #loadIcon.
Теперь перейдем непосредственно к логике загрузки
... /** * Загружаем или переиспользуем иконку * @param iconName name of icon */ #loadIcon(iconName:string) { //Сохраняем ссылку на спрайт для сокращения обращений к DOM const spriteEl = document.getElementById(this.#SPRITE_ID); //Проверяем есть ли спрайт if(spriteEl === null) { return console.error('svg-icon undefined sprite element'); } //Пытаемся выбрать иконку из спрайта let icon = spriteEl.querySelector(`[id="${iconName}.svg"]`); //Если иконки нету, загружаем ее if(!icon) { //Проверяем наличие кеш объекта для промисов if(!window[this.#SPRITE_ID]) { window[this.#SPRITE_ID] = {}; } //Проверяем есть ли уже промис на загрузку иконки if(window[this.#SPRITE_ID][iconName]) { //Если есть, ожидаем его выполнения window[this.#SPRITE_ID][iconName].then((iconSvg:string) => { if(iconSvg) { this.#addIconInSprite(iconSvg, iconName, spriteEl); icon = spriteEl.querySelector(`[id="${iconName}.svg"]`); } else { console.error(`svg-icon ${iconName} response undefined`); } }); } else { //Если промиса нету, запускаем fetch иконки window[this.#SPRITE_ID][iconName] = fetch(`${this.#ICONS_PATH}/${iconName}.svg`).then( async (response) => { const iconSvg = await response?.text(); if(iconSvg) { this.#addIconInSprite(iconSvg, iconName, spriteEl); icon = spriteEl.querySelector(`[id="${iconName}.svg"]`); } else { console.error(`svg-icon ${iconName} response undefined`); } return iconSvg; }).catch(err => console.error('svg-icon fetch err', err)); } //Если иконка есть, создаем свг елемент с ее содержимым } const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), useElement = document.createElementNS('http://www.w3.org/2000/svg', 'use'); useElement.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', `#${iconName}.svg`); svg.append(useElement); this.appendChild(svg); } ...
Логика загрузки иконки тоже весьма тривиальная, мы проверяем ее наличие в SVG спрайте, если иконка есть, дублируем ее в компонент, а если нет, то проверяем наличие промиса загрузки иконки чтобы не дублировать сетевые запросы, если промиса нету, то все-таки загружаем иконки по сети. Осталось рассмотреть последний метод, добавления иконки в спрайт
... /** * Добавляем иконку в svg спрайт * @param svgContent svg text content from response * @param iconName name of icon */ #addIconInSprite(svgContent:string, iconName:string, spriteEl:HTMLElement) { //Создаем template и добавляем в него полученный свг контент const tmp = document.createElement('template'); tmp.innerHTML = svgContent; //Выделяем только svg елемент из полученного контента const tmpSvg = tmp.content.querySelector('svg'), symbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol'); if(tmpSvg) { //Копируем аттрибуты из оригинального свг symbol.setAttribute('id', iconName + '.svg'); if(null !== tmpSvg.getAttribute('viewBox')) { symbol.setAttribute('viewBox', tmpSvg.getAttribute('viewBox')); } if(null !== tmpSvg.getAttribute('fill')) { symbol.setAttribute('fill', tmpSvg.getAttribute('fill')); } symbol.innerHTML = tmpSvg.innerHTML; } else { console.error(`svg-icon not found svg content for ${iconName}`); } const spriteDefs = spriteEl.querySelector('defs'); if(spriteDefs) { spriteDefs.append(symbol); } }
Некоторые могут спросить, зачем создавать template, выбирать из него SVG ведь мы и так загружаем SVG — ответ прост, SVG контент может содержать комментарии, а нас интересует только конкретно SVG элемент. Также ради оптимизации обращений к DOM функция принимает в аргументах ссылку на SVG спрайт который мы нашли в предыдущем методе #loadIcon. Также мы копируем атрибуты viewBox и fill для сохранения размера и цвета оригинальной иконки. Дальнейшее перекрашивание иконок и изменение размера нам доступно через CSS. Осталось лишь объявить компонент свг иконок
customElements.define('svg-icon', SVGIcon);
Вот и все, в оригинале всего 113 строк кода с комментариями в гите) Какие плюсы мы имеем в итоге?
-
Веб-компонент не привязан к сборщику, можно просто складировать иконки в папке проекта
-
Веб-компонент не привязан к фреймворку, будет одинаково хорошо дружить с vue\angular\svelte\react\etc.. любых версий
-
Из-за первых двух пунктов наш сборщик фронта работает чуточку быстрее.
-
Веб-компонент не имеет внешних зависимостей в целом.
Буду рад конструктивной критике, код ревью и предложениям по улучшению и оптимизации веб-компонента.
ссылка на оригинал статьи https://habr.com/ru/articles/819905/
Добавить комментарий