首页
/ Novu use_figma 技能参考:Figma Plugin API 常用脚本模式详解(common-patterns)

Novu use_figma 技能参考:Figma Plugin API 常用脚本模式详解(common-patterns)

2026-09-05 13:40:33作者:范垣楠Rhoda

本文基于 Novu 仓库中 .agents/skills/figma-use/references/common-patterns.md 这份技能参考文档展开。它是 use_figma 技能(Figma Plugin API 自动化脚本执行)的"可直接复用的代码骨架库",覆盖脚本返回值结构、节点创建、自动布局、变量与模式(Modes)、组件变体、团队库导入以及大型组件集的多步拆分等 10 类高频操作。读完本文,你可以直接掌握在 Figma 文件上下文内编写、执行并串联多轮 Plugin API 脚本的完整套路,知道每种操作的正确代码形态、关键约束和常见陷阱。

文档定位:它是 use_figma 技能的"工作代码示例库"

common-patterns.mduse_figma 技能 参考文档集的一部分,在 SKILL.md 的参考文档索引中被定义为:

Need working code examples — Script scaffolds: shapes, text, auto-layout, variables, components, multi-step workflows

也就是说,它不是 API 字典(那是 plugin-api-standalone.d.ts 的职责),而是一组已经过验证、可直接复制修改的最小工作示例,每个示例解决一类高频场景:

章节(原文档) 解决的问题
Basic Script Structure 脚本如何把数据传回调用方
Create a Styled Shape / Text Node / Frame with Auto-Layout 基础节点的创建与定位
Create Variable Collections and Bindings 创建多模式变量集并绑定到填充
Create Components and Import by Key 组件变体属性、团队库组件导入
Component Sets with Variable Modes 变体命名与变量模式联合使用
Multi-Step Large ComponentSet Pattern 50+ 变体的分多次调用拆分策略
Read Existing Nodes and Return Data 只读巡检脚本的写法

在 Novu 仓库中,该技能由 skills-lock.json 登记为来自 figma/mcp-server-guide 的同步技能(figma-use),说明其内容遵循 Figma 官方 MCP 工具链的约定。技能规则明确要求:每次调用 use_figma 工具前必须先加载该技能,并始终传 skillNames: "figma-use" 参数用于使用追踪(该参数不影响执行)。

理解这些示例前,先记住三条贯穿所有代码的硬规则(来自 SKILL.md 的 Critical Rules,也与本文所有示例的形态直接对应):

  1. return 是唯一输出通道。返回值被自动 JSON 序列化;console.log() 的输出永远不会传回调用方,不要调用 figma.closePlugin(),也不要自己包 async IIFE(代码会自动包在 async 上下文中,顶层 await 可直接用)。
  2. 失败是原子的。脚本一旦报错就完全不执行、文件零改动,所以正确姿势是先读懂报错、修正脚本再重试,而不是盲目重试。
  3. 小步增量推进。每次调用最多做 10 个左右逻辑操作,创建/修改后把节点 ID 返回,作为下一次调用的输入——这正是下面"Basic Script Structure"存在的意义。

基本脚本结构:return 结构化的 ID 追踪

原文档给出的第一例是所有脚本的骨架:

const createdNodeIds = []
const mutatedNodeIds = []

// Your code here — track every node you create or mutate
// createdNodeIds.push(newNode.id)
// mutatedNodeIds.push(existingNode.id)

return {
  success: true,
  createdNodeIds,
  mutatedNodeIds,
  // Plus any other useful data for subsequent calls
  count: createdNodeIds.length
}

要点拆解:

  • 每个脚本维护两个数组:createdNodeIds(本次新建的节点)与 mutatedNodeIds(本次修改的既有节点)。这是硬性要求而非建议——后续调用要靠这些 ID 去引用、校验和清理这些节点。
  • 返回值可以附带任意"对后续调用有用"的数据,如数量统计、集合 ID、模式 ID 等。
  • gotchas.md 中的反例对照:只返回父级 frame 的 ID、丢失子节点 ID 是典型错误;正确做法是把 frame、rect、text 三个 ID 都放进 createdNodeIds,并额外给出 rootNodeId 方便定位。

基础节点创建:先找"空地",再放节点

创建带样式的形状

所有"追加到当前页面"的示例都复用同一段空位扫描逻辑:

// Find clear space to the right of existing content
const page = figma.currentPage
let maxX = 0
for (const child of page.children) {
  maxX = Math.max(maxX, child.x + child.width)
}

const rect = figma.createRectangle()
rect.name = "Blue Box"
rect.resize(200, 100)
rect.fills = [{ type: 'SOLID', color: { r: 0.047, g: 0.549, b: 0.914 } }]
rect.cornerRadius = 8
rect.x = maxX + 100  // offset from existing content
rect.y = 0
figma.currentPage.appendChild(rect)
return { nodeId: rect.id }

这段代码体现了几条关键约定:

  • 新顶层节点默认落在 (0,0)。直接 appendChild 到页面会让多个新节点互相堆叠、并盖住既有内容。因此先遍历 figma.currentPage.children,用 Math.max(child.x + child.width) 求出最右边界,再以 maxX + 100 的间距放置。注意这只对"直接挂在页面上的顶层节点"必要;放进 frame 或自动布局容器的子节点由父级排布,无需扫描。
  • 颜色是 0–1 浮点域而非 0–255,例如 {r: 0.047, g: 0.549, b: 0.914} 表示一种蓝色;写成 {r: 12, g: 140, b: 233} 会直接触发校验报错。
  • fills 是整体重赋值,不是原地修改——示例中 rect.fills = [...] 一次性替换整个数组。
  • 创建后返回 { nodeId: rect.id },供后续调用引用。

创建文本节点

// Find clear space to the right of existing content
const page = figma.currentPage
let maxX = 0
for (const child of page.children) {
  maxX = Math.max(maxX, child.x + child.width)
}

await figma.loadFontAsync({ family: "Inter", style: "Regular" })
const text = figma.createText()
text.characters = "Hello World"
text.fontSize = 16
text.fills = [{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }]
text.textAutoResize = 'WIDTH_AND_HEIGHT'
text.x = maxX + 100
text.y = 0
figma.currentPage.appendChild(text)
return { nodeId: text.id }

文本节点比形状多一条前置约束:figma.loadFontAsync() 必须先于任何文本操作SKILL.md 的规则 8 强调,这不仅是"设置文字"之前,而是包括 appendChildinsertChildsetBoundVariablesetExplicitVariableModeForCollection 在内的任何触碰含未加载字体节点的操作之前。如果文档中已存在文本节点,建议在脚本开头用 await figma.listAvailableFontsAsync() 发现可用字体后预加载全部字体,完整的预加载模式见 gotchas.md。另外注意字体样式名必须与实际发布名一致,"SemiBold""Semi Bold" 的差异是经典踩坑点。

本例还展示了 textAutoResize = 'WIDTH_AND_HEIGHT'(宽高都随内容自适应)的用法:文本节点不需要手动 resize

创建带自动布局的 Frame

// Find clear space to the right of existing content
const page = figma.currentPage
let maxX = 0
for (const child of page.children) {
  maxX = Math.max(maxX, child.x + child.width)
}

const frame = figma.createAutoLayout('VERTICAL')
frame.name = "Card"
frame.primaryAxisAlignItems = 'MIN'
frame.counterAxisAlignItems = 'MIN'
frame.paddingLeft = 16
frame.paddingRight = 16
frame.paddingTop = 12
frame.paddingBottom = 12
frame.itemSpacing = 8
frame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]
frame.cornerRadius = 8
frame.x = maxX + 100
frame.y = 0
figma.currentPage.appendChild(frame)
return { nodeId: frame.id }

这里的关键 API 是 figma.createAutoLayout('VERTICAL')——它是 figma.createFrame() 的"一步到位"替代:创建出的 frame 已启用自动布局且两轴默认 HUG 内容。SKILL.md 明确建议"任何需要自动布局的容器都优先用 createAutoLayout,不要手写 layoutMode + primaryAxisSizingMode + layoutSizingX 的多步配置",因为这既啰嗦又容易踩顺序坑。该 API 在类型定义文件中也有正式声明,见 plugin-api-standalone.d.ts#L1083-L1093

属性语义速读:

  • primaryAxisAlignItems = 'MIN':主轴(垂直方向)顶部对齐;
  • counterAxisAlignItems = 'MIN':交叉轴(水平方向)左侧对齐;
  • paddingLeft/Right/Top/Bottom = 16/16/12/12itemSpacing = 8:内边距与子项间距,单位是 px 数值;
  • 创建后子节点可以直接设 layoutSizingHorizontal = 'FILL' 撑满,前提是 appendChild 已完成(FILL 必须在挂到自动布局父级之后设置,提前设置会抛错)。

