首页
/ Novu figma-use Skill 详解:Figma Plugin API 常用操作模式实战指南

Novu figma-use Skill 详解:Figma Plugin API 常用操作模式实战指南

2026-09-05 16:36:42作者:龚格成

Novu 仓库的 .agents/skills/figma-use/ 目录内置了一个面向 AI Agent 的 Figma 操作技能包,其中 references/plugin-api-patterns.md 是该技能包的核心参考文档之一,系统梳理了通过 Plugin API 在 Figma 文件中创建节点、设置填充与描边、配置 Auto Layout、应用特效、构建组件与变量等高频操作的"可复制即用"模式。读懂本文后,你将掌握在 use_figma 脚本中正确编写 Figma Plugin API 代码的全部关键细节——包括页面上下文切换、执行结果返回、Auto Layout 的致命顺序陷阱、组件变体的网格布局等——并能结合仓库中的 SKILL.mdgotchas.md 规避常见踩坑点。

技能包背景:文档在整个 skill 中的定位

plugin-api-patterns.md 自称"Part of the use_figma skill",它与同目录下的其他参考文档分工协作。从 SKILL.md 的 Reference Docs 表格可以看到官方推荐的加载时机:

文档 加载时机 覆盖内容
gotchas.md 任何 use_figma 调用前 所有已知陷阱及 WRONG/CORRECT 对照
common-patterns.md 需要可运行代码示例时 脚本骨架:图形、文本、Auto Layout、变量、组件
plugin-api-patterns.md 创建/编辑节点时 填充、描边、Auto Layout、特效、分组、克隆、样式
api-reference.md 需要精确 API 面时 节点创建、变量 API、哪些可用哪些不可用
validation-and-recovery.md 多步写入或错误恢复时 校验与错误恢复工作流
component-patterns.md 创建组件/变体时 combineAsVariants、组件属性、INSTANCE_SWAP
plugin-api-standalone.d.ts 需要精确类型签名时 完整类型定义文件,应 grep 而非整读

也就是说,plugin-api-patterns.md 承担"怎么写具体节点操作代码"这一职责;至于 API 的全景能力边界,应配合 plugin-api-standalone.index.md 索引用法来理解。SKILL.md 明确要求:在写任何 Plugin API 代码前,先加载索引文件,再对 .d.ts 做针对性 grep 查找符号——这是一个大型类型文件,不应一次性读入。

执行基础(Execution Basics)

页面上下文:每次调用都从第一页开始

文档开篇就点出一个最重要的执行模型特性:页面上下文在每次 use_figma 调用之间会重置——figma.currentPage 每次调用都从文件的第一页开始。因此切换到目标页必须使用异步方法:

const targetPage = figma.root.children.find(p => p.name === "My Page");
await figma.setCurrentPageAsync(targetPage);
// targetPage.children is now populated

同步赋值 figma.currentPage = page 完全不可用,会直接抛出 "Setting figma.currentPage is not supported"。这一条在 gotchas.md 中同样被列为页面规则陷阱:setCurrentPageAsync(page) 不仅切换页面,还会按需加载该页的内容——这也是为什么 SKILL.md 的关键规则第 9 条称之为"Pages load incrementally"。如果你的工作流跨多次 use_figma 调用且目标是默认页之外的页面,每次调用开头都要重新执行页面切换。

用 return 回传结果

use_figma 的脚本会被自动包装进带错误处理的 async IIFE,所以只需要写普通 JS,用 return 把数据送回 Agent:

// 返回对象——自动序列化为 JSON
return { nodeId: frame.id, count: 5 }

// 返回字符串
return "Created 3 components"

错误会被自动捕获,无需手写 try/catch。文档特别强调 figma.notify() 不存在——所有信息都必须通过 return 值传递。SKILL.md 还进一步收紧了这条规则:每个创建或修改了画布节点的脚本,必须把所有受影响的节点 ID 收集到 return 值里(如 return { createdNodeIds: [...], mutatedNodeIds: [...] }),这是后续调用引用、校验和清理这些节点的前提,而非可选项。

增量式工作

