Да кажем, че имате набор от икони във Figma и проект в React/Vue/каквото и да е друго. Не е изненада, ще трябва да използвате тези икони. Как можем да ги импортираме?
Ако сте достатъчно упорит, можете да ги изтеглите един по един и да ги добавите като активи. Но това е тъжен начин. Не ми харесва.
Нека спрем да сме тъжни и вместо това да бъдем страхотни. Ето какво ще ни трябва:
*без повече шум*
Ще ни трябва Figma токен. Не мисля, че е необходимо да обяснявам как се прави това, така че вместо това има раздел за помощ: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens .
Настройките по подразбиране са достатъчно добри: имаме нужда от достъп само за четене до съдържанието на файла.
Ще използвам иконите за материален дизайн (Общност) като пример.
Ще ни трябва файлов ключ и идентификатор на главния възел:
Сега, когато имаме достатъчно предпоставки, нека започнем да кодираме. Харесвам моите скриптове да са отделени от основния код на проекта, така че ще създам папка scripts
в корена и fetch-icons.ts
в нея. Няма нужда да го правите на TypeScript, но това прави тази статия доста изискана, така че ще се придържам към нея.
Това обаче ще изисква още няколко зависимости:
yarn add -D tsx dotenv axios @figma/rest-api-spec
dotenv
е тук, за да използва променливите .env
, tsx
— за стартиране на TS скрипта (фантазия, помните ли?), @figma/rest-api-spec
е за безопасност на типа, а axios
е просто защото съм свикнал с него. Единственият задължителен е dotenv
(освен ако не сте съгласни с добавянето на API ключа към кода) и ще добавя „чистата“ версия в края. Засега ще имаме тази настройка и ето с какво започваме:
import * as dotenv from "dotenv"; dotenv.config(); const PERSONAL_ACCESS_TOKEN = String( process.env.VITE_FIGMA_PERSONAL_ACCESS_TOKEN ); // I've added my Figma token to the .env file, and so should you const FIGMA_API_URL = "https://api.figma.com/v1"; const FILE_KEY = "v50KJO82W9bBJUppE8intT"; // this one is from the URL const NODE_ID = "2402-2207"; // node-id query param, also from the URL
Първо ще получим метаданните за основния кадър:
import { GetFileNodesResponse, GetImagesResponse, HasChildrenTrait, } from "@figma/rest-api-spec"; // ... const fetchFrameData = async ( nodeId: string ): Promise<HasChildrenTrait> => { const { data } = await axios.get<GetFileNodesResponse>( `${FIGMA_API_URL}/files/${FILE_KEY}/nodes?ids=${nodeId}`, getConfig() ); return data.nodes[nodeId].document as HasChildrenTrait; };
Ще върне информацията за своите деца. Това могат да бъдат изображения (техният тип ще бъде INSTANCE
) или други рамки, които ще анализираме рекурсивно:
const getImageNodes = ( frameData: HasChildrenTrait ): Record<string, string> => { const nodes: Record<string, string> = {}; for (const image of frameData.children.filter( (node) => node.type === 'INSTANCE' )) { // normalizeName simply converts 'check-box' to 'CheckBox' const name = normalizeName(image.name); const id = image.id; nodes[id] = name; } for (const frame of frameData.children.filter( (node) => node.type === 'FRAME' )) { Object.assign(nodes, getImageNodes(frame)); } return nodes; };
И накрая, когато имаме куп идентификатори на изображения, ще извлечем URL адреси на SVG файлове:
const fetchImageUrls = async ( nodeIds: string[] ): Promise<GetImagesResponse> => { const { data } = await axios.get<GetImagesResponse>( `${FIGMA_API_URL}/images/${FILE_KEY}?ids=${nodeIds.join(",")}&format=svg`, getConfig() ); return data; };
И ще завършим с масив от такива обекти:
{ name: 'CheckBox', url: 'https://figma-alpha-api.s3.us-west-2.amazonaws.com/images/7dd5bc31-4f26-4701-b155-7bf7dea45824' }
Както вече се досещате, ще ги изтеглим всички, но това не е най-интересното.
Всеки път, когато извличаме SVG, получаваме нещо подобно:
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M17 6H7C3.69 6 1 8.69 [...] 9 17 9Z" fill="black" /> </svg>
Ще премахнем svg
таговете, тъй като те са идентични за всяка икона (предполагам, че са с еднакъв размер за по-лесно). Но по-интересното е, че ще заменим всички fill=”black”
с контейнер в литерал на шаблон и в резултат на това ще имаме такава функция:
export const IconToggleOn = (color: string) => `<path d="M17 6H7C3.69 [...] 9 17 9Z" fill="${color}"/>`;
Ето как го получаваме:
const cleanupSvg = (data: string): string => { const svgTags = /<\/?svg.*?>/g; const colorFills = /fill=["'].+?["']/g; return data .replace(svgTags, "") .replace(colorFills, 'fill="${color}"') .replace(/\n/g, ""); };
Очевидно няма да е необходимо да заменяте цветовете, ако трябва да импортирате някои лога, но създаването на различна логика за тях ще бъде вашата домашна работа.
Добре, ето нашия план:
Няма да цитирам целия файл — можете да го проверите в GitHub . Нека просто да разгледаме самото изтегляне:
const FILE_BATCH_SIZE = 10; const fileContent: string[] = []; const getPromise = async (item: IconData) => { try { const { data } = await axios.get<string>( item.url, getConfig("image/svg+xml") // returns necessary options and headers ); return { name: item.name, data }; } catch (err) { console.error(err); } }; for (let i = 0; i < iconsData.length; i += FILE_BATCH_SIZE) { const batch = await Promise.allSettled( iconsData.slice(i, i + FILE_BATCH_SIZE).map(getPromise) ); for (const icon of batch) { if (icon?.status === "fulfilled" && icon.value) { const iconName = `Icon${icon.value.name}`; const svgData = cleanupSvg(icon.value.data); fileContent.push( `export const ${iconName} = (color: string) =>\n \`${svgData}\`;\n` ); } } }
Пакетирането е важно тук, защото в противен случай Figma няма да ви хареса, ако решите да изтеглите няколко хиляди файла паралелно.
Ако трябва да получите икона с името, което получавате от бекенда, можете също да създадете речник:
const iconArray: string[] = []; // ... for (const icon of batch) { if (icon?.status === "fulfilled" && icon.value) { const iconName = `Icon${icon.value.name}`; const svgData = cleanupSvg(icon.value.data); fileContent.push( `export const ${iconName} = (color: string) =>\n \`${svgData}\`;\n` ); iconArray.push(iconName); // <-- we added this } } // ... when all files are processed fileContent.push( `\r\nexport const iconDictionary: Record<string, (color: string) => string> = {\r\n ${iconArray.join( ",\r\n " )}\r\n};\r\n` );
Този подход е по-лош, тъй като целият набор ще бъде добавен към пакета, дори ако използвате само една икона чрез речника, но понякога трябва да направите нещо неприятно. Освен това всички \r\n
и интервали са тук, за да зарадват моя ESLint: може да се наложи да коригирате количеството им.
Тук идва последната стъпка; нека направим нов файл:
import path from "path"; import { fileURLToPath } from "url"; // ... const PATH_TO_ICONS = "../src/assets/icons"; const currentPath = fileURLToPath(import.meta.url); const currentDirectory = path.dirname(currentPath); const fileName = "index.ts"; const filePath = path.join( path.resolve(currentDirectory, PATH_TO_ICONS), fileName ); // ... fs.writeFileSync(filePath, fileContent.join(""));
Добра идея е да сортирате иконите след извличане на URL адресите, което е толкова просто, колкото това:
iconsData.sort((a, b) => a.name.localeCompare(b.name));
Сортираният списък ще бъде по-лесен за четене и, което е по-важно, ще имаме четлива разлика, когато получим нови икони. В противен случай редът не е гарантиран и добавянето на една икона може да обърка всичко.
Имаме нужда от компонент, който приема низа на иконата, цвета и размера като подпори и рисува нещо сладко:
import React, { useMemo } from "react"; import { IconSvg } from "../types/IconSvg"; interface IconProps { svg: IconSvg; // type IconSvg = (color: string) => string; color?: string; size?: number | string; } const refine = (x: string | number) => { return typeof x === "number" || !/\D+/.test(x) ? `${x}px` : x; }; const MyIcon: React.FC<IconProps> = ({ svg, color, size, }) => { const iconColor = useMemo(() => color ?? "black", [color]); const refinedSize = useMemo(() => refine(size ?? 24), [size]); const currentIcon = useMemo(() => svg(currentColor), [svg, currentColor]); return ( <svg viewBox="0 0 24 24" // make sure it's the same as in Figma dangerouslySetInnerHTML={{ __html: currentIcon }} style={{ width: refinedSize, minWidth: refinedSize, height: refinedSize, minHeight: refinedSize, }} ></svg> ); }; export default MyIcon;
Вярвам, че е доста лесно, с изключение на прецизиране на размера: когато го внедрих за първи път, Chrome прие числа като ширина/височина CSS свойства, но Firefox не, така че добавих px
. Дори и вече да не е калъф, нека да си остане такъв, какъвто трябва да бъде по стандартите.
И ето автодовършването, което обещах:
Ето как го готвя. Надявам се да ви е полезно.
Недостатъкът на този подход е, че иконите ще бъдат вградени и няма да бъдат кеширани поради това. Въпреки това не мисля, че е голяма работа за няколко байта. Освен това можете да добавите въртене, промяна на цвета при задържане, каквото и да е друго, и ще имате едно малко швейцарско ножче от тези икони.
Освен това ще изисква организиран файл във Figma. Но хей, ако можете да напишете целия този код, можете също така да убедите вашите дизайнери да го подредят.
Можете да намерите пълния код, включително React и Vue компоненти и Node.js версия с по-малко зависимости тук: https://github.com/Smileek/fetch-icons