变量系统:多模式集合与填充绑定

创建带多个模式的变量集合

const collection = figma.variables.createVariableCollection("Theme/Colors")
// Rename the default mode
collection.renameMode(collection.modes[0].modeId, "Light")
const darkModeId = collection.addMode("Dark")
const lightModeId = collection.modes[0].modeId

const bgVar = figma.variables.createVariable("bg", collection, "COLOR")
bgVar.setValueForMode(lightModeId, { r: 1, g: 1, b: 1, a: 1 })
bgVar.setValueForMode(darkModeId, { r: 0.1, g: 0.1, b: 0.1, a: 1 })

const textVar = figma.variables.createVariable("text", collection, "COLOR")
textVar.setValueForMode(lightModeId, { r: 0, g: 0, b: 0, a: 1 })
textVar.setValueForMode(darkModeId, { r: 1, g: 1, b: 1, a: 1 })

return {
  collectionId: collection.id,
  lightModeId,
  darkModeId,
  bgVarId: bgVar.id,
  textVarId: textVar.id
}

流程解析:

  1. createVariableCollection("Theme/Colors") 创建集合,名称用斜杠表达层级;
  2. 新集合自带一个默认模式,用 collection.renameMode(collection.modes[0].modeId, "Light") 重命名为 Light,再 collection.addMode("Dark") 追加 Dark 模式;
  3. createVariable("bg", collection, "COLOR") 创建颜色变量(第三个参数是变量类型,此处为 COLOR),注意 SKILL.md 规则 11:createVariable 的集合参数既可传对象也可传 ID 字符串,推荐传对象;
  4. 每个模式用 setValueForMode(modeId, {r,g,b,a}) 独立赋值,实现 Light/Dark 双主题;
  5. 把 collection、mode、variable 的 ID 全部 return——多步工作流的后续调用要靠这些字符串字面量取回对象。

补充一点 SKILL.md 规则 16 的要求:创建变量时应显式设置 variable.scopes(如背景用 ["FRAME_FILL", "SHAPE_FILL"]、文字色用 ["TEXT_FILL"]、间距用 ["GAP"]),因为默认的 ALL_SCOPES 会污染所有属性选择器。完整 scope 清单见 variable-patterns.md

把颜色变量绑定到填充

const variable = await figma.variables.getVariableByIdAsync("VariableID:1:2")
const rect = figma.createRectangle()
const basePaint = { type: 'SOLID', color: { r: 0, g: 0, b: 0 } }

// setBoundVariableForPaint returns a NEW paint — capture it!
const boundPaint = figma.variables.setBoundVariableForPaint(basePaint, "color", variable)
rect.fills = [boundPaint]

return { nodeId: rect.id }

这个示例浓缩了两条最容易被忽视的规则:

  • 跨调用取变量:上一轮 return 出的 bgVarId 以字符串字面量(如 "VariableID:1:2")传入本轮,用 await figma.variables.getVariableByIdAsync(id) 恢复为对象。
  • setBoundVariableForPaint 返回的是一个全新的 paint 对象,必须捕获返回值再赋给 rect.fills。它不会修改 basePaint 本身。这是 SKILL.md 规则 10 的原文:"returns a NEW paint — must capture and reassign"。该 API 在类型定义中的声明位置见 plugin-api-standalone.d.ts#L2157

basePaint 里的具体颜色值在这里只是占位——绑定后实际渲染颜色以变量值为准,所以示例统一写成黑色基座。

组件与变体:属性、导入与变量模式

带组件属性的变体创建

原文档在此节开头给出了一条总规则(加粗强调):组件属性(TEXT、BOOLEAN、INSTANCE_SWAP)必须在每个变体的循环体内、combineAsVariants 之前添加,组件集(combine 后的 COMPONENT_SET)会从子组件继承这些属性。完整示例:

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

// Assume defaultIconComp is an existing icon component (discovered earlier)
const defaultIconComp = figma.getNodeById('ICON_COMPONENT_ID')

const components = []
const variants = ["primary", "secondary"]