不要在一次调用里构建整个屏幕。文档建议把工作拆成小步:创建 tokens/变量 → 创建文本样式 → 构建单个组件 → 组合区块 → 拼装整屏。每步之间用 get_metadata 校验结构,每个主要创建里程碑后用 get_screenshot 尽早发现视觉问题。这与 SKILL.md 第 6 节的"Incremental Workflow"完全一致:单次调用最多约 10 个逻辑操作,先搭占位骨架(placeholder = true),再逐块填充。

创建节点(Creating Nodes)

创建 Frame

const frame = figma.createFrame();
frame.name = "Container";
frame.resize(1440, 900);
frame.x = 0;
frame.y = 0;
frame.fills = [{ type: "SOLID", color: { r: 0.98, g: 0.98, b: 0.99 } }];

注意颜色分量使用 0–1 区间而非 0–255({r: 1, g: 0, b: 0} 才是纯红)。SKILL.md 的关键规则第 6 条将此列为硬性约定;gotchas.md 给出了对照:写成 r: 255 会直接抛 ZeroToOne 校验错误。

创建文本:必须先加载字体

// MUST load font before any text operations
await figma.loadFontAsync({ family: "Inter", style: "Regular" });

const text = figma.createText();
text.fontName = { family: "Inter", style: "Regular" };
text.fontSize = 16;
text.lineHeight = { value: 24, unit: "PIXELS" };
text.letterSpacing = { value: 0, unit: "PERCENT" };
text.characters = "Hello World";
text.fills = [{ type: "SOLID", color: { r: 0.1, g: 0.1, b: 0.12 } }];

这里有两个易错点。其一,lineHeightletterSpacing 必须使用 {value, unit} 结构而非裸数字,这是 SKILL.md Pre-Flight Checklist 的明确检查项。其二,字体加载的时机比直觉中更靠前:SKILL.md 关键规则第 8 条指出,任何涉及含未加载字体节点的操作(包括 appendChildinsertChildsetBoundVariable 甚至 findAll 回调)都需要字体已加载;如果文件里已有文本节点,应在脚本开头用 figma.listAvailableFontsAsync() 发现可用字体并逐一 loadFontAsync,而不是只在建文本时加载。字体样式名也应先经 listAvailableFontsAsync() 核实,不要凭记忆猜——"SemiBold""Semi Bold" 是常见坑。

矩形、椭圆与直线

const rect = figma.createRectangle();
rect.name = "Background";
rect.resize(400, 300);
rect.cornerRadius = 12;
rect.fills = [{ type: "SOLID", color: { r: 0.95, g: 0.95, b: 0.96 } }];

const circle = figma.createEllipse();
circle.name = "Avatar Circle";
circle.resize(48, 48);
circle.fills = [{ type: "SOLID", color: { r: 0.85, g: 0.87, b: 0.90 } }];

const line = figma.createLine();
line.name = "Divider";
line.resize(400, 0);
line.strokes = [{ type: "SOLID", color: { r: 0, g: 0, b: 0 }, opacity: 0.08 }];
line.strokeWeight = 1;

从 SVG 字符串导入图标

const svgString = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M5 12h14M12 5l7 7-7 7" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;

const node = figma.createNodeFromSvg(svgString);
node.name = "Icon/Arrow Right";
node.resize(24, 24);

figma.createNodeFromSvg() 是程序化放置图标的标准手段,配合命名约定(如 Icon/Arrow Right)与 SKILL.md"先探查文件既有命名规范再创建"的原则保持一致。

填充与描边(Fills & Strokes)

所有颜色遵循 0–1 区间。文档列出的核心形态如下。

纯色 / 带透明度 / 无填充

node.fills = [{ type: "SOLID", color: { r: 0.2, g: 0.2, b: 0.25 } }];
node.fills = [{ type: "SOLID", color: { r: 0.2, g: 0.2, b: 0.25 }, opacity: 0.5 }];
node.fills = [];  // 透明

注意透明度写在 paint 层级opacity: 0.5),而不是 color 对象里加 a 字段——SKILL.md 的 Pre-Flight Checklist 明确要求 paint 的 color 对象只用 {r, g, b}

线性渐变

