📋 目录导航

  1. 为什么你必须学会Figma插件开发?
  2. Figma插件架构深度解析
  3. 环境搭建:5分钟搞定开发环境
  4. Hello World:你的第一个插件
  5. 核心API实战:节点操作大全
  6. UI与主线程通信:postMessage机制
  7. 进阶实战:批量图层命名插件
  8. React + TypeScript 现代开发方案
  9. 调试技巧与性能优化
  10. 发布上线:从开发到Figma社区
  11. 2026年插件开发趋势与AI集成
  12. 常见问题FAQ

1. 为什么你必须学会Figma插件开发?

1.1 插件生态的爆发式增长

Figma作为全球设计协作工具的领导者,其插件生态在2026年已经达到了**10,000+插件。根据Figma官方数据,超过70%**的专业设计师每天都会使用至少3个插件来提升工作效率。这意味着插件开发不仅是技术能力的体现,更是一个巨大的流量入口和商业机会。

1.2 插件能解决什么痛点?

场景 痛点 插件解决方案
批量操作 手动重命名100个图层 一键批量命名
设计规范 检查颜色对比度是否符合WCAG 自动对比度检测
数据填充 用Lorem ipsum占位 真实数据自动填充
代码导出 手动标注尺寸和样式 一键生成CSS/Tailwind
团队协作 设计Token同步困难 双向同步GitHub

1.3 商业价值

  • 流量获取: 一个优质插件可以获得数万次安装,直接导流到你的个人品牌或产品
  • 技术变现: Figma支持插件内付费,Tokens Studio等插件已实现月入数万美金
  • 职业竞争力: 懂插件开发的前端/设计师在招聘市场溢价30%+

2. Figma插件架构深度解析

2.1 双线程架构:理解核心原理

Figma插件采用沙盒双线程架构,这是理解插件开发的第一课:

┌─────────────────────────────────────────────────────────────┐
│                    Figma Desktop App                         │
│  ┌─────────────────┐        ┌─────────────────────────┐     │
│  │   主线程 (Main)  │        │     UI线程 (iframe)      │     │
│  │                 │        │                         │     │
│  │  • 访问Figma API │◄──────►│  • HTML/CSS/JS          │     │
│  │  • 操作文档节点  │  postMessage  │  • React/Vue UI        │     │
│  │  • 执行逻辑代码  │        │  • 用户交互              │     │
│  │  • 无网络访问    │        │  • 可访问外部API         │     │
│  │  • 无DOM操作    │        │  • 无直接访问Figma API   │     │
│  └─────────────────┘        └─────────────────────────┘     │
└─────────────────────────────────────────────────────────────┘

关键理解点:

  • 主线程运行在Figma的沙盒中,拥有完整的Figma API访问权限,但无法直接访问网络DOM
  • UI线程是一个普通的iframe,可以使用任何Web技术(React/Vue/Canvas),但无法直接操作Figma文档
  • 两者通过 postMessage 进行通信,这是唯一的桥梁

2.2 插件生命周期

用户触发插件 → Figma加载main代码 → 执行figma.showUI() → 加载UI HTML
     │                                                              │
     │                    ←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←│
     │         (UI通过postMessage发送指令给主线程)                    │
     │                                                              │
     │→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→│
     │         (主线程通过figma.ui.postMessage发送数据给UI)          │
     │                                                              │
     └────────────────── figma.closePlugin() ───────────────────────┘

3. 环境搭建:5分钟搞定开发环境

3.1 必备工具清单

工具 用途 下载链接
Figma Desktop App 插件开发和测试必需 官网下载
VS Code 代码编辑器 官网下载
Node.js 18+ 构建工具链 官网下载
TypeScript 类型安全开发 npm install -g typescript

3.2 创建第一个插件项目

步骤1: 打开Figma Desktop,创建或打开一个设计文件

步骤2: 菜单栏 → PluginsDevelopmentNew Plugin...

步骤3: 在弹窗中选择:

  • Figma design (支持Figma Design、FigJam、Slides等)
  • 插件名称:MyFirstPlugin
  • 模板选择:Custom UI (带交互界面的插件)
  • 点击 Save as 保存到本地文件夹

步骤4: Figma会自动生成以下文件结构:

MyFirstPlugin/
├── manifest.json      # 插件配置文件(核心)
├── code.ts           # 主线程代码(沙盒环境)
├── code.js           # 编译后的主线程代码
├── ui.html           # UI界面(iframe中运行)
├── tsconfig.json     # TypeScript配置
└── package.json      # 依赖管理

3.3 manifest.json 完全解析

manifest.json 是插件的"身份证",Figma通过它识别插件:

{
  "name": "MyFirstPlugin",
  "id": "1234567890123456789",
  "api": "1.0.0",
  "editorType": ["figma"],
  "main": "code.js",
  "ui": "ui.html",
  "documentAccess": "dynamic-page",
  "networkAccess": {
    "allowedDomains": ["none"]
  }
}
字段 类型 必填 说明
name string 插件名称,显示在菜单中
id string 发布时 插件唯一ID,开发时可省略
api string API版本,建议始终用最新版
editorType array 支持的产品:figma/figjam/slides/dev
main string 主线程JS入口文件路径
ui string/object UI HTML文件路径,支持多页面
documentAccess string 文档访问权限:dynamic-page/none
networkAccess object 网络访问白名单配置
permissions array 额外权限:currentuser
relaunchButtons array 插件关闭后显示的快捷按钮

高级配置示例(多页面UI + 网络权限):