for (const variant of variants) {
  const comp = figma.createComponent()
  comp.name = `variant=${variant}`
  comp.layoutMode = 'HORIZONTAL'
  comp.primaryAxisAlignItems = 'CENTER'
  comp.counterAxisAlignItems = 'CENTER'
  comp.paddingLeft = 12
  comp.paddingRight = 12
  comp.paddingTop = 8
  comp.paddingBottom = 8
  comp.layoutSizingHorizontal = 'HUG'
  comp.layoutSizingVertical = 'HUG'
  comp.cornerRadius = 6
  comp.itemSpacing = 8

  // TEXT property — label
  const labelKey = comp.addComponentProperty('Label', 'TEXT', 'Button')
  const label = figma.createText()
  label.characters = "Button"
  label.fontSize = 14
  comp.appendChild(label)
  label.componentPropertyReferences = { characters: labelKey }

  // BOOLEAN + INSTANCE_SWAP — icon slot
  const showIconKey = comp.addComponentProperty('Show Icon', 'BOOLEAN', false)
  const iconSlotKey = comp.addComponentProperty('Icon', 'INSTANCE_SWAP', defaultIconComp.id)
  const iconInstance = defaultIconComp.createInstance()
  comp.insertChild(0, iconInstance)  // icon before label
  iconInstance.componentPropertyReferences = {
    visible: showIconKey,
    mainComponent: iconSlotKey
  }

  components.push(comp)
}

const componentSet = figma.combineAsVariants(components, figma.currentPage)
componentSet.name = "Button"

// Layout variants in a row after combining (they stack at 0,0 by default)
const colW = 140
componentSet.children.forEach((child, i) => {
  child.x = i * colW
  child.y = 0
})
// Resize from actual child bounds — formula-based sizing is error-prone
let maxX = 0, maxY = 0
for (const c of componentSet.children) {
  maxX = Math.max(maxX, c.x + c.width)
  maxY = Math.max(maxY, c.y + c.height)
}
componentSet.resizeWithoutConstraints(maxX + 40, maxY + 40)

return {
  componentSetId: componentSet.id,
  componentIds: components.map(c => c.id)
}

