Создаем React-компоненты иконок с помощью Figma API и SVGR. Часть 1.
Tags: node, javascript, react
Published: 2022-11-16 11:00:40

[Исходники первой части](https://github.com/Sm1t/figma-svgr-icons-part-1)
[Исходники второй части](https://github.com/Sm1t/figma-svgr-icons-part-2)

Не так давно я занимался обновлением API иконок в библиотеке React-компонентов (далее — **ui-kit**). Основной задачей обновления было избавиться от необходимости настраивать svg-лоадер на проекте, которой использует ui-kit (так как иконки просто лежали как svg-файлы в отдельной папке), и добавить иконкам некоторый общий API, например цветов и размеров.

Заодно было решено полностью автоматизировать процесс добавления иконок в ui-kit, так как до этого иконки вручную экспортировались из Figma и добавлялись в ui-kit.

В данной статье мы рассмотрим решение описанных выше задач на отдельном примере.

Статья разделена на две части. **В первой** части мы познакомимся с Figma API, а также напишем автоматизацию (далее — **скрипт**) для загрузки иконок из макетов. **Во второй** — займёмся преобразованием svg-иконок в готовые к использованию React-компоненты с помощью **SVGR**, а также создадим компонент **SvgIcon**, который будет наделять иконки некоторым общим API, и будем использовать его в SVGR Custom template.

Пример работы скрипта, который мы создали в первой части статьи:

![Пример работы скрипта загрузки иконок](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7cohrd3hmd0cofliqtmm.gif)

## О примере

В качестве иконок мы будем использовать общедоступные **Material Design Icons**. Они отлично подходят для примера, так как хорошо организованы: разбиты на группы, есть определенные правила именования.

Прежде всего я [создал копию](https://www.figma.com/file/x2vqkyeGdrL0nnSbLslveL/Material-Design-Icons-(Community)?node-id=0%3A1) иконок [Material Design Icons](https://www.figma.com/community/file/878585965681562011) в Figma.

Нам не нужны все иконки. Будет достаточно несколько небольших групп на одной из страниц. Поэтому я оставил 3 небольшие группы иконок на странице **Filled**: Alert, Content и Editor.

![Копия иконок Material Design Icons в Figma](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i261xeylimw1mtygpwef.png)

В начале статьи указана ссылка на исходники первой части. Чтобы пример работал, после клонирования репозитория вам необходимо создать свою копию иконок Material Design и использовать ваш **personal access token** для Figma API. Тема аутентификации Figma API ещё будет затронута ниже.

## Figma API

Figma API предоставляет доступ для чтения и взаимодействия с файлами Figma. Выполняя HTTP запросы по определённым эндпоинтам с параметрами, можно запрашивать файлы, изображения, версии файлов, пользователей, комментарии и т.д. Подробно ознакомиться с Figma API можно [по ссылке](https://www.figma.com/developers/api).

Для работы с Figma API необходима аутентификация. В этом примере я буду использовать **personal access token**. При выполнении запросов будет достаточно указать его в заголовке **x-figma-token**. Подробнее о способах аутентификации можно узнать [в документации](https://www.figma.com/developers/api#authentication).

Нас интересует [получение информации о файле](https://www.figma.com/developers/api#get-files-endpoint) и [получение информации об изображениях](https://www.figma.com/developers/api#get-images-endpoint).

### Получение информации о файле

Под файлом подразумевается [копия иконок Material Design](https://www.figma.com/file/x2vqkyeGdrL0nnSbLslveL/Material-Design-Icons-(Community)?node-id=0%3A1), созданная мною ранее.
Для начала нам необходимо узнать полный url, по которому мы сможем выполнить **http** запрос для получения информации о файле. Базовый url для Figma API — `https://api.figma.com/`. Затем к нему добавляется эндпоинт нужного типа. Для получения информации о файле используется эндпоинт `GET /v1/files/:key`, где **key** — уникальный идентификатор файла, который можно узнать из ссылки на файл, используя шаблон `https://www.figma.com/file/:key/:title`.
В [нашем случае](https://www.figma.com/file/x2vqkyeGdrL0nnSbLslveL/Material-Design-Icons-(Community)?node-id=0%3A1) это **x2vqkyeGdrL0nnSbLslveL**.

![url файла иконок в Figma](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6o8mj5f3wqswolg2y9n0.png) 

Таким образом, полный url для запроса информации о файле — `https://api.figma.com/v1/files/x2vqkyeGdrL0nnSbLslveL`.
Если мы выполним **GET**-запрос по этому url, то получим информацию о всём файле. Нас же интересуют только иконки на странице **Filled**.

Иконки сгруппированы в отдельных фреймах, и у каждого фрейма есть идентификатор узла **node-id**, который можно узнать из url.

![Идентификатор фрейма с иконками Alert](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3i4n5q9df803sbeftqip.png)

Как мы видим, идентификатор узла с иконками Alert — **6%3A8347**.
**%3A** — это закодированный символ двоеточие ":". Следовательно, идентификатор в незакодированном виде — **6:8347**. Таким же образом мы узнаём идентификаторы для иконок Content и Editor — **6:8877** и **6:9532** соответственно.

Чтобы получить информацию только об этих узлах, необходимо в **query** параметрах запроса указать **ids** — список идентификаторов узлов, о которых мы хотим получить информацию, через запятую.

Также можно добавить параметр **depth=3**, чтобы получить информацию об объектах не глубже третьего уровня вложенности: это сильно уменьшит размер ответа, при этом вся необходимая нам информация будет по-прежнему доступна. Таким образом, финальный url принимает следующий вид:
`https://api.figma.com/v1/files/x2vqkyeGdrL0nnSbLslveL?ids=6:8347,6:8877,6:9532&depth=3`.

Теперь мы можем выполнить GET-запрос.

![Пример запроса информации о файле в Postman](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hxox8g03lqjuk7xj5xh1.png)

> в заголовках не забываем про `x-figma-token`

В ответе нас интересует только поле **components**. Оно содержит объект, ключи которого — идентификаторы узлов, а значения — **metadata**. Узлами в данном случае являются иконки. Идентификаторы мы будем использовать для получения информации об изображениях, а из **metadata** сможем взять **name** — название иконки.

![Пример запроса информации о файле в Postman, компоненты](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0blvyqxvpfzilksyzu7b.png)

Перед тем как мы перейдем к получению информации об изображениях, добавлю несколько слов об используемом подходе получения информации об иконках.

Подход с перечислением идентификаторов узлов с иконками может показаться не совсем удобным. И у него действительно есть свои минусы: таких узлов может быть много, и нужно следить за их актуальностью.

Альтернативой такому подходу может быть загрузка информации о всей странице с иконками без перечисления **ids** - `GET https://api.figma.com/v1/files/x2vqkyeGdrL0nnSbLslveL`. Мы по-прежнему получим всю необходимую информацию в components, и при этом нет необходимости перечислять идентификаторы фреймов с иконками. Но при этом появляется риск получить в components какой-либо сторонний объект, который не является иконкой, и ссылка на его изображение будет вести на пустую страницу, что может приводить к ошибкам. Я лично сталкивался с такими проблемами, и, честно говоря, отладка таких проблем — не самое приятное занятие. Учитывайте это при выборе стратегии получения информации об иконках.

### Получение информации об изображениях

Для получения информации об изображениях используется эндпоинт `GET /v1/images/:key`. В параметрах запроса необходимо указать **ids** — идентификаторы узлов, для которых необходимы изображения — через запятую, а также **format**. Идентификаторы мы можем получить, взяв ключи объекта components из предыдущего запроса, а формат в нашем случае — **svg**.

Для примера запроса возьмём несколько первых идентификаторов: **6:8346**, **6:8344** и **6:8343**

Таким образом, url принимает следующий вид: `https://api.figma.com/v1/images/x2vqkyeGdrL0nnSbLslveL?ids=6:8346,6:8344,6:8343&format=svg`

![Пример запроса информации об изображениях в Postman](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sxfy4duqos9c4cyi0waw.png) 

В ответе мы получим для каждого указанного идентификатора соответствующий **url** для загрузки изображения в указанном формате. Пусть вас не смущает слово изображение: ссылки ведут на страницы с **svg**-иконками.

![Пример перехода по ссылке на изображение](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x8igrmt1o7cnyo614kry.png)

Их мы и будем загружать с помощью скрипта.

## Загрузка иконок

Наш скрипт будет состоять из следующих шагов:
1. запрашиваем информацию об иконках;
2. запрашиваем изображения иконок в формате svg;
3. загружаем svg-исходники иконок;
4. сохраняем иконки во временной папке;
5. делаем временную папку с иконками основной.

Временная папка необходима, чтобы не нарушать целостность основной папки с иконками во время выполнения скрипта на случай, если что-то пойдёт не так.

### Инструменты

Используемая версия **nodejs** - **16.13.0**.
Для выполнения шагов, описанных выше, воспользуемся [Listr](https://github.com/SamVerschueren/listr): он предоставляет удобный способ для выполнения последовательных (и не только) задач, а также делает красивый вывод информации о выполнении каждого шага в терминал.
Для работы с файловой системой будем использовать [fs-extra](https://github.com/jprichardson/node-fs-extra), а для выполнения http-запросов — [axios](https://github.com/axios/axios).

### Настройки

Создадим следующую файловую структуру:

```
scripts/
  icons/
    load-icons.js    // скрипт загрузки иконок
    icons.config.js  // настройки
  figma-api.js       // интерфейс для работы с Figma API
```

В **icons.config.js** вынесем константы, которые будем использовать в скрипте загрузки иконок:

```javascript
module.exports = {
  iconsFileKey: 'x2vqkyeGdrL0nnSbLslveL',
  iconsFolder: 'icons',
  iconsFramesIds: ['6:8347', '6:8877', '6:9532'],
  tempIconsFolder: 'temp_icons',
  personalAccessToken: 'YOUR_PERSONAL_ACCESS_TOKEN',
}
```

Если будете копировать пример, не забудьте заменить **iconsFileKey** и **personalAccessToken**.

> скорее всего, ваш `personal access token` вы захотите хранить в переменных окружения, но для примера допустим, что он хранится так же в конфиге.

### Интерфейс для работы с Figma API

В **figma-api.js** реализуем функцию-конструктор, которая принимает **personal acess token** и возвращает методы для работы с Figma API. В нашем случае нам необходимо только 2 метода: **getFile** и **getImages**. Так как мы выше уже разобрались, какие эндпоинты и с какими параметрами для этого необходимы, нам не составит труда их реализовать.

> для работы с Figma API также есть множество библиотек, которые предоставляют удобный и полноценный api для работы с Figma, например [figma-api](https://github.com/didoo/figma-api)

```javascript
const axios = require('axios');

function FigmaApi({ personalAccessToken }) {
  const instance = axios.create({
    baseURL: 'https://api.figma.com/',
    headers: { 'X-Figma-Token': personalAccessToken },
  });

  return {
    getFile: (fileKey, { ids, ...opts } = {}) =>
      instance
        .get(`/v1/files/${fileKey}`, {
          params: {
            ids: ids?.join(','),
            ...opts,
          }
        })
        .then(res => res.data),
    getImage: (fileKey, { ids, ...opts } = {}) =>
      instance
        .get(`/v1/images/${fileKey}`, {
          params: {
            ids: ids?.join(','),
            ...opts,
          }
        })
        .then(res => res.data),
  }
}

module.exports = {
  FigmaApi,
}
```

### Скрипт загрузки иконок

Давайте разберем каждый шаг отдельно.

**Шаг первый** — запрашиваем информацию об иконках. Нас интересует только объект `components`, поэтому сразу получим его через деструктуризацию:

```javascript
const { components } = await figmaApi.getFile(iconsFileKey, {
  ids: iconsFramesIds,
  depth: 3,
});
```

**Шаг второй** — запрашиваем изображения иконок в формате **svg**. Для этого необходимо передать список идентификаторов иконок, а, как мы помним, ключами `components` и являются эти идентификаторы.

```javascript
const { images } = await figmaApi.getImage(iconsFileKey, {
  ids: Object.keys(components),
  format: 'svg',
});
```

**Шаг третий** — загружаем svg-исходники иконок. Так как **images** — это объект, значениями которого являются ссылки на страницы с иконками, сделать это можно, выполнив обычный **GET**-запрос для каждого значения в объекте **images**:

```javascript
const sources = await Promise.all(
  Object.keys(images).map(id => axios.get(images[id])
);
```

Таким образом, мы получим массив исходников для всех иконок, но уже на следующем шаге нам нужно знать название файла для каждого исходника. Поэтому мы немного модифицируем код, добавив после каждого **GET**-запроса создание объекта с полями **fileName** (используем название иконки) и **source** (исходники):

```javascript
const sources = await Promise.all(
  Object.keys(images).map(
    id => axios.get(images[id]).then(res => ({
      fileName: components[id].name,
      source: res.data,
    }))
  )
);
```

**Шаг четвертый** - сохраняем иконки во временной папке.

```javascript
await Promise.all(
  sources.map(({ fileName, source }) =>
    fs.outputFile(`${tempIconsFolder}/${fileName}.svg`, source)
  )
);
```

**Шаг пятый** — делаем временную папку с иконками основной.

```javascript
await fs.move(tempIconsFolder, iconsFolder, { overwrite: true });
```

Теперь остается собрать все шаги вместе. Каждый шаг мы будем выполнять как отдельные задачи с помощью инструмента **Listr**, о котором я упоминал ранее. Обмениваться информацией между шагами можно с помощью контекста **ctx**. В итоге мы получаем готовый скрипт загрузки иконок:

```javascript
const fs = require('fs-extra');
const axios = require('axios');
const Listr = require('listr');

const { FigmaApi } = require('../figma-api');

const {
  iconsFileKey,
  iconsFolder,
  iconsFramesIds,
  personalAccessToken,
  tempIconsFolder,
} = require('./icons.config')

const figmaApi = new FigmaApi({ personalAccessToken })

const tasks = new Listr([
  {
    title: 'Запрос информации об иконках',
    task: async ctx => {
      const { components } = await figmaApi.getFile(iconsFileKey, {
        ids: iconsFramesIds,,
        depth: 3,
      });
      ctx.components = components;
    }
  },
  {
    title: 'Запрос изображений иконок в формате svg',
    task: async ctx => {
      const { components } = ctx;
      const { images } = await figmaApi.getImage(iconsFileKey, {
        ids: Object.keys(components),
        format: 'svg',
      });
      ctx.images = images;
    }
  },
  {
    title: 'Загрузка svg-исходников иконок',
    task: async ctx => {
      const { components, images } = ctx;
      const sources = await Promise.all(
        Object.keys(images).map(
          id => axios.get(images[id]).then(res => ({
            fileName: components[id].name,
            source: res.data,
          }))
        )
      );
      ctx.sources = sources;
    }
  },
  {
    title: 'Сохранение иконок во временной папке',
    task: async ctx => {
      const { sources } = ctx;
      await Promise.all(
        sources.map(({ fileName, source }) =>
          fs.outputFile(`${tempIconsFolder}/${fileName}.svg`, source)
        )
      );
    }
  },
  {
    title: 'Замена основной папки с иконками временной папкой',
    task: async () => {
      await fs.move(tempIconsFolder, iconsFolder, { overwrite: true });
    }
  },
])

tasks.run().catch(error => {
  console.log(error);
  fs.remove(tempIconsFolder);
});
```

В конце скрипта происходит запуск выполнения задач **tasks.run()** и обработка ошибок: если что-то пошло не так, выводим сообщение об ошибке и не забываем подчистить за собой, удалив временную папку с иконками.

Добавим в **package.json** алиас для запуска скрипта загрузки иконок:
```javascript
"scripts": {
  "load-icons": "node scripts/icons/load-icons"
}
```

Теперь можно вызвать скрипт и понаблюдать за его работой:

![Пример работы скрипта загрузки иконок](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7cohrd3hmd0cofliqtmm.gif) 

## Заключение

Таким образом, у нас получилось полностью автоматизировать добавление иконок в проект.
Скрипт будет отличаться в зависимости от проекта, структуры макетов, договоренностей с дизайнерами и т.д. Также можно заняться его оптимизацией, например добавить режим, в котором уже загруженные иконки не будут загружаться — все в ваших руках.

В следующей части мы преобразуем иконки в готовые к использованию React-компоненты, используя библиотеку SVGR и Custom template. Также мы создадим компонент **SvgIcon** для наделения иконок некоторым API, после чего иконки можно будет использовать следующим образом:

```javascript
import { AddAlert } from 'icons'

const Example = () => {
  return <AddAlert size='large' color='warning' />
}
```