{
  "name": "Advanced Plugin",
  "id": "9876543210987654321",
  "api": "1.0.0",
  "editorType": ["figma", "figjam"],
  "main": "code.js",
  "ui": {
    "main": "ui.html",
    "settings": "settings.html"
  },
  "documentAccess": "dynamic-page",
  "networkAccess": {
    "allowedDomains": ["https://api.example.com", "https://cdn.example.com"],
    "devAllowedDomains": ["http://localhost:3000"]
  },
  "permissions": ["currentuser"],
  "relaunchButtons": [
    { "command": "show", "name": "Open Panel" }
  ]
}

3.4 TypeScript编译配置

在VS Code中按 Ctrl+Shift+B (Windows) / Cmd+Shift+B (Mac),选择 watch-tsconfig.json,开启自动编译。


4. Hello World:你的第一个插件

4.1 需求分析

我们要做一个简单插件:在UI中输入数字,点击按钮后在Figma画布上创建对应数量的橙色矩形。

4.2 主线程代码 (code.ts)

// code.ts - 运行在Figma沙盒中

// 显示UI界面,参数为UI尺寸
figma.showUI(__html__, { width: 300, height: 200 });

// 监听UI发送的消息
figma.ui.onmessage = (msg: { type: string; count: number }) => {
  if (msg.type === 'create-rectangles') {
    const count = msg.count;
    const nodes: SceneNode[] = [];

    // 批量创建矩形
    for (let i = 0; i < count; i++) {
      const rect = figma.createRectangle();
      rect.x = i * 150;          // 水平排列
      rect.y = 0;
      rect.fills = [{            // 设置橙色填充
        type: 'SOLID',
        color: { r: 1, g: 0.5, b: 0 }
      }];
      rect.name = `Rectangle ${i + 1}`;
      figma.currentPage.appendChild(rect);  // 添加到当前页面
      nodes.push(rect);
    }

    // 选中所有创建的矩形并聚焦
    figma.currentPage.selection = nodes;
    figma.viewport.scrollAndZoomIntoView(nodes);

    // 关闭插件(可选,根据需求决定)
    // figma.closePlugin();
  }
};

4.3 UI界面代码 (ui.html)