逐点解析:

  • 命名约定 variant=${variant}:变体名即属性声明,primary/secondary 会构成一个名为 variant 的属性轴。组件集排版代码正是靠解析这种 key=value, key=value 命名来定位网格坐标的。
  • TEXT 属性:comp.addComponentProperty('Label', 'TEXT', 'Button') 返回值就是属性 key 字符串(形如 "label#4:0",后缀不可预测),直接用于 label.componentPropertyReferences = { characters: labelKey } 把文本节点的 characters 关联到该属性。gotchas.md 专门用 WRONG/CORRECT 示例警告:不要猜测 key、不要把返回值当对象取 Object.keys()(那会得到字符串首字符索引 '0')。
  • BOOLEAN + INSTANCE_SWAP 组合:'Show Icon' 是布尔开关,'Icon' 是实例交换槽位。iconInstance.componentPropertyReferences 同时挂 visible: showIconKey(控制可见性)和 mainComponent: iconSlotKey(控制可替换的主组件),这是"图标槽位 + 显隐开关"的标准写法。comp.insertChild(0, iconInstance) 保证图标排在标签之前。
  • combineAsVariants(components, figma.currentPage) 把独立组件合并为 COMPONENT_SET(类型定义见 plugin-api-standalone.d.ts#L1742-L1749),之后 componentSet.name = "Button" 重命名组件集。
  • 合并后变体全部堆在 (0,0):必须手动按 i * colW 排开;并用 resizeWithoutConstraints 依据实际子节点包围盒外扩 40px 重设尺寸——原文档特意注明"formula-based sizing is error-prone"(用公式推算组件集尺寸容易出错,应以真实子节点边界为准)。

按 Key 导入团队库组件

// Import a single published component by key
const comp = await figma.importComponentByKeyAsync("COMPONENT_KEY")
const instance = comp.createInstance()
instance.x = 40
instance.y = 40
figma.currentPage.appendChild(instance)

// Import a published component set by key and select a variant
const compSet = await figma.importComponentSetByKeyAsync("COMPONENT_SET_KEY")
const variant =
  compSet.children.find((c) =>
    c.type === "COMPONENT" && c.name.includes("size=md")
  ) || compSet.defaultVariant

const variantInstance = variant.createInstance()
variantInstance.x = 240
variantInstance.y = 40
figma.currentPage.appendChild(variantInstance)

return {
  componentId: comp.id,
  componentSetId: compSet.id,
  placedInstanceIds: [instance.id, variantInstance.id]
}

原文档在此节明确划了一条边界:importComponentByKeyAsyncimportComponentSetByKeyAsync 导入的是团队库(team libraries)中已发布的组件,而不是当前文件里的组件;当前文件内的组件应直接用 figma.getNodeByIdAsync()findOne()/findAll() 定位。变体选择用"按 size=md 命名查找 + compSet.defaultVariant 兜底"的双保险写法,再对选中的变体调用 createInstance()。返回值同时登记组件 ID 与实例 ID(placedInstanceIds),符合"返回所有创建/修改节点 ID"的硬规则。

组件集 + 变量模式的完整模式

这个示例把"变体轴"与"变量模式"对齐:primary/secondary 两个变体分别锁定变量集合中对应的模式。

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

// 1. Create color collection with modes per variant
const colors = figma.variables.createVariableCollection("Component/Colors")
colors.renameMode(colors.modes[0].modeId, "primary")
const primaryMode = colors.modes[0].modeId
const secondaryMode = colors.addMode("secondary")

const bgVar = figma.variables.createVariable("bg", colors, "COLOR")
bgVar.setValueForMode(primaryMode, { r: 0, g: 0.4, b: 0.9, a: 1 })
bgVar.setValueForMode(secondaryMode, { r: 0, g: 0, b: 0, a: 0 })

const textVar = figma.variables.createVariable("text-color", colors, "COLOR")
textVar.setValueForMode(primaryMode, { r: 1, g: 1, b: 1, a: 1 })
textVar.setValueForMode(secondaryMode, { r: 0.1, g: 0.1, b: 0.1, a: 1 })

// 2. Create components with variable bindings
const modeMap = { primary: primaryMode, secondary: secondaryMode }
const components = []

for (const [variantName, modeId] of Object.entries(modeMap)) {
  const comp = figma.createComponent()
  comp.name = "variant=" + variantName
  comp.layoutMode = "HORIZONTAL"
  comp.primaryAxisAlignItems = "CENTER"
  comp.counterAxisAlignItems = "CENTER"
  comp.paddingLeft = 12; comp.paddingRight = 12
  comp.layoutSizingHorizontal = "HUG"
  comp.layoutSizingVertical = "HUG"
  comp.cornerRadius = 6

  // Bind background fill to variable
  const bgPaint = figma.variables.setBoundVariableForPaint(
    { type: "SOLID", color: { r: 0, g: 0, b: 0 } }, "color", bgVar
  )
  comp.fills = [bgPaint]

  // Add text with bound color
  const label = figma.createText()
  label.fontName = { family: "Inter", style: "Medium" }
  label.characters = "Button"
  label.fontSize = 14
  const textPaint = figma.variables.setBoundVariableForPaint(
    { type: "SOLID", color: { r: 0, g: 0, b: 0 } }, "color", textVar
  )
  label.fills = [textPaint]
  comp.appendChild(label)

  // 3. CRITICAL: Set explicit mode so this variant renders correctly
  comp.setExplicitVariableModeForCollection(colors, modeId)

  components.push(comp)
}

// 4. Combine into component set
const componentSet = figma.combineAsVariants(components, figma.currentPage)
componentSet.name = "Button"

return {
  componentSetId: componentSet.id,
  colorCollectionId: colors.id
}

四步结构与关键细节:

  1. 变量集合的模式与变体一一对应:primary 模式背景为蓝色 {r:0, g:0.4, b:0.9}、文字白色;secondary 模式背景透明 {r:0, g:0, b:0, a:0}、文字深灰。注意 a 字段在变量值层面是允许的(与纯色 paint 的 color 不带 a、透明度放在 paint 层的 opacity 上不同)。
  2. 每个组件内完成绑定:背景 comp.fills 和文字 label.fills 都用 setBoundVariableForPaint 生成新 paint 后整体赋值,前文"捕获返回值"规则再次出现。文本节点在设置 characters/fontSize 前先 label.fontName = { family: "Inter", style: "Medium" },与脚本开头的 loadFontAsync 呼应。
  3. 最关键的一步 comp.setExplicitVariableModeForCollection(colors, modeId):原文档以 "CRITICAL" 标注——不显式锁定模式,该变体渲染时可能取到错误模式下的变量值,导致 primary 变体显示出 secondary 的配色。
  4. 合并与命名:combineAsVariants + componentSet.name = "Button",return 组件集 ID 和颜色集合 ID 两条线索,供下一轮调用(如排版、截图验证)使用。

大型组件集:多步拆分模式(Multi-Step)

对于 50+ 变体的组件集,原文档给出策略:拆成多次 use_figma 调用,每次只做一件事,用 return 的 ID 作为下一轮的输入字面量。这直接呼应 SKILL.md 中"Incremental Workflow"一节"每次调用最多 10 个逻辑操作、每步验证后再前进"的原则。

第 1 次调用:建变量集合,返回全部 ID

// Hex-to-0-1 helper
const hex = (h) => {
  if (!h) return { r: 0, g: 0, b: 0, a: 0 }; // transparent
  return {
    r: parseInt(h.slice(1,3), 16) / 255,
    g: parseInt(h.slice(3,5), 16) / 255,
    b: parseInt(h.slice(5,7), 16) / 255,
    a: 1
  };
};

const coll = figma.variables.createVariableCollection("MyComponent/Colors");
coll.renameMode(coll.modes[0].modeId, "mode1");
const mode2Id = coll.addMode("mode2");

// Create variables from data map
const colorData = { "bg/default": ["#0B6BCB", "#636B74"], /* ... */ };
const modeOrder = ["mode1", "mode2"];
const modeIds = { mode1: coll.modes[0].modeId, mode2: mode2Id };
const varIds = {};

for (const [name, values] of Object.entries(colorData)) {
  const v = figma.variables.createVariable(name, coll, "COLOR");
  values.forEach((hex_val, i) => {
    v.setValueForMode(modeIds[modeOrder[i]], hex_val ? hex(hex_val) : { r:0, g:0, b:0, a:0 });
  });
  varIds[name] = v.id;
}

// Return ALL IDs — needed by subsequent calls
return { collId: coll.id, modeIds, varIds };

工程化要点:

  • hex 辅助函数:把 #RRGGBB 十六进制拆成三段 parseInt(x, 16) / 255 归一到 0–1;空值返回全透明 {r:0, g:0, b:0, a:0},让数据表可以直接表达"该模式无此颜色"。
  • 数据驱动:颜色定义集中在 colorData 映射(键如 "bg/default" 编码了"用途/状态"),循环统一创建变量并逐模式赋值,避免为每个变量手写一段 setValueForMode
  • return { collId, modeIds, varIds }:集合 ID、模式 ID 表、变量 ID 表全部交回,这是第 2 次调用的全部依赖。

第 2 次调用:用存储的 ID 建组件、合并、排版

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

// Paste IDs from Call 1 as literals
const collId = "VariableCollectionId:X:Y";
const modeIds = { mode1: "X:0", mode2: "X:1" };
const varIds = { /* ... from Call 1 ... */ };

const getVar = async (id) => await figma.variables.getVariableByIdAsync(id);
const bindColor = async (varId) => figma.variables.setBoundVariableForPaint(
  { type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', await getVar(varId)
);
const collection = await figma.variables.getVariableCollectionByIdAsync(collId);

const components = [];
for (const mode of ["mode1", "mode2"]) {
  for (const state of ["default", "hover"]) {
    const comp = figma.createComponent();
    comp.name = `mode=${mode}, state=${state}`;
    comp.layoutMode = 'HORIZONTAL';
    comp.primaryAxisAlignItems = 'CENTER';
    comp.counterAxisAlignItems = 'CENTER';
    comp.layoutSizingHorizontal = 'HUG';
    comp.layoutSizingVertical = 'HUG';
    comp.fills = [await bindColor(varIds[`bg/${state}`])];
    comp.setExplicitVariableModeForCollection(collection, modeIds[mode]);
    // ... add text children ...
    components.push(comp);
  }
}

// Combine — all children stack at (0,0)!
const cs = figma.combineAsVariants(components, figma.currentPage);
cs.name = "MyComponent";

// CRITICAL: layout variants in a structured grid mapped to variant axes.
const stateOrder = ["default", "hover"];
const modeOrder2 = ["mode1", "mode2"];
const colW = 140, rowH = 56;

for (const child of cs.children) {
  const props = Object.fromEntries(
    child.name.split(', ').map(p => p.split('='))
  );
  const col = stateOrder.indexOf(props.state);
  const row = modeOrder2.indexOf(props.mode);
  child.x = col * colW;
  child.y = row * rowH;
}
// Resize from actual child bounds
let maxX = 0, maxY = 0;
for (const child of cs.children) {
  maxX = Math.max(maxX, child.x + child.width);
  maxY = Math.max(maxY, child.y + child.height);
}
cs.resizeWithoutConstraints(maxX + 40, maxY + 40);

// Wrap in section
const section = figma.createSection();
section.name = "MyComponent Section";
section.appendChild(cs);
section.resize(cs.width + 200, cs.height + 200);

return { csId: cs.id, count: components.length };

这段是全文档信息密度最高的示例,值得逐段对照:

  • ID 以字符串字面量粘贴:注释 "Paste IDs from Call 1 as literals" 点破了多步调用的数据传递方式——跨调用没有共享变量,第 1 次 return 的 JSON 由调用方原样嵌入第 2 次脚本。SKILL.md 的 Pre-Flight Checklist 中也有对应条目:"IDs from previous calls are passed as string literals (not variables)"。
  • 两个小工具函数收拢样板:getVar 封装 getVariableByIdAsync, bindColor 把"取变量 + 生成绑定 paint"压缩成一步,comp.fills = [await bindColor(...)] 即可。
  • 双轴变体命名:mode=${mode}, state=${state} 声明了 modestate 两个属性轴;后续排版代码用 child.name.split(', ').map(p => p.split('=')) + Object.fromEntries 把名字反解析成属性表,再按 stateOrder/modeOrder2 查行列索引,把变体摆进"state 为列、mode 为行"的网格(child.x = col * colW; child.y = row * rowH)。这比单行排列更适合多变体集,也让网格对人类和后续脚本都可读。
  • 再次出现的三条纪律:合并后子节点全在 (0,0) 需要手动排版;尺寸用 resizeWithoutConstraints 从实际包围盒外扩得出;setExplicitVariableModeForCollection 在每个组件上逐一调用。
  • 用 Section 包裹:figma.createSection() 建分区并 appendChild(cs) 后按 +200 外扩 resize,给组件集留出画布余量,这是交付前的组织动作。

读取既有节点:只读巡检脚本

最后一个示例展示了"只读调用"——不创建任何节点,只把结构数据 return 回去:

const page = figma.currentPage
const nodes = page.findAll(n => n.type === 'FRAME')
const data = nodes.map(n => ({
  id: n.id,
  name: n.name,
  width: n.width,
  height: n.height,
  childCount: n.children?.length || 0
}))
return { frames: data }

它的价值在于配合 SKILL.md 增量工作流的第 1 步 Inspect first:写任何创建脚本前,先跑一轮只读脚本摸清文件里已有哪些 frame、命名习惯和层级,让新内容"匹配现状而不是强加新约定"。同一技能中还提供了列出全部页面/组件/变量集合的巡检脚本,可作为扩展模板。

模式速查:何时用哪段骨架

场景 核心 API 对应章节
任何脚本收尾 结构化 return { createdNodeIds, mutatedNodeIds, ... } 基本脚本结构
新建顶层节点 空位扫描 maxX + 100 定位 形状/文本/Frame 示例
文本 loadFontAsync,后 createText 文本节点
容器 figma.createAutoLayout('VERTICAL') 自动布局 Frame
主题变量 createVariableCollection + renameMode/addMode + setValueForMode 变量集合
变量上色 setBoundVariableForPaint 返回值再赋 fills 填充绑定
变体属性 循环内 addComponentProperty,再 combineAsVariants 组件变体
团队库复用 importComponentByKeyAsync / importComponentSetByKeyAsync 按 Key 导入
变体 × 模式 每组件 setExplicitVariableModeForCollection 变量模式组件集
50+ 变体 调用 1 建变量 return ID,调用 2 粘贴字面量建组件 多步模式
写前侦察 page.findAll + return 结构数据 读取节点

需要进一步深入时,建议按 SKILL.md 第 10 节的参考文档索引继续加载:plugin-api-patterns.md(fills、strokes、effects 等节点细节)、variable-patterns.md(scopes 与别名)、component-patterns.md(INSTANCE_SWAP 与变体排版)、text-style-patterns.md(字体发现与文字样式)、gotchas.md(全部已知坑位的 WRONG/CORRECT 对照),以及作为 API 权威来源的 plugin-api-standalone.d.ts(建议按符号 grep 而不是整文件加载)。

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