node.fills = [{
  type: "GRADIENT_LINEAR",
  gradientStops: [
    { color: { r: 0.2, g: 0.36, b: 0.96, a: 1 }, position: 0 },
    { color: { r: 0.56, g: 0.24, b: 0.88, a: 1 }, position: 1 }
  ],
  gradientTransform: [[1, 0, 0], [0, 1, 0]]
}];

描边与多层填充

node.strokes = [{ type: "SOLID", color: { r: 0.85, g: 0.85, b: 0.87 } }];
node.strokeWeight = 1;
node.strokeAlign = "INSIDE";  // 可选 "CENTER", "OUTSIDE"

// 多层填充:自上而下叠加
node.fills = [
  { type: "SOLID", color: { r: 0.95, g: 0.95, b: 0.96 } },
  { type: "SOLID", color: { r: 0.2, g: 0.36, b: 0.96 }, opacity: 0.05 }
];

SKILL.md 关键规则第 7 条补充了一个重要的实现约束:fills/strokes 是只读数组——不能原地修改元素,正确做法是克隆、修改后整体重新赋值(这也是文档示例里全部使用 node.fills = [...] 整体替换写法的原因)。

Auto Layout:最容易出错的区域

优先使用 createAutoLayout()

文档明确建议优先使用 figma.createAutoLayout()——它返回的 frame 已经设置好 layoutMode 且两轴都 hug 内容,子节点可以立即使用 layoutSizingHorizontal/Vertical = "FILL"

const frame = figma.createAutoLayout(); // 默认 HORIZONTAL
const column = figma.createAutoLayout("VERTICAL");

frame.itemSpacing = 16;
frame.paddingTop = 24;
frame.paddingBottom = 24;
frame.paddingLeft = 24;
frame.paddingRight = 24;

如果需要非 Auto Layout 的 frame,用 figma.createFrame() 手动设置:

const frame = figma.createFrame();
frame.layoutMode = "VERTICAL";              // 或 "HORIZONTAL"
frame.resize(360, 1);                       // 宽度固定,高度自动
frame.primaryAxisSizingMode = "AUTO";       // 主轴 hug
frame.counterAxisSizingMode = "FIXED";     // 交叉轴固定

关键顺序:resize() 必须先于 sizing mode

文档用 CRITICAL ORDERING 标注了这个最常见的布局 bug:resize()静默地把两个 sizing mode 都重置为 FIXED。如果在设置 primaryAxisSizingMode = "AUTO" 之后再调 resize(),你的 HUG 设置会被覆盖,frame 被锁定在你传入的精确像素尺寸上——即使是 1 这种"随手值",这正是社区里常见的"1px 尺寸"怪病的根源。

gotchas.md 对此有完整的 WRONG/CORRECT 展开,其"经验法则"是:绝不把 10 这类垃圾值传给一个你打算 HUG 的轴——要么先 resize() 再设 sizing mode,要么给一个即使被静默重置也不会造成视觉 bug 的合理默认值。

对齐

// 主轴
frame.primaryAxisAlignItems = "MIN";            // Start
frame.primaryAxisAlignItems = "CENTER";         // Center
frame.primaryAxisAlignItems = "MAX";            // End
frame.primaryAxisAlignItems = "SPACE_BETWEEN";  // Distribute

// 交叉轴
frame.counterAxisAlignItems = "MIN";     // Start
frame.counterAxisAlignItems = "CENTER";  // Center
frame.counterAxisAlignItems = "MAX";     // End
// 注意:'STRETCH' 不是合法值——用 'MIN' + 子节点 layoutSizingX = 'FILL'

子节点尺寸:FILL 必须在 appendChild 之后设置

// 重要:FILL 只能在子节点挂到 auto-layout 父节点之后设置
parent.appendChild(child)
child.layoutSizingHorizontal = "FILL";   // 撑满父节点
child.layoutSizingHorizontal = "HUG";    // 收缩到内容
child.layoutSizingHorizontal = "FIXED"; // 手动宽度

child.layoutSizingVertical = "FILL";
child.layoutSizingVertical = "HUG";
child.layoutSizingVertical = "FIXED";

