paint-brush
Cách nhập biểu tượng từ Figma theo cách thú vịtừ tác giả@smileek
Bài viết mới

Cách nhập biểu tượng từ Figma theo cách thú vị

từ tác giả Andrei Sieedugin8m2025/03/04
Read on Terminal Reader

dài quá đọc không nổi

Hướng dẫn này sẽ chỉ cho bạn cách dễ dàng nhập biểu tượng từ Figma vào dự án React/Vue và tạo thành phần cho phép sử dụng chúng ở nhiều kích cỡ và màu sắc khác nhau.
featured image - Cách nhập biểu tượng từ Figma theo cách thú vị
Andrei Sieedugin HackerNoon profile picture

Giả sử bạn có một bộ biểu tượng trong Figma và một dự án trong React/Vue/bất kỳ thứ gì khác. Không có gì ngạc nhiên, bạn sẽ cần sử dụng các biểu tượng đó. Làm thế nào chúng ta có thể nhập chúng?


Nếu bạn đủ cứng đầu, bạn có thể tải xuống từng cái một và thêm chúng vào như tài sản. Nhưng đó là một cách buồn. Không giống như vậy.


Hãy ngừng buồn và thay vào đó hãy tuyệt vời. Đây là những gì chúng ta cần:

  • Một thành phần tuyệt vời cho phép sử dụng các biểu tượng ở nhiều kích cỡ và màu sắc khác nhau.
  • Tự động hoàn thành theo tên biểu tượng.
  • Cập nhật đơn giản khi có biểu tượng mới được thêm vào.


*không cần giải thích thêm*

Bắt đầu tại đây

Chúng ta sẽ cần một mã thông báo Figma. Tôi không nghĩ cần phải giải thích cách thực hiện việc này, vì vậy đây là phần trợ giúp thay thế: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens .


Thiết lập mặc định khá tốt: chúng ta cần quyền chỉ đọc vào nội dung tệp.



Cho tôi ID của bạn

Tôi sẽ sử dụng Biểu tượng Material Design (Cộng đồng) làm ví dụ.


Chúng ta sẽ cần một khóa tệp và ID nút chính:


Bây giờ chúng ta đã có đủ điều kiện tiên quyết, hãy bắt đầu viết mã. Tôi thích các tập lệnh của mình được tách biệt khỏi mã dự án chính, vì vậy tôi sẽ tạo một thư mục scripts trong thư mục gốc và một fetch-icons.ts trong đó. Không cần phải tạo nó trong TypeScript, nhưng nó làm cho bài viết này trở nên khá lạ mắt, vì vậy tôi sẽ làm theo.


Tuy nhiên, điều này sẽ đòi hỏi thêm một vài sự phụ thuộc nữa:

 yarn add -D tsx dotenv axios @figma/rest-api-spec


dotenv ở đây để sử dụng các biến .env , tsx — để chạy tập lệnh TS (lạ nhỉ, nhớ không?), @figma/rest-api-spec dành cho an toàn kiểu, và axios chỉ vì tôi đã quen với nó. Chỉ cần dotenv (trừ khi bạn đồng ý thêm khóa API vào mã), và tôi sẽ thêm phiên bản "thuần túy" vào cuối. Hiện tại, chúng ta sẽ có thiết lập này, và đây là những gì chúng ta bắt đầu:

 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


Tìm về!

Đầu tiên, chúng ta sẽ lấy siêu dữ liệu cho khung chính:

 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; };


Nó sẽ trả về thông tin về các phần tử con của nó. Chúng có thể là hình ảnh (kiểu của chúng sẽ là INSTANCE ) hoặc các khung khác mà chúng ta sẽ phân tích cú pháp đệ quy:

 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; };


Cuối cùng, khi chúng ta có một loạt ID hình ảnh, chúng ta sẽ lấy URL tệp 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; };


Và chúng ta sẽ có được một mảng các đối tượng như vậy:

 { name: 'CheckBox', url: 'https://figma-alpha-api.s3.us-west-2.amazonaws.com/images/7dd5bc31-4f26-4701-b155-7bf7dea45824' }


Như bạn đã đoán, chúng ta sẽ tải xuống tất cả, nhưng đó không phải là phần thú vị nhất.

Giải phẫu của họ

Mỗi lần chúng ta lấy một SVG, chúng ta sẽ nhận được nội dung tương tự như sau:

 <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>


Chúng ta sẽ xóa các thẻ svg vì chúng giống hệt nhau đối với mỗi biểu tượng (tôi cho rằng chúng có cùng kích thước để đơn giản). Nhưng điều thú vị hơn là chúng ta sẽ thay thế tất cả fill=”black” bằng một trình giữ chỗ trong một mẫu theo nghĩa đen, và kết quả là chúng ta sẽ có một hàm như sau:

 export const IconToggleOn = (color: string) => `<path d="M17 6H7C3.69 [...] 9 17 9Z" fill="${color}"/>`;


