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

sm1t

Vyacheslav Konyshev

Posted on November 16, 2022

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

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

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

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

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

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

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

Пример работы скрипта загрузки иконок

О примере

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

Прежде всего я создал копию иконок Material Design Icons в Figma.

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

Копия иконок Material Design Icons в Figma

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

Figma API

Figma API предоставляет доступ для чтения и взаимодействия с файлами Figma. Выполняя HTTP запросы по определённым эндпоинтам с параметрами, можно запрашивать файлы, изображения, версии файлов, пользователей, комментарии и т.д. Подробно ознакомиться с Figma API можно по ссылке.

Для работы с Figma API необходима аутентификация. В этом примере я буду использовать personal access token. При выполнении запросов будет достаточно указать его в заголовке x-figma-token. Подробнее о способах аутентификации можно узнать в документации.

Нас интересует получение информации о файле и получение информации об изображениях.

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

Под файлом подразумевается копия иконок Material Design, созданная мною ранее.
Для начала нам необходимо узнать полный url, по которому мы сможем выполнить http запрос для получения информации о файле. Базовый url для Figma API — https://api.figma.com/. Затем к нему добавляется эндпоинт нужного типа. Для получения информации о файле используется эндпоинт GET /v1/files/:key, где key — уникальный идентификатор файла, который можно узнать из ссылки на файл, используя шаблон https://www.figma.com/file/:key/:title.
В нашем случае это x2vqkyeGdrL0nnSbLslveL.

url файла иконок в Figma

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

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

Идентификатор фрейма с иконками Alert

Как мы видим, идентификатор узла с иконками 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

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

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

Пример запроса информации о файле в Postman, компоненты

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

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

Альтернативой такому подходу может быть загрузка информации о всей странице с иконками без перечисления 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

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

Пример перехода по ссылке на изображение

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

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

Наш скрипт будет состоять из следующих шагов:

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

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

Инструменты

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

Настройки

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

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

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

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

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

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

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

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

для работы с Figma API также есть множество библиотек, которые предоставляют удобный и полноценный api для работы с Figma, например figma-api

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

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

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

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

const { components } = await figmaApi.getFile(iconsFileKey, {
  ids: iconsFramesIds,
  depth: 3,
});
Enter fullscreen mode Exit fullscreen mode

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

const { images } = await figmaApi.getImage(iconsFileKey, {
  ids: Object.keys(components),
  format: 'svg',
});
Enter fullscreen mode Exit fullscreen mode

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

const sources = await Promise.all(
  Object.keys(images).map(id => axios.get(images[id])
);
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

await fs.move(tempIconsFolder, iconsFolder, { overwrite: true });
Enter fullscreen mode Exit fullscreen mode

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

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

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

Добавим в package.json алиас для запуска скрипта загрузки иконок:

"scripts": {
  "load-icons": "node scripts/icons/load-icons"
}
Enter fullscreen mode Exit fullscreen mode

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

Пример работы скрипта загрузки иконок

Заключение

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

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

import { AddAlert } from 'icons'

const Example = () => {
  return <AddAlert size='large' color='warning' />
}
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
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