SKILL.md 关键规则第 12 条将顺序错误列为会直接抛错的场景:在 parent.appendChild(child) 之前设置 layoutSizingHorizontal/Vertical = 'FILL' 会抛出 "node must be an auto-layout frame..." 错误,这就是其错误恢复表中的标准修复项(把 appendChild 挪到 layoutSizingX = 'FILL' 之前)。gotchas.md 还补充了一个更隐蔽的交互:父节点处于 HUG 时,FILL 子节点会塌缩到最小尺寸(父节点必须是 FIXED 或 FILL),这是 select 下拉框、输入框等场景文本被截断的常见原因。

换行布局与绝对定位

// 类网格的换行布局
frame.layoutMode = "HORIZONTAL";
frame.layoutWrap = "WRAP";
frame.itemSpacing = 24;          // 横向间距
frame.counterAxisSpacing = 24;   // 纵向间距(行间)

// Auto Layout 内部的绝对定位(如右上角角标)
child.layoutPositioning = "ABSOLUTE";
child.constraints = { horizontal: "MAX", vertical: "MIN" };  // 右上角
child.x = parentWidth - childWidth - 8;
child.y = 8;

特效(Effects)

特效数组与填充同构,注意阴影颜色使用四通道 {r, g, b, a}

投影

node.effects = [{
  type: "DROP_SHADOW",
  color: { r: 0, g: 0, b: 0, a: 0.08 },
  offset: { x: 0, y: 4 },
  radius: 16,
  spread: -2,
  visible: true,
  blendMode: "NORMAL"
}];

内阴影 / 背景模糊 / 图层模糊

node.effects = [{
  type: "INNER_SHADOW",
  color: { r: 0, g: 0, b: 0, a: 0.05 },
  offset: { x: 0, y: 1 },
  radius: 2,
  spread: 0,
  visible: true,
  blendMode: "NORMAL"
}];

node.effects = [{ type: "BACKGROUND_BLUR", radius: 16, visible: true }];
node.effects = [{ type: "LAYER_BLUR", radius: 8, visible: true }];

多层特效叠加

node.effects = [
  { type: "DROP_SHADOW", color: { r: 0, g: 0, b: 0, a: 0.04 }, offset: { x: 0, y: 1 }, radius: 3, spread: 0, visible: true, blendMode: "NORMAL" },
  { type: "DROP_SHADOW", color: { r: 0, g: 0, b: 0, a: 0.06 }, offset: { x: 0, y: 8 }, radius: 24, spread: -4, visible: true, blendMode: "NORMAL" }
];

这套"细贴边阴影 + 大弥散阴影"的双层结构是卡片类组件的典型配方;配合后文的 Effect Style,可以把阴影固化为可复用的设计 token。

透明度、混合模式、圆角与裁剪

node.opacity = 0.5;
node.blendMode = "NORMAL";  // "MULTIPLY", "SCREEN", "OVERLAY", "DARKEN", "LIGHTEN" 等

// 圆角:统一
node.cornerRadius = 12;
// 圆角:逐角设置
node.topLeftRadius = 12;
node.topRightRadius = 12;
node.bottomLeftRadius = 0;
node.bottomRightRadius = 0;

// 裁剪:子节点被 frame 边界裁掉
frame.clipsContent = true;

分组与组织(Grouping & Organization)

分组、Section 与子节点插入

const group = figma.group([node1, node2, node3], figma.currentPage);
group.name = "Grouped Elements";

const section = figma.createSection();
section.name = "My Section";
section.resize(800, 600); // Section 上 resize 与 resizeWithoutConstraints 等效
section.x = 0;
section.y = 0;
// 重要:Section 不会自动撑开——添加内容后务必手动 resize

parentFrame.appendChild(childNode);
parentFrame.insertChild(0, childNode);  // 在指定索引处插入

Section 的两个细节值得注意:一是它对 resize / resizeWithoutConstraints 一视同仁;二是内容增多时不会自动扩展,必须在添加内容后再 resize

顶层节点避开 (0,0)