<!-- ui.html - 运行在iframe中 -->
<!DOCTYPE html>
<html>
<head>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      padding: 20px;
      background: #f5f5f5;
    }
    .container {
      background: white;
      border-radius: 8px;
      padding: 20px;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    }
    h2 { font-size: 18px; margin-bottom: 16px; color: #333; }
    label {
      display: block;
      margin-bottom: 8px;
      font-size: 14px;
      color: #666;
    }
    input[type="number"] {
      width: 100%;
      padding: 10px 12px;
      border: 1px solid #ddd;
      border-radius: 6px;
      font-size: 14px;
      margin-bottom: 16px;
    }
    button {
      width: 100%;
      padding: 12px;
      background: #0061FF;
      color: white;
      border: none;
      border-radius: 6px;
      font-size: 14px;
      font-weight: 600;
      cursor: pointer;
      transition: background 0.2s;
    }
    button:hover { background: #0050d8; }
    button:active { transform: scale(0.98); }
  </style>
</head>
<body>
  <div class="container">
    <h2>🎨 矩形生成器</h2>
    <label for="count">创建数量:</label>
    <input type="number" id="count" value="5" min="1" max="50" />
    <button id="create">创建矩形</button>
  </div>

  <script>
    document.getElementById('create').onclick = () => {
      const count = parseInt(document.getElementById('count').value) || 5;
      // 向主线程发送消息
      parent.postMessage(
        { pluginMessage: { type: 'create-rectangles', count: count } },
        '*'
      );
    };
  </script>
</body>
</html>

4.4 运行测试

  1. 确保TypeScript已编译(code.tscode.js
  2. Figma中:PluginsDevelopmentMyFirstPlugin
  3. 在弹出的UI中输入数字,点击按钮
  4. 你应该看到橙色矩形出现在画布上!🎉

5. 核心API实战:节点操作大全

5.1 节点创建API

Figma提供了丰富的节点创建API,以下是2026年最新支持的创建方法:

// 基础形状
figma.createRectangle()           // 矩形
figma.createEllipse()             // 椭圆/圆形
figma.createLine()                // 线条
figma.createStar()                // 星形
figma.createPolygon()             // 多边形
figma.createVector()              // 矢量路径
figma.createBooleanOperation()    // 布尔运算

// 容器与布局
figma.createFrame()               // 普通Frame
figma.createAutoLayout()          // Auto Layout Frame(推荐用于布局容器)
figma.createAutoLayout("VERTICAL") // 垂直Auto Layout
figma.createComponent()           // 创建组件
figma.createComponentInstance()   // 创建组件实例
figma.createSection()             // 创建Section

// 文本与图片
figma.createText()                // 文本节点
figma.createTextPath()            // 路径文本
figma.createImage()               // 从Uint8Array创建图片
figma.createSlice()               // 切片(用于导出)

// 页面管理
figma.createPage()                // 创建新页面(仅限Design文件)

5.2 节点查询与遍历

// 获取当前选中的节点
const selection = figma.currentPage.selection;

// 通过ID获取节点(跨页面)
const node = figma.getNodeById("123:456");

// 遍历当前页面所有节点
figma.currentPage.children.forEach(child => {
  console.log(child.name, child.type);
});

// 深度遍历(查找所有文本节点)
const textNodes = figma.currentPage.findAll(node => node.type === 'TEXT');

// 查找特定名称的节点
const header = figma.currentPage.findOne(node => node.name === 'Header');

// 遍历选中节点的子节点
if (selection.length > 0 && 'children' in selection[0]) {
  const parent = selection[0] as FrameNode;
  parent.children.forEach(child => {
    // 处理子节点
  });
}

5.3 节点属性操作

const rect = figma.createRectangle();

// 基础属性
rect.name = "My Rectangle";
rect.x = 100;
rect.y = 200;
rect.resize(200, 100);        // 设置宽高
rect.rotation = 45;           // 旋转角度
rect.cornerRadius = 12;       // 圆角
rect.topLeftRadius = 8;       // 单独设置圆角(Figma 2023+)

// 填充样式
rect.fills = [{
  type: 'SOLID',
  color: { r: 0.2, g: 0.6, b: 1 },
  opacity: 0.8
}];

// 渐变填充
rect.fills = [{
  type: 'GRADIENT_LINEAR',
  gradientTransform: [[0, 1, 0], [-1, 0, 1]],
  gradientStops: [
    { position: 0, color: { r: 1, g: 0, b: 0, a: 1 } },
    { position: 1, color: { r: 0, g: 0, b: 1, a: 1 } }
  ]
}];

// 描边
rect.strokes = [{
  type: 'SOLID',
  color: { r: 0, g: 0, b: 0 }
}];
rect.strokeWeight = 2;
rect.strokeAlign = 'CENTER';  // 'INSIDE' | 'OUTSIDE' | 'CENTER'

// 效果(阴影、模糊)
rect.effects = [
  {
    type: 'DROP_SHADOW',
    color: { r: 0, g: 0, b: 0, a: 0.25 },
    offset: { x: 0, y: 4 },
    radius: 8,
    visible: true,
    blendMode: 'NORMAL'
  },
  {
    type: 'LAYER_BLUR',
    radius: 10,
    visible: true
  }
];

// 不透明度与混合模式
rect.opacity = 0.9;
rect.blendMode = 'MULTIPLY';  // 多种混合模式可选

// 布局约束(在Auto Layout中)
rect.layoutAlign = 'STRETCH';
rect.layoutGrow = 1;
rect.constraints = {
  horizontal: 'SCALE',
  vertical: 'SCALE'
};

5.4 Auto Layout 操作

// 创建Auto Layout Frame
const frame = figma.createAutoLayout();
frame.name = "Card Container";

// 配置Auto Layout属性
frame.layoutMode = 'VERTICAL';        // 'VERTICAL' | 'HORIZONTAL'
frame.primaryAxisAlignItems = 'CENTER'; // 主轴对齐
frame.counterAxisAlignItems = 'CENTER'; // 交叉轴对齐
frame.itemSpacing = 16;               // 子元素间距
frame.paddingTop = 24;
frame.paddingBottom = 24;
frame.paddingLeft = 24;
frame.paddingRight = 24;
frame.layoutWrap = 'WRAP';            // 是否换行(Figma 2024+)

// 添加子元素
const child1 = figma.createRectangle();
child1.resize(100, 100);
frame.appendChild(child1);

const child2 = figma.createText();
child2.characters = "Hello Auto Layout";
frame.appendChild(child2);

5.5 文本节点高级操作

const text = figma.createText();

// 基础设置
text.characters = "Hello Figma Plugin!";
text.fontSize = 24;
text.fontName = { family: "Inter", style: "Bold" };

// 加载字体(必须先加载才能修改文本)
await figma.loadFontAsync(text.fontName);
text.characters = "修改后的文本";

// 文本样式
text.textAlignHorizontal = 'CENTER';
text.textAlignVertical = 'CENTER';
text.textAutoResize = 'WIDTH_AND_HEIGHT'; // 'NONE' | 'WIDTH_AND_HEIGHT' | 'HEIGHT'
text.paragraphIndent = 20;
text.paragraphSpacing = 10;

// 部分文本样式(使用TextSegment)
text.setRangeFontSize(0, 5, 32);  // 前5个字符设置不同字号
text.setRangeFills(6, 10, [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]);

// 行高与字间距
text.lineHeight = { unit: 'PIXELS', value: 28 };
text.letterSpacing = { unit: 'PERCENT', value: -2 };
text.textCase = 'UPPER';  // 'ORIGINAL' | 'UPPER' | 'LOWER' | 'TITLE'

5.6 图片处理

// 从网络获取图片(需要在UI线程中完成)
// UI线程代码:
async function loadImage(url: string): Promise<Uint8Array> {
  const response = await fetch(url);
  const buffer = await response.arrayBuffer();
  return new Uint8Array(buffer);
}

// 主线程代码:
figma.ui.onmessage = async (msg) => {
  if (msg.type === 'insert-image') {
    const image = figma.createImage(msg.bytes);
    const rect = figma.createRectangle();
    rect.resize(msg.width, msg.height);
    rect.fills = [{
      type: 'IMAGE',
      imageHash: image.hash,
      scaleMode: 'FILL'  // 'FILL' | 'FIT' | 'CROP' | 'TILE'
    }];
    figma.currentPage.appendChild(rect);
  }
};

6. UI与主线程通信:postMessage机制

6.1 通信原理

UI线程 (iframe)                    主线程 (Sandbox)
     │                                    │
     │  parent.postMessage({...}, '*')   │
     │ ─────────────────────────────────> │
     │                                    │  figma.ui.onmessage
     │                                    │
     │  window.onmessage                  │
     │ <───────────────────────────────── │  figma.ui.postMessage({...})
     │                                    │

6.2 完整通信示例

主线程 (code.ts):

figma.showUI(__html__, { width: 400, height: 300 });

// 向UI发送数据(如当前选中节点信息)
function sendSelectionToUI() {
  const selection = figma.currentPage.selection;
  const nodesInfo = selection.map(node => ({
    id: node.id,
    name: node.name,
    type: node.type,
    width: 'width' in node ? node.width : null,
    height: 'height' in node ? node.height : null
  }));

  figma.ui.postMessage({
    type: 'selection-update',
    nodes: nodesInfo
  });
}

// 监听选择变化
figma.on('selectionchange', sendSelectionToUI);
sendSelectionToUI(); // 初始化发送

// 接收UI消息
figma.ui.onmessage = async (msg) => {
  switch (msg.type) {
    case 'rename-nodes':
      const { prefix, startNumber } = msg;
      figma.currentPage.selection.forEach((node, index) => {
        node.name = `${prefix} ${startNumber + index}`;
      });
      figma.notify(`✅ 已重命名 ${figma.currentPage.selection.length} 个图层`);
      break;

    case 'export-nodes':
      for (const node of figma.currentPage.selection) {
        const bytes = await node.exportAsync({
          format: 'PNG',
          constraint: { type: 'SCALE', value: 2 }
        });
        figma.ui.postMessage({
          type: 'export-complete',
          name: node.name,
          bytes: bytes
        });
      }
      break;

    case 'close':
      figma.closePlugin();
      break;
  }
};

UI线程 (ui.html):

<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: sans-serif; padding: 16px; }
    .node-list { margin: 12px 0; }
    .node-item {
      padding: 8px;
      background: #f0f0f0;
      border-radius: 4px;
      margin-bottom: 4px;
      font-size: 12px;
    }
    input, button { padding: 8px; margin: 4px 0; }
  </style>
</head>
<body>
  <h3>🎯 图层批量操作</h3>

  <div id="selection-info">
    <p>当前选中: <span id="count">0</span> 个图层</p>
    <div id="node-list" class="node-list"></div>
  </div>

  <hr>

  <h4>批量重命名</h4>
  <input type="text" id="prefix" placeholder="前缀,如 Icon" value="Layer" />
  <input type="number" id="startNum" placeholder="起始编号" value="1" />
  <button id="rename">执行重命名</button>

  <hr>

  <button id="export">导出选中图层 (2x PNG)</button>
  <button id="close" style="background:#ff4444;color:white;">关闭插件</button>

  <script>
    // 接收主线程消息
    window.onmessage = (event) => {
      const msg = event.data.pluginMessage;
      if (!msg) return;

      switch (msg.type) {
        case 'selection-update':
          document.getElementById('count').textContent = msg.nodes.length;
          const list = document.getElementById('node-list');
          list.innerHTML = msg.nodes.map(n => 
            `<div class="node-item">${n.type}: ${n.name} (${n.width}x${n.height})</div>`
          ).join('');
          break;

        case 'export-complete':
          console.log('导出完成:', msg.name);
          // 可以在这里触发浏览器下载
          const blob = new Blob([msg.bytes], { type: 'image/png' });
          const url = URL.createObjectURL(blob);
          const a = document.createElement('a');
          a.href = url;
          a.download = `${msg.name}.png`;
          a.click();
          break;
      }
    };

    // 发送消息给主线程
    document.getElementById('rename').onclick = () => {
      const prefix = document.getElementById('prefix').value;
      const startNum = parseInt(document.getElementById('startNum').value);
      parent.postMessage(
        { pluginMessage: { type: 'rename-nodes', prefix, startNumber: startNum } },
        '*'
      );
    };

    document.getElementById('export').onclick = () => {
      parent.postMessage(
        { pluginMessage: { type: 'export-nodes' } },
        '*'
      );
    };

    document.getElementById('close').onclick = () => {
      parent.postMessage(
        { pluginMessage: { type: 'close' } },
        '*'
      );
    };
  </script>
</body>
</html>

6.3 通信最佳实践

// 1. 定义消息类型接口(TypeScript)
interface PluginMessage {
  type: 'create-rect' | 'rename' | 'export' | 'close';
  data?: any;
}

// 2. 使用类型安全的通信
function postMessageToUI(msg: PluginMessage) {
  figma.ui.postMessage(msg);
}

// 3. 错误处理
figma.ui.onmessage = async (msg) => {
  try {
    await handleMessage(msg);
  } catch (error) {
    figma.notify(`❌ 错误: ${error.message}`, { error: true });
    figma.ui.postMessage({ type: 'error', message: error.message });
  }
};

// 4. 进度反馈
async function batchProcess(nodes: SceneNode[]) {
  const total = nodes.length;
  for (let i = 0; i < total; i++) {
    await processNode(nodes[i]);
    figma.ui.postMessage({
      type: 'progress',
      current: i + 1,
      total: total,
      percent: Math.round(((i + 1) / total) * 100)
    });
  }
}

7. 进阶实战:批量图层命名插件

7.1 需求分析

设计一个实用的插件:智能图层命名助手,功能包括:

  • 批量重命名选中图层
  • 支持多种命名规则(序号、前缀、后缀、替换)
  • 实时预览命名结果
  • 支持撤销操作

7.2 完整代码实现

code.ts:

// 类型定义
interface RenameRule {
  type: 'prefix' | 'suffix' | 'replace' | 'number' | 'camelCase' | 'kebab-case';
  value?: string;
  search?: string;
  replacement?: string;
  startNumber?: number;
  padding?: number;
}

interface RenameMessage {
  type: 'rename';
  rule: RenameRule;
}

// 显示UI
figma.showUI(__html__, { width: 420, height: 500 });

// 发送初始选中状态
function updateSelection() {
  const selection = figma.currentPage.selection;
  figma.ui.postMessage({
    type: 'selection',
    count: selection.length,
    nodes: selection.map(n => ({
      id: n.id,
      name: n.name,
      type: n.type
    }))
  });
}

figma.on('selectionchange', updateSelection);
updateSelection();

// 处理重命名逻辑
figma.ui.onmessage = (msg: RenameMessage) => {
  if (msg.type === 'rename') {
    const selection = figma.currentPage.selection;
    if (selection.length === 0) {
      figma.notify('⚠️ 请先选择图层', { error: true });
      return;
    }

    const { rule } = msg;
    const results: { oldName: string; newName: string }[] = [];

    selection.forEach((node, index) => {
      const oldName = node.name;
      let newName = oldName;

      switch (rule.type) {
        case 'prefix':
          newName = `${rule.value} ${oldName}`;
          break;
        case 'suffix':
          newName = `${oldName} ${rule.value}`;
          break;
        case 'replace':
          newName = oldName.replace(
            new RegExp(rule.search || '', 'g'),
            rule.replacement || ''
          );
          break;
        case 'number':
          const num = (rule.startNumber || 1) + index;
          const pad = rule.padding || 2;
          const numStr = num.toString().padStart(pad, '0');
          newName = `${rule.value || ''} ${numStr}`;
          break;
        case 'camelCase':
          newName = toCamelCase(oldName);
          break;
        case 'kebab-case':
          newName = toKebabCase(oldName);
          break;
      }

      node.name = newName;
      results.push({ oldName, newName });
    });

    figma.notify(`✅ 已重命名 ${selection.length} 个图层`);
    figma.ui.postMessage({ type: 'rename-complete', results });
  }
};

// 工具函数
function toCamelCase(str: string): string {
  return str
    .replace(/[^a-zA-Z0-9]/g, ' ')
    .split(' ')
    .map((word, i) => 
      i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
    )
    .join('');
}

function toKebabCase(str: string): string {
  return str
    .replace(/([a-z])([A-Z])/g, '$1-$2')
    .replace(/[^a-zA-Z0-9]/g, '-')
    .toLowerCase()
    .replace(/-+/g, '-')
    .replace(/^-|-$/g, '');
}

ui.html (使用更现代的UI):

<!DOCTYPE html>
<html>
<head>
  <style>
    :root {
      --primary: #0061FF;
      --primary-hover: #0050d8;
      --bg: #f5f5f5;
      --card: #ffffff;
      --text: #333333;
      --text-secondary: #666666;
      --border: #e0e0e0;
      --success: #10b981;
      --error: #ef4444;
    }

    * { margin: 0; padding: 0; box-sizing: border-box; }

    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: var(--bg);
      color: var(--text);
      padding: 16px;
      font-size: 13px;
    }

    .card {
      background: var(--card);
      border-radius: 12px;
      padding: 16px;
      margin-bottom: 12px;
      box-shadow: 0 1px 3px rgba(0,0,0,0.08);
    }

    h2 { font-size: 16px; margin-bottom: 12px; }
    h3 { font-size: 13px; color: var(--text-secondary); margin-bottom: 8px; text-transform: uppercase; }

    .selection-info {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 12px;
      background: #f0f7ff;
      border-radius: 8px;
      margin-bottom: 12px;
    }

    .selection-info .count {
      font-size: 24px;
      font-weight: 700;
      color: var(--primary);
    }

    .rule-buttons {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 8px;
      margin-bottom: 12px;
    }

    .rule-btn {
      padding: 10px;
      border: 1px solid var(--border);
      border-radius: 8px;
      background: white;
      cursor: pointer;
      text-align: center;
      font-size: 12px;
      transition: all 0.2s;
    }

    .rule-btn:hover { border-color: var(--primary); }
    .rule-btn.active {
      background: var(--primary);
      color: white;
      border-color: var(--primary);
    }

    input[type="text"], input[type="number"] {
      width: 100%;
      padding: 10px 12px;
      border: 1px solid var(--border);
      border-radius: 8px;
      font-size: 13px;
      margin-bottom: 8px;
      outline: none;
    }

    input:focus { border-color: var(--primary); }

    .input-row {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 8px;
    }

    .btn-primary {
      width: 100%;
      padding: 12px;
      background: var(--primary);
      color: white;
      border: none;
      border-radius: 8px;
      font-size: 14px;
      font-weight: 600;
      cursor: pointer;
      margin-top: 12px;
    }

    .btn-primary:hover { background: var(--primary-hover); }
    .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }

    .preview {
      margin-top: 12px;
      padding: 12px;
      background: #f8f9fa;
      border-radius: 8px;
      max-height: 150px;
      overflow-y: auto;
    }

    .preview-item {
      display: flex;
      justify-content: space-between;
      padding: 4px 0;
      font-size: 12px;
      border-bottom: 1px solid var(--border);
    }

    .preview-item:last-child { border-bottom: none; }
    .old-name { color: var(--text-secondary); text-decoration: line-through; }
    .new-name { color: var(--success); font-weight: 600; }

    .hidden { display: none; }
  </style>
