Исходники первой части
Исходники второй части
В первой части мы автоматизировали загрузку 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.
Как уже упоминалось в первой части, скрипт и настройки инструментов могут и будут отличаться в зависимости от проекта. Не всегда достичь полной автоматизации будет так же просто, как в примере из текущей статьи. Автоматизация требует достаточного уровня согласованности, соблюдения договорённостей между разработчиками и дизайнерами, а также хорошей структуры макетов и др. Но если хорошо постараться, то весь процесс можно будет свести к запуску одной команды.
Top comments (0)