文档隐含了一个前提——所有 figma.create*() 新建的顶层节点都落在 (0,0) 并相互堆叠。gotchas.md 将此列为头号陷阱,标准做法是扫描 figma.currentPage.children 找到最右边界,再把新节点放到右侧留白处(如 frame.x = maxX + 100)。嵌套在其他 frame 或 auto-layout 容器内的子节点由父级定位,无需做重叠扫描。

组件与变体(Components & Variants)

创建组件与实例

const component = figma.createComponent();
component.name = "Button/Primary";
component.description = "Primary action button.";

const instance = component.createInstance();
instance.x = 200;
instance.y = 100;

按 key 导入团队库组件

文档特别提醒:importComponentByKeyAsync 系列方法导入的是团队库(其他文件)中的组件;当前文件内的组件应改用 figma.getNodeByIdAsync()findOne()/findAll()

// 按 key 导入团队库中已发布的组件
const comp = await figma.importComponentByKeyAsync(componentKey)
const instance = comp.createInstance()

// 按 key 导入已发布的组件集(component set)
const set = await figma.importComponentSetByKeyAsync(componentSetKey)
const variant = set.defaultVariant
const variantInstance = variant.createInstance()

combineAsVariants:合并后必须摆网格

// 重要:传入的必须是 ComponentNode(frame 会抛错)
const componentSet = figma.combineAsVariants(
  [variantA, variantB, variantC],
  figma.currentPage
);
componentSet.name = "Button";
componentSet.description = "Button component with multiple variants.";

// 关键:合并后所有变体堆叠在 (0,0),必须手动摆成网格
let maxX = 0, maxY = 0;
componentSet.children.forEach((child, i) => {
  child.x = (i % numCols) * colWidth;
  child.y = Math.floor(i / numCols) * rowHeight;
});
for (const child of componentSet.children) {
  maxX = Math.max(maxX, child.x + child.width);
  maxY = Math.max(maxY, child.y + child.height);
}
componentSet.resizeWithoutConstraints(maxX + 40, maxY + 40);

component-patterns.md 把"合并后摆网格"列为 Required 步骤,并补充:对 size × style × state 这类多轴变体,应从子节点名称解析出网格位置。另一条时序约束同样重要——组件属性必须在 combineAsVariants 之前加到各变体组件上(合并后 component set 从子节点继承全部属性),不要直接向 ComponentSetNode 加属性。

组件属性:返回值是字符串 key

// addComponentProperty 返回字符串 key——务必捕获!
const labelKey = component.addComponentProperty("label", "TEXT", "Button");
const showIconKey = component.addComponentProperty("showIcon", "BOOLEAN", true);
const iconSlotKey = component.addComponentProperty("iconSlot", "INSTANCE_SWAP", defaultIconId);

// 必须通过 componentPropertyReferences 把属性绑定到子节点
labelNode.componentPropertyReferences = { characters: labelKey };
iconInstance.componentPropertyReferences = {
  visible: showIconKey,
  mainComponent: iconSlotKey
};

addComponentProperty 返回的是动态生成的 key 字符串(形如 "label#4:0",后缀不可预测),绝不能硬编码或凭猜测填入 componentPropertyReferencesgotchas.md 还指出了两个进阶错误:把返回值当对象处理(Object.keys(result)[0] 会取到字符串下标 '0'),以及在 COMPONENT_SET 上误用——该 API 在 ComponentSetNode 上同样始终返回 key 字符串。

样式(Styles)

文本样式

await figma.loadFontAsync({ family: "Inter", style: "Regular" });

const style = figma.createTextStyle();
style.name = "Body/Default";
style.fontName = { family: "Inter", style: "Regular" };
style.fontSize = 16;
style.lineHeight = { value: 24, unit: "PIXELS" };
style.letterSpacing = { value: 0, unit: "PERCENT" };

// 应用到文本节点
textNode.textStyleId = style.id;

注意创建/修改文本样式同样要先 loadFontAsync;字体名应通过 listAvailableFontsAsync() 核实而非凭记忆填写(详见 text-style-patterns.md 的字体发现流程)。

特效样式

