设计到代码的自动化流程:从 Figma 到前端组件的现代工作流
设计稿更新了,开发要手动同步颜色值;图标改了,需要重新导出 SVG;组件调整了,代码要跟着改。这些重复劳动不仅消耗时间,还容易出错。
设计到代码的自动化,就是用工具和流程解决这些问题,让设计变更自动同步到代码。
自动化的核心环节
设计工具 (Figma)
│
├─→ 设计令牌 (Tokens)
│ │
│ └─→ CSS 变量 / Tailwind 配置 / SCSS 变量
│
├─→ 图标资产 (Icons)
│ │
│ └─→ SVG 组件 / Icon Font / Sprite
│
├─→ 图片资产 (Images)
│ │
│ └─→ 优化后的图片 / 多尺寸图片
│
└─→ 组件规格 (Specs)
│
└─→ 组件文档 / 类型定义
设计令牌自动化
使用 Tokens Studio (原 Figma Tokens)
Tokens Studio 是 Figma 最流行的令牌管理插件。
// tokens.json - Tokens Studio 导出格式
{
"global": {
"colors": {
"blue": {
"50": { "value": "#eff6ff", "type": "color" },
"500": { "value": "#3b82f6", "type": "color" },
"900": { "value": "#1e3a8a", "type": "color" }
},
"gray": {
"50": { "value": "#f9fafb", "type": "color" },
"500": { "value": "#6b7280", "type": "color" },
"900": { "value": "#111827", "type": "color" }
}
},
"spacing": {
"xs": { "value": "4px", "type": "spacing" },
"sm": { "value": "8px", "type": "spacing" },
"md": { "value": "16px", "type": "spacing" },
"lg": { "value": "24px", "type": "spacing" },
"xl": { "value": "32px", "type": "spacing" }
},
"borderRadius": {
"sm": { "value": "4px", "type": "borderRadius" },
"md": { "value": "8px", "type": "borderRadius" },
"lg": { "value": "12px", "type": "borderRadius" },
"full": { "value": "9999px", "type": "borderRadius" }
}
},
"semantic": {
"colors": {
"primary": { "value": "{global.colors.blue.500}", "type": "color" },
"text-primary": { "value": "{global.colors.gray.900}", "type": "color" },
"text-secondary": { "value": "{global.colors.gray.500}", "type": "color" },
"background": { "value": "{global.colors.gray.50}", "type": "color" }
}
}
}
Style Dictionary 转换
Style Dictionary 是 Amazon 开源的令牌转换工具。
// config.js - Style Dictionary 配置
module.exports = {
source: ['tokens/**/*.json'],
platforms: {
// CSS 变量
css: {
transformGroup: 'css',
buildPath: 'build/css/',
files: [{
destination: 'variables.css',
format: 'css/variables',
options: {
outputReferences: true,
},
}],
},
// SCSS 变量
scss: {
transformGroup: 'scss',
buildPath: 'build/scss/',
files: [{
destination: '_variables.scss',
format: 'scss/variables',
}],
},
// JavaScript/TypeScript
js: {
transformGroup: 'js',
buildPath: 'build/js/',
files: [{
destination: 'tokens.js',
format: 'javascript/es6',
}, {
destination: 'tokens.d.ts',
format: 'typescript/es6-declarations',
}],
},
// Tailwind CSS
tailwind: {
transformGroup: 'js',
buildPath: 'build/',
files: [{
destination: 'tailwind.tokens.js',
format: 'javascript/tailwind',
}],
},
},
};
自定义 Tailwind 格式
// formats/tailwind.js
const StyleDictionary = require('style-dictionary');
StyleDictionary.registerFormat({
name: 'javascript/tailwind',
formatter: function({ dictionary }) {
const tokens = {
colors: {},
spacing: {},
borderRadius: {},
fontSize: {},
fontFamily: {},
boxShadow: {},
};
dictionary.allProperties.forEach(prop => {
const category = prop.attributes.category;
const name = prop.name.replace(`${category}-`, '');
if (tokens[category]) {
setNestedValue(tokens[category], name, prop.value);
}
});
return `module.exports = ${JSON.stringify(tokens, null, 2)}`;
},
});
function setNestedValue(obj, path, value) {
const keys = path.split('-');
let current = obj;
keys.forEach((key, index) => {
if (index === keys.length - 1) {
current[key] = value;
} else {
current[key] = current[key] || {};
current = current[key];
}
});
}
CI/CD 集成
# .github/workflows/sync-tokens.yml
name: Sync Design Tokens
on:
repository_dispatch:
types: [figma-tokens-updated]
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Pull tokens from Figma
run: npx token-transformer tokens.json build/tokens.json
env:
FIGMA_TOKEN: ${{ secrets.FIGMA_TOKEN }}
- name: Build tokens
run: npx style-dictionary build
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
title: 'chore: sync design tokens'
commit-message: 'chore: sync design tokens from Figma'
branch: sync-tokens
body: |
自动同步来自 Figma 的设计令牌更新。
请检查以下变更:
- CSS 变量
- Tailwind 配置
- TypeScript 类型
图标资产自动化
Figma API 导出图标
// scripts/export-icons.js
const Figma = require('figma-api');
const fs = require('fs');
const path = require('path');
const { optimize } = require('svgo');
const figma = new Figma.Api({
personalAccessToken: process.env.FIGMA_TOKEN,
});
async function exportIcons() {
const fileId = process.env.FIGMA_FILE_ID;
const iconsPageName = 'Icons';
// 获取文件结构
const file = await figma.getFile(fileId);
// 找到图标页面
const iconsPage = file.document.children.find(
page => page.name === iconsPageName
);
if (!iconsPage) {
throw new Error(`Page "${iconsPageName}" not found`);
}
// 收集所有图标组件
const icons = [];
collectIcons(iconsPage, icons);
console.log(`Found ${icons.length} icons`);
// 批量获取 SVG
const iconIds = icons.map(i => i.id);
const images = await figma.getImage(fileId, {
ids: iconIds.join(','),
format: 'svg',
});
// 下载并优化 SVG
for (const icon of icons) {
const svgUrl = images.images[icon.id];
if (!svgUrl) continue;
const response = await fetch(svgUrl);
let svg = await response.text();
// SVGO 优化
const optimized = optimize(svg, {
plugins: [
'removeDoctype',
'removeComments',
'removeMetadata',
'removeTitle',
'removeDesc',
'removeUselessDefs',
'removeEditorsNSData',
'removeEmptyAttrs',
'removeEmptyContainers',
'removeUnusedNS',
{
name: 'removeAttrs',
params: { attrs: ['fill', 'stroke'] },
},
{
name: 'addAttributesToSVGElement',
params: {
attributes: [
{ fill: 'currentColor' },
{ width: '1em' },
{ height: '1em' },
],
},
},
],
});
const outputPath = path.join('src/icons', `${icon.name}.svg`);
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, optimized.data);
console.log(`Exported: ${icon.name}`);
}
}
function collectIcons(node, icons, prefix = '') {
if (node.type === 'COMPONENT') {
icons.push({
id: node.id,
name: prefix ? `${prefix}/${node.name}` : node.name,
});
}
if (node.children) {
const newPrefix = node.type === 'FRAME' ? node.name : prefix;
node.children.forEach(child => collectIcons(child, icons, newPrefix));
}
}
exportIcons().catch(console.error);
生成 Vue 图标组件
// scripts/generate-icon-components.js
const fs = require('fs');
const path = require('path');
const iconsDir = 'src/icons';
const outputDir = 'src/components/icons';
function generateIconComponent(name, svg) {
const componentName = toPascalCase(name) + 'Icon';
// 移除 SVG 外层标签,保留内容
const svgContent = svg
.replace(/<svg[^>]*>/, '')
.replace(/<\/svg>/, '')
.trim();
return `<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
:width="size"
:height="size"
v-bind="$attrs"
>
${svgContent}
</svg>
</template>
<script setup lang="ts">
withDefaults(defineProps<{
size?: string | number;
}>(), {
size: '1em',
});
</script>
`;
}
function generateIndex(icons) {
const exports = icons.map(name => {
const componentName = toPascalCase(name) + 'Icon';
return `export { default as ${componentName} } from './${componentName}.vue';`;
});
return exports.join('\n');
}
function toPascalCase(str) {
return str
.split(/[-_/]/)
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join('');
}
// 主逻辑
const icons = fs.readdirSync(iconsDir)
.filter(file => file.endsWith('.svg'))
.map(file => file.replace('.svg', ''));
fs.mkdirSync(outputDir, { recursive: true });
icons.forEach(name => {
const svg = fs.readFileSync(path.join(iconsDir, `${name}.svg`), 'utf-8');
const component = generateIconComponent(name, svg);
const componentName = toPascalCase(name) + 'Icon';
fs.writeFileSync(
path.join(outputDir, `${componentName}.vue`),
component
);
});
// 生成 index.ts
fs.writeFileSync(
path.join(outputDir, 'index.ts'),
generateIndex(icons)
);
console.log(`Generated ${icons.length} icon components`);
组件文档自动生成
从 Figma 提取组件规格
// scripts/extract-component-specs.js
async function extractComponentSpecs(fileId, componentName) {
const file = await figma.getFile(fileId);
// 找到组件
const component = findComponent(file.document, componentName);
if (!component) return null;
// 提取属性
const specs = {
name: component.name,
description: component.description,
variants: [],
properties: [],
};
// 如果是组件集,提取变体
if (component.type === 'COMPONENT_SET') {
component.children.forEach(variant => {
specs.variants.push({
name: variant.name,
properties: parseVariantName(variant.name),
});
});
}
// 提取组件属性
if (component.componentPropertyDefinitions) {
Object.entries(component.componentPropertyDefinitions).forEach(
([key, def]) => {
specs.properties.push({
name: key,
type: def.type,
defaultValue: def.defaultValue,
options: def.variantOptions,
});
}
);
}
return specs;
}
function parseVariantName(name) {
// 解析 "Size=Large, Variant=Primary" 格式
const props = {};
name.split(', ').forEach(part => {
const [key, value] = part.split('=');
props[key] = value;
});
return props;
}
生成 TypeScript 类型
// scripts/generate-types.js
function generateComponentTypes(specs) {
const { name, properties } = specs;
const propTypes = properties.map(prop => {
let type;
switch (prop.type) {
case 'VARIANT':
type = prop.options.map(o => `'${o}'`).join(' | ');
break;
case 'BOOLEAN':
type = 'boolean';
break;
case 'TEXT':
type = 'string';
break;
case 'INSTANCE_SWAP':
type = 'React.ReactNode';
break;
default:
type = 'unknown';
}
return ` ${toCamelCase(prop.name)}?: ${type};`;
});
return `export interface ${name}Props {
${propTypes.join('\n')}
}
`;
}
完整工作流示例
package.json 脚本
{
"scripts": {
"tokens:pull": "node scripts/pull-tokens.js",
"tokens:build": "style-dictionary build",
"tokens:sync": "npm run tokens:pull && npm run tokens:build",
"icons:export": "node scripts/export-icons.js",
"icons:generate": "node scripts/generate-icon-components.js",
"icons:sync": "npm run icons:export && npm run icons:generate",
"design:sync": "npm run tokens:sync && npm run icons:sync",
"prepare": "husky install"
}
}
Git Hooks 集成
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 检查设计令牌是否有变更
if git diff --cached --name-only | grep -q "tokens/"; then
echo "Design tokens changed, rebuilding..."
npm run tokens:build
git add build/
fi
Webhook 触发
// 在 Tokens Studio 中配置 Webhook
// POST https://api.github.com/repos/{owner}/{repo}/dispatches
// GitHub Actions 监听 repository_dispatch 事件
// 自动创建 PR 同步令牌
最佳实践
1. 单一数据源
Figma (设计真相源)
│
└─→ 代码 (生成产物)
永远不要手动修改生成的代码!
2. 版本控制
tokens/
├── v1.0.0/
│ └── tokens.json
├── v1.1.0/
│ └── tokens.json
└── latest -> v1.1.0
3. 变更日志
// scripts/generate-changelog.js
function compareTokens(oldTokens, newTokens) {
const changes = {
added: [],
removed: [],
modified: [],
};
// 比较逻辑...
return changes;
}
4. 验证检查
// scripts/validate-tokens.js
function validateTokens(tokens) {
const errors = [];
// 检查颜色对比度
// 检查命名规范
// 检查引用完整性
if (errors.length > 0) {
throw new Error(`Token validation failed:\n${errors.join('\n')}`);
}
}
结语
设计到代码的自动化不是一蹴而就的,它需要设计师和开发者共同建立流程和规范。但一旦建立起来,收益是巨大的:
- 消除重复工作:手动同步变成自动同步
- 减少错误:机器比人更可靠
- 加速迭代:设计变更快速落地
- 保持一致:单一数据源确保统一
记住:自动化的目标不是取代人,而是让人专注于更有价值的工作。
"Automate the boring stuff, so you can focus on the interesting stuff."