</head>
<body>
  <div class="card">
    <h2>🎯 智能图层命名</h2>
    <div class="selection-info">
      <span class="count" id="count">0</span>
      <span>个图层已选中</span>
    </div>
  </div>

  <div class="card">
    <h3>选择规则</h3>
    <div class="rule-buttons">
      <div class="rule-btn active" data-rule="prefix">添加前缀</div>
      <div class="rule-btn" data-rule="suffix">添加后缀</div>
      <div class="rule-btn" data-rule="replace">查找替换</div>
      <div class="rule-btn" data-rule="number">序号命名</div>
      <div class="rule-btn" data-rule="camelCase">驼峰命名</div>
      <div class="rule-btn" data-rule="kebab-case">短横线命名</div>
    </div>

    <div id="input-prefix">
      <input type="text" id="prefix-val" placeholder="输入前缀,如 Icon" />
    </div>

    <div id="input-suffix" class="hidden">
      <input type="text" id="suffix-val" placeholder="输入后缀,如 Copy" />
    </div>

    <div id="input-replace" class="hidden">
      <input type="text" id="search-val" placeholder="查找内容" />
      <input type="text" id="replace-val" placeholder="替换为" />
    </div>

    <div id="input-number" class="hidden">
      <input type="text" id="number-prefix" placeholder="前缀,如 Button" />
      <div class="input-row">
        <input type="number" id="start-num" placeholder="起始编号" value="1" />
        <input type="number" id="padding" placeholder="补零位数" value="2" />
      </div>
    </div>

    <button class="btn-primary" id="apply">应用重命名</button>
  </div>

  <div class="card preview hidden" id="preview-card">
    <h3>预览结果</h3>
    <div id="preview-list"></div>
  </div>

  <script>
    let currentRule = 'prefix';
    let selectedNodes = [];

    // 规则切换
    document.querySelectorAll('.rule-btn').forEach(btn => {
      btn.onclick = () => {
        document.querySelectorAll('.rule-btn').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        currentRule = btn.dataset.rule;

        // 显示对应输入框
        document.querySelectorAll('[id^="input-"]').forEach(el => el.classList.add('hidden'));
        document.getElementById(`input-${currentRule}`).classList.remove('hidden');

        updatePreview();
      };
    });

    // 接收主线程消息
    window.onmessage = (event) => {
      const msg = event.data.pluginMessage;
      if (!msg) return;

      if (msg.type === 'selection') {
        document.getElementById('count').textContent = msg.count;
        selectedNodes = msg.nodes;
        updatePreview();
      }

      if (msg.type === 'rename-complete') {
        document.getElementById('preview-card').classList.remove('hidden');
        document.getElementById('preview-list').innerHTML = msg.results.map(r => `
          <div class="preview-item">
            <span class="old-name">${r.oldName}</span>
            <span>→</span>
            <span class="new-name">${r.newName}</span>
          </div>
        `).join('');
      }
    };

    // 更新预览
    function updatePreview() {
      if (selectedNodes.length === 0) return;
      // 这里可以添加实时预览逻辑
    }

    // 应用重命名
    document.getElementById('apply').onclick = () => {
      const rule = { type: currentRule };

      switch (currentRule) {
        case 'prefix':
          rule.value = document.getElementById('prefix-val').value;
          break;
        case 'suffix':
          rule.value = document.getElementById('suffix-val').value;
          break;
        case 'replace':
          rule.search = document.getElementById('search-val').value;
          rule.replacement = document.getElementById('replace-val').value;
          break;
        case 'number':
          rule.value = document.getElementById('number-prefix').value;
          rule.startNumber = parseInt(document.getElementById('start-num').value) || 1;
          rule.padding = parseInt(document.getElementById('padding').value) || 2;
          break;
      }

      parent.postMessage(
        { pluginMessage: { type: 'rename', rule } },
        '*'
      );
    };
  </script>
