设计到代码的自动化流程:从 Figma 到前端组件的现代工作流

HTMLPAGE 团队
12 分钟阅读

探索设计系统自动化的最佳实践,涵盖设计令牌同步、组件代码生成、资产导出等核心环节,实现设计与开发的无缝衔接。

#设计自动化 #Figma API #Design Tokens #代码生成 #DevOps

设计到代码的自动化流程:从 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')}`);
  }
}

结语

设计到代码的自动化不是一蹴而就的,它需要设计师和开发者共同建立流程和规范。但一旦建立起来,收益是巨大的:

  1. 消除重复工作:手动同步变成自动同步
  2. 减少错误:机器比人更可靠
  3. 加速迭代:设计变更快速落地
  4. 保持一致:单一数据源确保统一

记住:自动化的目标不是取代人,而是让人专注于更有价值的工作。

"Automate the boring stuff, so you can focus on the interesting stuff."