Создаем React-компоненты иконок с помощью Figma API и SVGR. Часть 2.
Vyacheslav Konyshev
Posted on November 16, 2022
Исходники первой части
Исходники второй части
В первой части мы автоматизировали загрузку svg иконок из Figma. Теперь нам предстоит преобразовать их в готовые к использованию React-компоненты и наделить некоторым API, например цвета и размеры. Для этого мы будем использовать SVGR и его расширенные возможности. Мы будем дорабатывать скрипт из первой части и в итоге полностью автоматизируем процесс от нарисованной иконки в Figma до готового React-компонента.
SVGR
Думаю SVGR не нуждается в представлении: по поисковому запросу "convert svg to react" в Google первым результатом будет SVGR. А всем известный create-react-app использует @svgr/webpack.
SVGR предоставляет CLI с опцией --out-dir
, которая позволяет преобразовать папку целиком, а также возможность кастомизировать шаблон, благодаря чему мы сможем наделить наши иконки некоторым API.
Помимо этого SVGR использует SVGO для оптимизации кода SVG перед преобразованием его в компонент и Prettier для форматирования JavaScript. Всё это, конечно, можно настраивать. У этого инструмента ещё много возможностей и достоинств. Узнать обо всём подробнее вы сможете в документации.
Мы будем использовать SVGR версии 6.2.1, которая является последней на момент написания статьи. С выходом шестой версии было несколько важных обновлений, ознакомиться с которыми можно в гайде по миграции.
Конвертация svg-иконок в React-компоненты
Начнём с небольшого обновления icons.config.js. Хотелось бы видеть итоговые React-компоненты в папке icons, в которую сейчас загружаются svg-исходники. Давайте это изменим и будем загружать исходники иконок в папку icons_sources. Для этого обновим iconsFolder в нашем конфиге icons.config.js:
module.exports = {
...
iconsFolder: 'icons_sources',
...
}
Теперь можем приступать к созданию компонентов иконок. Добавим @svgr/cli в наш проект.
yarn add --dev @svgr/cli
Затем создадим конфигурационный файл svgr.config.js и укажем outDir, куда мы хотим сохранять компоненты:
module.exports = {
outDir: 'icons',
}
После чего мы уже можем запустить команду:
yarn svgr icons_sources
И сразу получим набор React-компонентов:
Более того, SVGR автоматически сгенерировал файл index.js с реэкспортами всех компонентов. Как мы видим, SVGR по умолчанию приводит названия компонентов к формату PascalCase, например для иконки add_alert.svg компонент AddAlert.js соответственно.
Так как в будущем мы планируем добавить возможность управлять цветом и размером иконок, давайте сразу позаботимся об этом, добавив некоторые настройки в конфиг SVGR.
Во-первых, необходимо заменить цвет заливки по умолчанию на currentColor, чтобы в дальнейшем можно было управлять цветом иконки через css-свойство color для элемента svg. SVGR предоставляет возможность модифицировать атрибуты перед преобразованием с помощью опции replaceAttrValues. Давайте воспользуемся этой возможностью,:
module.exports = {
outDir: 'icons',
replaceAttrValues: {
'#323232': 'currentColor',
},
}
цвет наших иконок мы можем подсмотреть в макетах или в одной из загруженных svg-иконок — в нашем случае это #323232
Во-вторых, нам нужно, чтобы атрибут viewBox остался после преобразования. Сейчас он удаляется, так как в дефолтном SVGO-пресете включен плагин removeViewBox: viewBox удаляется в том случае, если он соответствует значениям атрибутов ширины и высоты. Иконки Material как раз попадают под это правило.
Настройки SVGO мы можем указать с помощью опции svgoConfig. Согласно документации SVGO, мы можем настраивать плагины с помощью параметра overrides. Нам необходимо отключить плагин removeViewBox, поэтому настройки будут выглядеть следующим образом:
module.exports = {
outDir: 'icons',
replaceAttrValues: {
'#323232': 'currentColor',
},
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
},
},
},
],
},
}
В-третьих, мы можем удалить необязательный атрибут xmlns. Для этого в плагины SVGO добавим removeXMLNS:
svgoConfig: {
plugins: [
...
'removeXMLNS',
],
},
Это все настройки, которые нам необходимы на текущий момент. Давайте выполним команду yarn svgr icons_sources
и убедимся, что в компонентах используется currentColor, viewBox и отсутствует xmlns:
Чтобы в дальнейшем выполнять загрузку иконок из figma и их преобразование в React-компоненты за одну команду, можно добавить в package.json соответствующий скрипт:
// package.json
...
"scripts": {
...
"icons": "yarn load-icons && svgr icons_sources"
Storybook
Чтобы проверить компоненты в действии, предлагаю подключить Storybook. Им также будет удобно пользоваться при разработке API для иконок. Не будем отвлекаться на описание этого инструмента: думаю, многие с ним знакомы. А если нет, рекомендую срочно это исправить.
Storybook при установке смотрит в зависимости и определяет конфигурацию, поэтому давайте добавим в зависимости react и react-dom. Так как наш пример — это "библиотека" иконок, то react и react-dom устанавливаем в peerDependencies, а чтобы работал Storybook, дублируем в devDependencies:
yarn add --peer react react-dom
// and
yarn add --dev react react-dom
После чего вызываем команду для добавления Storybook:
npx sb init
После того как эта команда отработает, в проект добавится папка stories с примерами по умолчанию.
Удалим всё содержимое папки stories и добавим два новых файла. Первый — Icons.js, в котором импортируем все иконки и рендерим их в компоненте Icons:
// stories/Icons.js
import React from 'react';
import * as icons from '../icons';
export const Icons = () => (
<>
{Object.values(icons).map((Icon, index) => (
<Icon key={index} />
))}
</>
);
Второй, icons.storis.mdx, — страница с отображением иконок:
// stories/Icons.stories.mdx
import { Meta } from '@storybook/addon-docs';
import { Icons } from './Icons';
<Meta title="Icons" />
## Иконки
<Icons />
После чего можно запустить Storybook:
yarn storybook
Теперь мы видим, что компоненты иконок отлично работают:
API иконок
Как упоминалось ранее, мы добавим для иконок возможность указать цвет и размер. На самом деле мы уже можем управлять цветом и размером наших компонентов-иконок, так как SVGR по умолчанию пробрасывает все переданные параметры на корневой элемент, а о наследовании цвета и масштабировании мы уже позаботились при преобразовании в компоненты:
Мы можем в этом убедиться, добавив атрибуты color, height и width при создании элементов в stories/Icons.js:
// stories/Icons.js
...
<Icon key={index} color="orange" height={48} width={48} />
После чего мы увидим соответствующие изменения в Storybook:
Но просто возможность указать любой цвет и размер не всегда то, что нужно. Чаще всего необходимо предоставить некоторый стандартизированный набор цветов и размеров. Для этого нам нужно получить контроль над конечным набором параметров для svg.
SVGR Custom Template
Как мы видим из примеров выше, по умолчанию у компонентов корневой элемент — svg, для которого указаны параметры по умолчанию (из svg-исходников иконки), а через {...props}
прокидываются все остальные параметры. Но если заменить корневой элемент на некоторый компонент SvgIcon, то в этом компоненте мы получим доступ ко всем входящим параметрам и сможем управлять финальным набором параметров для svg.
Данный подход с SvgIcon используется в Material
SVGR предоставляет возможность задать шаблон, который будет использоваться при преобразовании иконок в компоненты — Custom Templates. С помощью этой возможности мы заменим корневой элемент компонентов на компонент SvgIcon, который мы реализуем чуть позже. Для этого добавим наш custom template в svgr.config.js:
const { types } = require('@babel/core');
module.exports = {
...
template: function svgrCustomTemplate(
{ imports, interfaces, componentName, props, jsx, exports },
{ tpl }
) {
// меняем корневой элемент на SvgIcon
jsx.openingElement.name.name = 'SvgIcon';
jsx.closingElement.name.name = 'SvgIcon';
// https://github.com/gregberge/svgr/issues/530
// при изменении корневого элемента пропадает спред пропсов
// поэтому необходимо добавить спред пропсов самостоятельно
jsx.openingElement.attributes.push(
types.jSXSpreadAttribute(types.identifier('props'))
);
return tpl`
${imports};
import { SvgIcon } from '../SvgIcon';
${interfaces};
const ${componentName} = (${props}) => (
${jsx}
);
${exports};
`
}
}
После выполнения команды yarn svgr icons_sources
мы увидим, что наши иконки приняли новый облик:
Теперь можно приступать к реализации компонента SvgIcon.
SvgIcon
Прежде всего давайте определимся с цветами и размерами, которые мы будем предоставлять. Для примера возьмем три цвета из палитры цветов Material: Error, Warning и Info:
А также два размера из Meterial Icons: small и large, значения которых 20x20 и 35x35 соответственно.
В итоге мы получим следующие наборы цветов и размеров:
const colors = {
error: '#ef5350',
info: '#03a9f4',
warning: '#ff9800',
}
const sizes = {
'small': 20,
'large': 35,
}
Как мы уже обсуждали выше, по умолчанию в корневой элемент компонента передаются следующие параметры:
- width, height и viewBox — размеры и viewBox по умолчанию
- children — содержимое svg-элемента: path, clipPath и др.
- {...props} — все остальные параметры
Чтобы наделить наши иконки API цветов и размеров, нам необходимо на стороне компонента SvgIcon реализовать поддержку параметров color и size, с помощью которых и будет происходить управление размером и цветом.
Следовательно, в компоненте SvgIcon мы делаем следующее:
- получаем с помощью деструктуризации параметры width, height, children, color и size
- все остальные параметры собираем в
...props
- возвращаем svg-элемент, в который первым делом пробрасываем все параметры
...props
, а значения color, height и width определяем на основе параметров color и size - пробрасываем children в svg-элемент.
Для простоты примера будем указывать стили с помощью атрибутов.
export const SvgIcon = ({
children, color, height, size, width, ...props
}) => {
return (
<svg
{...props}
color={colors[color] || color}
height={sizes[size] || height}
width={sizes[size] || width}
>
{children}
</svg>
);
};
Компонент SvgIcon в итоге выглядит следующим образом:
// SvgIcon/SvgIcon.js
import React, { forwardRef } from 'react';
import { node, oneOf } from 'prop-types';
const colors = {
error: '#ef5350',
info: '#03a9f4',
warning: '#ff9800',
}
const sizes = {
'small': 20,
'large': 35,
}
export const SvgIcon = forwardRef(function SvgIcon(
{ children, color, height, size, width, ...props },
ref
) {
return (
<svg
{...props}
color={colors[color] || color}
height={sizes[size] || height}
width={sizes[size] || width}
ref={ref}
>
{children}
</svg>
);
});
Для того чтобы посмотреть, как работает SvgIcon компонент, давайте добавим для него историю в storybook.
// stories/SvgIcon.stories.js
import React from 'react';
import { SvgIcon } from '../SvgIcon';
export default {
title: 'SvgIcon',
component: SvgIcon,
};
const Template = (args) => (
<SvgIcon width={24} height={24} viewBox="0 0 24 24" {...args}>
<path d="M19 13H13V19H11V13H5V11H11V5H13V11H19V13Z" fill="currentColor"/>
</SvgIcon>
);
export const Playground = Template.bind({});
Storybook автоматически создает элементы управления для аргументов на основе PropTypes или типов TypeScript. Давайте добавим PropTypes для нашего компонента.
yarn add prop-types
// SvgIcon/SvgIcon.js
import { node, oneOf } from 'prop-types';
...
SvgIcon.propTypes = {
children: node,
color: oneOf(Object.keys(colors)),
size: oneOf(Object.keys(sizes)),
};
Теперь мы сможем посмотреть на работу компонента SvgIcon в storybook:
И таким API будет наделён каждый компонент-иконка. Теперь мы можем использовать иконки следующим образом:
import { AddAlert, Warning, ErrorOutline } from '../icons';
const Example = () => {
return (
<>
<AddAlert size='large' color='info' />
<Warning color='warning' />
<ErrorOutline size='small' color='red' />
</>
)
}
Заключение
Таким образом, объединив скрипт загрузки иконки из первой части и преобразование svg-иконок в React-компоненты из второй, мы можем полностью автоматизировать процесс от иконки в Figma до готового к использованию компонента, наделённого некоторым стандартизированным API.
Как уже упоминалось в первой части, скрипт и настройки инструментов могут и будут отличаться в зависимости от проекта. Не всегда достичь полной автоматизации будет так же просто, как в примере из текущей статьи. Автоматизация требует достаточного уровня согласованности, соблюдения договорённостей между разработчиками и дизайнерами, а также хорошей структуры макетов и др. Но если хорошо постараться, то весь процесс можно будет свести к запуску одной команды.
Posted on November 16, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024