Создаем React-компоненты иконок с помощью Figma API и SVGR. Часть 2.

sm1t

Vyacheslav Konyshev

Posted on November 16, 2022

Создаем React-компоненты иконок с помощью Figma API и SVGR. Часть 2.

Исходники первой части
Исходники второй части

В первой части мы автоматизировали загрузку 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',
  ...
}
Enter fullscreen mode Exit fullscreen mode

Теперь можем приступать к созданию компонентов иконок. Добавим @svgr/cli в наш проект.

yarn add --dev @svgr/cli
Enter fullscreen mode Exit fullscreen mode

Затем создадим конфигурационный файл svgr.config.js и укажем outDir, куда мы хотим сохранять компоненты:

module.exports = {
  outDir: 'icons',
}
Enter fullscreen mode Exit fullscreen mode

После чего мы уже можем запустить команду:

yarn svgr icons_sources
Enter fullscreen mode Exit fullscreen mode

И сразу получим набор 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',
  },
}
Enter fullscreen mode Exit fullscreen mode

цвет наших иконок мы можем подсмотреть в макетах или в одной из загруженных 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,
          },
        },
      },
    ],
  },
}
Enter fullscreen mode Exit fullscreen mode

В-третьих, мы можем удалить необязательный атрибут xmlns. Для этого в плагины SVGO добавим removeXMLNS:

svgoConfig: {
  plugins: [
    ...
    'removeXMLNS',
  ],
},
Enter fullscreen mode Exit fullscreen mode

Это все настройки, которые нам необходимы на текущий момент. Давайте выполним команду yarn svgr icons_sources и убедимся, что в компонентах используется currentColor, viewBox и отсутствует xmlns:

Пример компонента после настроек SVGR

Чтобы в дальнейшем выполнять загрузку иконок из figma и их преобразование в React-компоненты за одну команду, можно добавить в package.json соответствующий скрипт:

// package.json
...
"scripts": {
  ...
  "icons": "yarn load-icons && svgr icons_sources"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

После чего вызываем команду для добавления Storybook:

npx sb init
Enter fullscreen mode Exit fullscreen mode

После того как эта команда отработает, в проект добавится папка 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} />
    ))}
  </>
);
Enter fullscreen mode Exit fullscreen mode

Второй, icons.storis.mdx, — страница с отображением иконок:

// stories/Icons.stories.mdx
import { Meta } from '@storybook/addon-docs';
import { Icons } from './Icons';

<Meta title="Icons" />

## Иконки

<Icons />
Enter fullscreen mode Exit fullscreen mode

После чего можно запустить Storybook:

yarn storybook
Enter fullscreen mode Exit fullscreen mode

Теперь мы видим, что компоненты иконок отлично работают:

Иконки в Storybook

API иконок

Как упоминалось ранее, мы добавим для иконок возможность указать цвет и размер. На самом деле мы уже можем управлять цветом и размером наших компонентов-иконок, так как SVGR по умолчанию пробрасывает все переданные параметры на корневой элемент, а о наследовании цвета и масштабировании мы уже позаботились при преобразовании в компоненты:

Передача пропсов в компоненте

Мы можем в этом убедиться, добавив атрибуты color, height и width при создании элементов в stories/Icons.js:

// stories/Icons.js
...
<Icon key={index} color="orange" height={48} width={48} />
Enter fullscreen mode Exit fullscreen mode

После чего мы увидим соответствующие изменения в Storybook:

Иконки в 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};
  `
  }
}
Enter fullscreen mode Exit fullscreen mode

После выполнения команды yarn svgr icons_sources мы увидим, что наши иконки приняли новый облик:

Скриншот с иконкой Add.js

Теперь можно приступать к реализации компонента SvgIcon.

SvgIcon

Прежде всего давайте определимся с цветами и размерами, которые мы будем предоставлять. Для примера возьмем три цвета из палитры цветов Material: Error, Warning и Info:

Цвета Material

А также два размера из Meterial Icons: small и large, значения которых 20x20 и 35x35 соответственно.

В итоге мы получим следующие наборы цветов и размеров:

const colors = {
  error: '#ef5350',
  info: '#03a9f4',
  warning: '#ff9800',
}

const sizes = {
  'small': 20,
  'large': 35,
}
Enter fullscreen mode Exit fullscreen mode

Как мы уже обсуждали выше, по умолчанию в корневой элемент компонента передаются следующие параметры:

  • 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

Компонент 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>
  );
});

Enter fullscreen mode Exit fullscreen mode

Для того чтобы посмотреть, как работает 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({});
Enter fullscreen mode Exit fullscreen mode

Storybook автоматически создает элементы управления для аргументов на основе PropTypes или типов TypeScript. Давайте добавим PropTypes для нашего компонента.

yarn add prop-types
Enter fullscreen mode Exit fullscreen mode
// SvgIcon/SvgIcon.js
import { node, oneOf } from 'prop-types';
...

SvgIcon.propTypes = {
  children: node,
  color: oneOf(Object.keys(colors)),
  size: oneOf(Object.keys(sizes)),
};
Enter fullscreen mode Exit fullscreen mode

Теперь мы сможем посмотреть на работу компонента SvgIcon в storybook:

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' />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Пример использования иконок

Заключение

Таким образом, объединив скрипт загрузки иконки из первой части и преобразование svg-иконок в React-компоненты из второй, мы можем полностью автоматизировать процесс от иконки в Figma до готового к использованию компонента, наделённого некоторым стандартизированным API.

Как уже упоминалось в первой части, скрипт и настройки инструментов могут и будут отличаться в зависимости от проекта. Не всегда достичь полной автоматизации будет так же просто, как в примере из текущей статьи. Автоматизация требует достаточного уровня согласованности, соблюдения договорённостей между разработчиками и дизайнерами, а также хорошей структуры макетов и др. Но если хорошо постараться, то весь процесс можно будет свести к запуску одной команды.

💖 💪 🙅 🚩
sm1t
Vyacheslav Konyshev

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