</body>
</html>

8. React + TypeScript 现代开发方案

8.1 为什么要用React?

  • 组件化: UI逻辑更清晰,复用性高
  • 状态管理: 复杂交互更容易维护
  • 生态丰富: 可使用React组件库(如figma-plugin-ds)
  • TypeScript支持: 类型安全,开发体验更好

8.2 项目搭建

使用 create-figma-plugin 脚手架(推荐):

# 安装脚手架
npm install -g create-figma-plugin

# 创建项目
create-figma-plugin --template react-editor-with-sidebar

# 或使用webpack手动搭建

8.3 Webpack配置

// webpack.config.js
const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = (env, argv) => ({
  mode: argv.mode === 'production' ? 'production' : 'development',
  devtool: argv.mode === 'production' ? false : 'inline-source-map',

  entry: {
    ui: './src/ui.tsx',      // UI入口
    code: './src/code.ts',   // 主线程入口
  },

  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'sass-loader'],
      },
      {
        test: /\.svg$/,
        use: '@svgr/webpack',
      },
    ],
  },

  resolve: {
    extensions: ['.tsx', '.ts', '.js', '.jsx'],
  },

  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist'),
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: './src/ui.html',
      filename: 'ui.html',
      chunks: ['ui'],
      inlineSource: '.(js|css)$',  // 内联JS和CSS
    }),
  ],
});