const shadowStyle = figma.createEffectStyle();
shadowStyle.name = "Shadow/Subtle";
shadowStyle.effects = [{
  type: "DROP_SHADOW",
  color: { r: 0, g: 0, b: 0, a: 0.06 },
  offset: { x: 0, y: 2 },
  radius: 8,
  spread: 0,
  visible: true,
  blendMode: "NORMAL"
}];

// 应用到节点
frame.effectStyleId = shadowStyle.id;

文本样式与特效样式是设计 token 化的两个抓手:把"Body/Default"这类命名规范落到样式上,后续所有文本节点通过 textStyleId 保持一致,阴影通过 effectStyleId 统一控制——对应仓库中 working-with-design-systems 系列文档的主题。

克隆、节点查找与布局网格

克隆与偏移

const clone = originalNode.clone();
clone.x = originalNode.x + originalNode.width + 40;
clone.name = "Copy of " + originalNode.name;

按名称/类型查找节点

// 当前页按名称找单个
const node = figma.currentPage.findOne(n => n.name === "My Frame");

// 按类型找全部
const allTexts = figma.currentPage.findAll(n => n.type === "TEXT");

// 按名称前缀批量找
const allButtons = figma.currentPage.findAll(n => n.name.startsWith("Button/"));

SKILL.md 另推荐了更紧凑的 node.query(selector) 查询 API,可用 CSS 式选择器替代冗长的 findAll + filter 循环,例如 figma.currentPage.query('FRAME[name^=Card] TEXT').set({ fills: [...] }) 可一次性批量更新所有卡片内的文本填充;其 QueryResult 支持 .first().values(['name','x','y']).set(props) 等链式方法,是探查与批量修改场景的高效替代。

布局网格

frame.layoutGrids = [
  {
    pattern: "COLUMNS",
    alignment: "STRETCH",
    count: 12,
    gutterSize: 24,
    offset: 80,
    visible: true
  }
];

12 列网格 + 24px gutter + 80px 边距是响应式布局的常见基准,适合在搭建整屏前先挂到 frame 上作为对齐参照。

约束与视口(Constraints & Viewport)

非 Auto Layout 场景下的约束

child.constraints = {
  horizontal: "LEFT_RIGHT",  // LEFT, RIGHT, CENTER, LEFT_RIGHT, SCALE
  vertical: "TOP"             // TOP, BOTTOM, CENTER, TOP_BOTTOM, SCALE
};

约束只在父 frame 不是 Auto Layout 时才有意义;Auto Layout 容器内用 layoutSizing* 控制尺寸、用 layoutPositioning = "ABSOLUTE" 做局部脱离(见上文"换行布局与绝对定位")。

视口缩放

// 缩放视口以聚焦指定节点
figma.viewport.scrollAndZoomIntoView([frame1, frame2]);

在脚本末尾缩放视口,可以让用户在 Figma 里立刻看到刚创建的内容,是多步工作流的良好收尾动作。

小结:写 use_figma 脚本前的核对清单

plugin-api-patterns.md 的模式与 SKILL.md 第 8 节的 Pre-Flight Checklist 对照,每次提交脚本前可快速自检:

  1. return 回传数据(含全部新建/修改的节点 ID),不用 figma.notify()、不用 console.log()
  2. 代码不包 async IIFE(平台已自动包装),且所有 Promise 都有 await
  3. 颜色用 0–1 区间,paint 的 color 只含 {r, g, b},透明度写在 paint 级 opacity
  4. 切页用 await figma.setCurrentPageAsync(page),同步 setter 必炸;
  5. FILLappendChild 之后设置;resize() 在设置 sizing mode 之前
  6. 任何文本操作前先 loadFontAsynclineHeight/letterSpacing{unit, value}
  7. 顶层新节点避开 (0,0),扫描页内已有内容的右边界再定位。

掌握这套模式后,use_figma 脚本的常见失败来源——页面上下文丢失、字体未加载、尺寸顺序错误、变体堆叠在 (0,0)——都有明确的预防手段;遇到错误时记住 SKILL.md 第 7 节的原则:use_figma 是原子的,失败脚本不会执行,先读错误信息、必要时用 get_metadata/get_screenshot 观察文件现状,修好再重试即可,无需担心半成品节点残留。

登录后查看全文
热门项目推荐
相关项目推荐