Đây là cách chúng tôi có được nó:

 const cleanupSvg = (data: string): string => { const svgTags = /<\/?svg.*?>/g; const colorFills = /fill=["'].+?["']/g; return data .replace(svgTags, "") .replace(colorFills, 'fill="${color}"') .replace(/\n/g, ""); };


Rõ ràng là bạn sẽ không cần phải thay đổi màu sắc nếu bạn cần nhập một số logo, nhưng việc tạo ra logic khác cho chúng sẽ là bài tập về nhà của bạn.

Hãy bắt đầu nào

Được rồi, đây là kế hoạch của chúng tôi:

  1. Xóa tập tin kết quả nếu nó tồn tại.
  2. Lấy các nút biểu tượng từ Figma.
  3. Lấy URL biểu tượng cho các nút đó.
  4. Tải xuống SVG.
  5. Tạo một tệp mới với các chức năng biểu tượng của chúng tôi.


Tôi sẽ không đọc lại toàn bộ tệp—bạn có thể kiểm tra nó trên GitHub . Chúng ta hãy cùng xem xét bản tải xuống:

 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` ); } } }


Việc xử lý theo lô rất quan trọng ở đây vì nếu không, Figma sẽ không thích bạn nếu bạn quyết định tải xuống hàng nghìn tệp cùng lúc.


Nếu bạn cần lấy biểu tượng theo tên bạn nhận được từ phần phụ trợ, bạn cũng có thể tạo một từ điển:

 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` );


Cách tiếp cận này tệ hơn vì toàn bộ bộ sẽ được thêm vào gói ngay cả khi bạn chỉ sử dụng một biểu tượng thông qua từ điển, nhưng đôi khi, bạn phải làm điều gì đó khó chịu. Ngoài ra, tất cả \r\n và khoảng trắng đều ở đây để làm cho ESLint của tôi vui vẻ: bạn có thể cần điều chỉnh số lượng của chúng.


Đây là bước cuối cùng; hãy tạo một tệp mới:

 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(""));


Một điều nhỏ bé

Bạn nên sắp xếp các biểu tượng sau khi lấy URL, cách thực hiện đơn giản như sau:

 iconsData.sort((a, b) => a.name.localeCompare(b.name));


Một danh sách được sắp xếp sẽ dễ đọc hơn và quan trọng hơn là chúng ta sẽ có một diff dễ đọc bất cứ khi nào chúng ta có biểu tượng mới. Nếu không, thứ tự không được đảm bảo và việc thêm một biểu tượng có thể làm hỏng mọi thứ.

Chúng tôi tạo lại tệp, nhưng thứ tự các biểu tượng vẫn giữ nguyên


Bây giờ thì sao?

Chúng ta cần thành phần chấp nhận chuỗi biểu tượng, màu sắc và kích thước làm thuộc tính và vẽ thứ gì đó đẹp mắt:

 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;


Tôi tin rằng nó khá đơn giản, ngoại trừ việc tinh chỉnh kích thước: khi tôi triển khai lần đầu tiên, Chrome chấp nhận các số làm thuộc tính CSS chiều rộng/chiều cao, nhưng Firefox thì không, vì vậy tôi đã thêm px . Ngay cả khi nó không còn là trường hợp nữa, chúng ta hãy giữ nguyên như nó được cho là theo tiêu chuẩn.


Và đây là tính năng tự động hoàn thành mà tôi đã hứa:

Vậy là hết rồi các bạn ạ

Đây là cách tôi nấu món này. Tôi hy vọng bạn thấy nó hữu ích.


Nhược điểm của cách tiếp cận này là các biểu tượng sẽ được nhúng và không được lưu vào bộ nhớ đệm vì lý do đó. Tuy nhiên, tôi không nghĩ đó là vấn đề lớn đối với một vài byte. Hơn nữa, bạn có thể thêm xoay, thay đổi màu khi di chuột, bất cứ điều gì khác và bạn sẽ có một con dao quân đội Thụy Sĩ nhỏ của các biểu tượng đó.


Ngoài ra, nó sẽ yêu cầu một tệp được sắp xếp trong Figma. Nhưng này, nếu bạn có thể viết tất cả mã này, bạn cũng có thể thuyết phục các nhà thiết kế của mình sắp xếp nó.


Bạn có thể tìm thấy mã đầy đủ, bao gồm các thành phần React và Vue, và phiên bản Node.js với ít phụ thuộc hơn tại đây: https://github.com/Smileek/fetch-icons