8.4 React UI组件示例

// src/ui.tsx
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
import './ui.css';

interface NodeInfo {
  id: string;
  name: string;
  type: string;
}

const App: React.FC = () => {
  const [nodes, setNodes] = useState<NodeInfo[]>([]);
  const [prefix, setPrefix] = useState('');
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    // 接收主线程消息
    window.onmessage = (event) => {
      const msg = event.data.pluginMessage;
      if (msg?.type === 'selection') {
        setNodes(msg.nodes);
      }
      if (msg?.type === 'rename-complete') {
        setLoading(false);
      }
    };
  }, []);

  const handleRename = () => {
    setLoading(true);
    parent.postMessage(
      { pluginMessage: { type: 'rename', prefix } },
      '*'
    );
  };

  return (
    <div className="app">
      <header>
        <h1>🎯 图层命名助手</h1>
        <span className="badge">{nodes.length} 选中</span>
      </header>

      <main>
        <div className="input-group">
          <label>前缀</label>
          <input
            type="text"
            value={prefix}
            onChange={(e) => setPrefix(e.target.value)}
            placeholder="例如: Icon"
          />
        </div>

        <button 
          className="btn-primary"
          onClick={handleRename}
          disabled={loading || nodes.length === 0}
        >
          {loading ? '处理中...' : '应用重命名'}
        </button>

        <div className="node-list">
          {nodes.map(node => (
            <div key={node.id} className="node-item">
              <span className="node-type">{node.type}</span>
              <span className="node-name">{node.name}</span>
            </div>
          ))}
        </div>
      </main>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById('root'));

8.5 使用Figma Design System组件库

为了让插件UI看起来"原生",推荐使用 figma-plugin-ds

npm install figma-plugin-ds
import { Button, Input, Select, Checkbox } from 'figma-plugin-ds-react';

// 使用Figma原生风格的组件
<Button onClick={handleClick} variant="primary">
  确认操作
</Button>

<Input 
  type="text" 
  placeholder="输入内容"
  onChange={handleChange}
/>

<Select 
  options={[
    { value: 'prefix', label: '添加前缀' },
    { value: 'suffix', label: '添加后缀' },
  ]}
  onChange={handleRuleChange}
/>

9. 调试技巧与性能优化

9.1 调试方法

1. 控制台调试

在插件UI中按 Ctrl+Shift+I (Windows) / Cmd+Option+I (Mac) 打开Chrome DevTools:

// 主线程日志会显示在Figma控制台
console.log('主线程日志:', figma.currentPage.selection);

// UI线程日志在DevTools中查看
console.log('UI线程日志:', document.body.innerHTML);

2. 使用figma.notify进行状态提示

figma.notify('✅ 操作成功');
figma.notify('❌ 发生错误', { error: true, timeout: 3000 });

3. 热重载 (Hot Reload)

Figma支持热重载,修改代码后自动刷新:

// 在开发模式下,Figma会自动检测文件变化
// 确保你的构建工具支持watch模式
// webpack: npx webpack --mode=development --watch

4. 使用iframe套娃解决UI热更新

<!-- ui.html -->
<div id="root"></div>
<script>
  // 开发模式下加载本地开发服务器
  if (location.href.includes('localhost')) {
    const iframe = document.createElement('iframe');
    iframe.src = 'http://localhost:3000';
    iframe.style.width = '100%';
    iframe.style.height = '100%';
    iframe.style.border = 'none';
    document.body.appendChild(iframe);

    // 转发消息
    window.onmessage = (e) => {
      if (e.source === iframe.contentWindow) {
        parent.postMessage(e.data, '*');
      }
    };
  }
</script>

9.2 性能优化

1. 批量操作

// ❌ 错误:逐个操作,性能差
nodes.forEach(node => {
  node.x += 10;  // 每次操作都会触发重绘
});

// ✅ 正确:批量操作
figma.skipInvisibleInstanceChildren = true;  // 跳过不可见子节点
const nodes = figma.currentPage.findAll(node => node.type === 'TEXT');
// 一次性处理

2. 异步加载字体

// 先加载所有需要的字体,再批量修改文本
const fonts = new Set<FontName>();
textNodes.forEach(node => {
  if (node.fontName !== figma.mixed) {
    fonts.add(node.fontName as FontName);
  }
});

// 并行加载
await Promise.all(
  Array.from(fonts).map(font => figma.loadFontAsync(font))
);

// 然后批量修改
textNodes.forEach(node => {
  node.characters = '新文本';
});

3. 使用requestAnimationFrame

// 对于大量节点操作,使用RAF避免阻塞
function batchProcess(nodes: SceneNode[], batchSize = 50) {
  let index = 0;

  function processBatch() {
    const batch = nodes.slice(index, index + batchSize);
    batch.forEach(node => {
      // 处理节点
    });

    index += batchSize;
    if (index < nodes.length) {
      requestAnimationFrame(processBatch);
    } else {
      figma.notify('处理完成');
    }
  }

  processBatch();
}

4. 避免重复查询

// ❌ 错误:在循环中重复查询
for (let i = 0; i < 100; i++) {
  const page = figma.currentPage;  // 每次都获取
  // ...
}

// ✅ 正确:缓存引用
const page = figma.currentPage;
for (let i = 0; i < 100; i++) {
  // 使用缓存的page
}

10. 发布上线:从开发到Figma社区

10.1 发布前检查清单

  • 插件功能完整且稳定
  • UI界面美观,符合Figma设计规范
  • 已处理所有边界情况(无选中、大量选中、空文档等)
  • 已添加错误处理和用户提示
  • 已测试不同场景(大文件、多页面、团队协作等)
  • 已准备插件图标(32x32 PNG)和封面图(1200x630 PNG)
  • 已编写插件描述和说明文档

10.2 打包插件

# 使用webpack打包生产版本
npx webpack --mode=production

# 或使用官方打包工具
npm run build

# 最终目录结构
dist/
├── manifest.json
├── code.js
└── ui.html

# 将dist目录压缩为zip
zip -r plugin.zip dist/

10.3 发布流程

  1. Figma DesktopPluginsDevelopmentPublish Plugin

  2. 填写插件信息:

    • 名称: 简洁明了,包含关键词
    • 描述: 详细说明功能和使用场景
    • 分类: 选择最相关的分类
    • 标签: 添加相关标签便于搜索
    • 图标: 32x32 PNG,简洁清晰
    • 封面图: 1200x630 PNG,展示插件效果
  3. 隐私与安全:

    • 说明数据收集情况(建议不收集任何用户数据)
    • 提供隐私政策链接(如有)
  4. 提交审核:

    • Figma官方审核通常需要 3-7个工作日
    • 确保描述真实,功能可用
    • 审核通过后插件会出现在Figma Community

10.4 发布后运营

  • 收集反馈: 关注评论和评分,及时修复bug
  • 持续更新: 定期发布新版本,增加功能
  • 文档完善: 提供详细的使用教程和FAQ
  • 社区互动: 在Figma Community和社交媒体上推广

11. 2026年插件开发趋势与AI集成

11.1 AI驱动的插件开发

2026年,AI与Figma插件的结合已经成为主流趋势:

1. AI辅助设计生成

// 使用AI生成UI布局
figma.ui.onmessage = async (msg) => {
  if (msg.type === 'ai-generate') {
    // 调用AI API生成布局数据
    const response = await fetch('https://api.ai-design.com/generate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ prompt: msg.prompt })
    });

    const layout = await response.json();

    // 根据AI返回的数据创建Figma节点
    const frame = figma.createFrame();
    frame.name = 'AI Generated Layout';

    layout.elements.forEach(el => {
      if (el.type === 'button') {
        const btn = figma.createRectangle();
        btn.resize(el.width, el.height);
        btn.fills = [{ type: 'SOLID', color: hexToRgb(el.color) }];
        frame.appendChild(btn);
      }
      // ...
    });
  }
};

2. MCP (Model Context Protocol) 集成

2026年最热门的趋势是通过MCP让AI Agent直接操作Figma:

AI Client (VS Code/Cursor) ←→ MCP Server ←→ WebSocket Bridge ←→ Figma Plugin

这种架构允许AI通过自然语言直接创建和修改设计:

  • “在画布中央创建一个登录表单”
  • “将所有按钮颜色改为品牌蓝色”
  • “导出所有图标为SVG”

3. 设计Token自动化

// 从设计系统自动提取Token
const tokens = {
  colors: extractColorTokens(figma.currentPage),
  spacing: extractSpacingTokens(figma.currentPage),
  typography: extractTypographyTokens(figma.currentPage)
};

// 同步到GitHub
figma.ui.postMessage({
  type: 'sync-tokens',
  tokens: tokens
});

11.2 2026年最佳实践

  1. 支持多产品: 插件同时支持Figma Design、FigJam、Slides
  2. 无障碍设计: 确保插件UI符合WCAG 2.2标准
  3. 性能优先: 处理大量节点时使用Web Worker或分批处理
  4. TypeScript严格模式: 启用strict模式,减少运行时错误
  5. 单元测试: 使用Jest等工具对核心逻辑进行测试

12. 常见问题FAQ

Q1: 插件开发需要付费吗?

A: 完全免费!任何Figma用户都可以开发插件。发布到Figma Community也是免费的,但审核通过后才能公开。

Q2: 主线程为什么不能访问网络?

A: 这是Figma的安全设计。主线程运行在沙盒中,只能访问Figma API。网络请求必须在UI线程(iframe)中完成,然后通过postMessage传递数据。

Q3: 如何调试主线程代码?

A: 使用 console.log() 输出到Figma控制台,或使用 figma.notify() 显示状态信息。也可以在代码中设置断点(通过DevTools的Sources面板)。

Q4: 插件可以修改用户的所有文件吗?

A: 只能修改当前打开的文件。插件没有跨文件访问权限,除非用户明确授权。

Q5: 如何支持中文和其他语言?

A: Figma API完全支持Unicode,可以直接使用中文。但需要注意字体加载问题,确保目标字体已安装。

Q6: 插件可以持久化存储数据吗?

A: 可以!使用 figma.clientStorage 存储键值对数据:

// 存储
await figma.clientStorage.setAsync('user-settings', { theme: 'dark' });

// 读取
const settings = await figma.clientStorage.getAsync('user-settings');

Q7: 如何处理大量节点不卡顿?

A:

  1. 使用 requestAnimationFrame 分批处理
  2. 使用 figma.skipInvisibleInstanceChildren = true 跳过不可见节点
  3. 避免在循环中频繁调用 figma.currentPage
  4. 使用 findAll 替代手动遍历

Q8: 插件UI可以使用第三方库吗?

A: 可以!UI线程是普通iframe,可以使用任何Web技术。但所有资源必须内联到HTML中(通过webpack等工具打包),不能引用外部CDN(除非在manifest中声明)。

Q9: 如何获取插件的安装量数据?

A: 在Figma开发者后台可以查看插件的安装量、使用次数、评分等数据。

Q10: 插件开发有哪些限制?

A:

  • 主线程无法访问网络和DOM
  • UI线程无法直接访问Figma API
  • 单次操作节点数量建议不超过1000个
  • 插件运行时间限制(长时间操作需要分批)
  • 无法访问用户文件系统(除通过UI线程的file input)

🎉 结语与资源汇总

恭喜你读到这里!你已经掌握了Figma插件开发的完整知识体系。从Hello World到React现代开发方案,从基础API到AI集成,你现在有能力开发出任何类型的Figma插件。

📚 推荐资源

资源 链接 说明
官方文档 developers.figma.com 最权威的API参考
插件示例 github.com/figma/plugin-samples 官方示例代码
React脚手架 create-figma-plugin 现代开发工具链
UI组件库 figma-plugin-ds Figma原生风格组件
类型定义 @figma/plugin-typings TypeScript类型支持
社区论坛 forum.figma.com 提问和交流

🚀 下一步行动

  1. 立即实践: 按照本文的Hello World示例,5分钟内跑通你的第一个插件
  2. 解决痛点: 观察你日常设计工作中的重复操作,思考如何用插件自动化
  3. 参与社区: 在GitHub上关注figma/plugin-samples,学习优秀插件源码
  4. 持续迭代: 发布你的第一个插件,收集反馈,不断优化

💡 爆款插件创意方向

  • AI设计助手: 结合GPT-4o/Claude 3.5生成UI布局
  • 设计规范检查器: 自动检测颜色、字体、间距是否符合设计系统
  • 多语言本地化: 一键翻译设计稿并生成多语言版本
  • 代码生成器: 从Figma设计直接生成React/Vue/Tailwind代码
  • 设计Token管理: 双向同步Figma与GitHub的设计Token

👉 Free Perspective - Figma 插件

能力 说明
四角透视 拖拽 TL/TR/BR/BL 四个角,实时 GPU 预览
Mesh 网格变形 3×3 控制点 + B 样条,支持轻微曲面
高质量输出 Canvas 逆映射 + 预乘 Alpha 双线性插值
非破坏性编辑 结果写入 pluginData,可二次编辑
纯本地运行 networkAccess: none,零 CDN 依赖

如果这篇文章对你有帮助,请点赞👍、收藏⭐、关注🔔!你的支持是我持续输出高质量技术内容的动力!

欢迎在评论区交流你的插件开发经验,或者提出你遇到的问题,我会一一解答!

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