首页
/ Novu 开源仓库实战:use_figma 的 Figma 插件 API 避坑指南 —— 常见错误与正确写法全解析

Novu 开源仓库实战:use_figma 的 Figma 插件 API 避坑指南 —— 常见错误与正确写法全解析

2026-09-08 15:56:42作者:霍妲思

在 Novu 开源仓库的 .agents/skills 中维护着一套面向 Agent 的 use_figma 技能,它允许通过纯 JavaScript 脚本在 Figma 文件中执行创建、编辑、变量绑定与设计系统构建等操作。本文以技能参考文档 gotchas.md 为骨架,系统梳理了 use_figma 场景下最常踩中的 30 余个陷阱,并为每一条提供 WRONG(错误)与 CORRECT(正确)双份可运行代码示例。读完本文,你将掌握颜色取值、Auto Layout、组件/变体、Variable 绑定、页面切换、节点尺寸与布局等方向的正确调用姿势,能有效规避“脚本报错”“静默失效”“布局错乱”三大类高频故障,并在出错后依据错误信息快速自愈。

本文主体对应技能参考文档 .agents/skills/figma-use/references/gotchas.md;技能总则见 SKILL.md,类型签名以 plugin-api-standalone.d.ts 为准(该文件为约 1.1 万行的类型声明,可按符号名 grep 定位)。

目录(按主题归类)

  • 画布创建与节点定位:新建节点默认落在 (0,0)、reparenting 后坐标不重置、网格混排重叠、Sections 不自动缩放
  • 组件属性与变体:addComponentProperty 返回字符串 key、combineAsVariants 的两个前提、变体网格需手动布局
  • Paint 与颜色:0–1 取值域、数组不可变、paint color 不含 a、空 fills 无法绑定变量
  • 页面上下文与脚本生命周期:currentPage 同步赋值不可用、脚本必须 return、禁止 figma.notify()getPluginData 不可用
  • Auto Layout 与尺寸顺序:FILL 需先入父容器、HUG 父容器压扁 FILL 子节点、layoutGrow 压缩、resize() 重置 sizing mode、width/height 只读
  • 字体与排版:lineHeight/letterSpacing 必须是对象、字体样式名需通过 listAvailableFontsAsync 发现
  • Variable 作用域与模式:集合自带 1 个 mode、作用域默认 ALL_SCOPES、显式 mode 需逐组件设置、模式数量受套餐限制、CSS 变量名不含空格
  • 类型安全与属性写入:类型守卫前置、对象不可扩展错误、detachInstance() 使祖先 ID 失效

一、新建节点默认落在 (0,0) —— 顶层节点必须手动避让

figma.create*() 创建出的节点坐标默认为 (0,0)。如果直接把多个节点 append 到页面,它们会互相重叠,并压住页面已有内容。

需要注意:这条规则只对直接 append 到页面的顶层节点生效。作为 Frame、Component 或 Auto Layout 容器子节点加入的节点,其位置由父容器负责,嵌套场景无需做重叠扫描。

// WRONG —— 顶层节点落在 (0,0),与已有页面内容重叠
const frame = figma.createFrame()
frame.name = "My New Frame"
frame.resize(400, 300)
figma.currentPage.appendChild(frame)

// CORRECT —— 先扫描已有内容的边界,把新顶层节点放到最右侧
const page = figma.currentPage
let maxX = 0
for (const child of page.children) {
  const right = child.x + child.width
  if (right > maxX) maxX = right
}
const frame = figma.createFrame()
frame.name = "My New Frame"
frame.resize(400, 300)
figma.currentPage.appendChild(frame)
frame.x = maxX + 100  // 与最右侧内容保持 100px 间距
frame.y = 0

// NOT NEEDED —— 容器内的子节点无需重叠扫描
const card = figma.createAutoLayout('VERTICAL')
const label = figma.createText()
card.appendChild(label)  // 由 auto-layout 定位,无需设置 x/y

use_figma 技能规则第 13 条也明确要求“将新顶层节点放在 (0,0) 之外”,并指向本文的避让示例。

二、addComponentProperty 返回的是字符串 key —— 严禁硬编码或猜测

Figma 会动态生成组件属性的 key(例如 "label#4:0"),后缀不可预测。必须捕获返回值并直接使用

// WRONG —— 猜测 / 硬编码 key
comp.addComponentProperty('label', 'TEXT', 'Button')
labelNode.componentPropertyReferences = { characters: 'label#0:1' }  // Error: key not found

// WRONG —— 把返回值当成对象处理
const result = comp.addComponentProperty('Label', 'TEXT', 'Button')
const propKey = Object.keys(result)[0]  // BUG: 返回 '0'(字符串的首个字符下标!)
labelNode.componentPropertyReferences = { characters: propKey }  // Error: property '0' not found

// CORRECT —— 返回值本身就是 key 字符串,直接使用
const propKey = comp.addComponentProperty('Label', 'TEXT', 'Button')
// propKey === "label#4:0"(具体值随文件变化;永远不要假设它)
labelNode.componentPropertyReferences = { characters: propKey }

在类型声明 plugin-api-standalone.d.ts 中,addComponentProperty 的签名正是 (propertyName, type, defaultValue, options?): string,可返回带唯一后缀的属性名;COMPONENT_SET 节点同理,返回值同样是字符串 key。

三、所有创建/变更的节点 ID 必须随返回值返回

凡是创建或改动画布节点的脚本,都必须在返回值中记录并返回所有受影响的节点 ID。否则后续调用无法引用、校验或清理这些节点——这正是技能规则第 15 条“MUST return ALL created/mutated node IDs”的来源。

// WRONG —— 只返回父 Frame 的 ID,丢失子节点
const frame = figma.createFrame()
const rect = figma.createRectangle()
const text = figma.createText()
frame.appendChild(rect)
frame.appendChild(text)
return { nodeId: frame.id }

// CORRECT —— 以结构化响应返回全部新建节点 ID
const frame = figma.createFrame()
const rect = figma.createRectangle()
const text = figma.createText()
frame.appendChild(rect)
frame.appendChild(text)
return {
  createdNodeIds: [frame.id, rect.id, text.id],
  rootNodeId: frame.id
}

// CORRECT —— 变更既有节点时同样返回这些 ID
const nodes = figma.currentPage.findAll(n => n.name === 'Card')
for (const n of nodes) {
  n.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]
}
return {
  mutatedNodeIds: nodes.map(n => n.id),
  count: nodes.length
}

四、Paint 与颜色相关陷阱

1. 颜色取值域是 0–1,不是 0–255

Figma 的 RGB 通道一律使用 0–1 浮点范围。写成 255 会被 ZeroToOne 校验拦截并抛错。

// WRONG —— 抛出 validation error
node.fills = [{ type: 'SOLID', color: { r: 255, g: 0, b: 0 } }]

// CORRECT
node.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]

RGBRGBA 类型声明均注明 0–1 range, not 0–255(见 plugin-api-standalone.d.tsRGB/RGBA 类型注释)。若数据源是 0–255,记得先除以 255。

2. fills/strokes 是不可变数组

对数组元素就地修改不会生效,Figma 不感知原地变更。必须克隆 → 修改 → 重新赋值。

// WRONG —— 原地修改无效
node.fills[0].color = { r: 1, g: 0, b: 0 }

// CORRECT —— 克隆、修改、重新赋值
const fills = JSON.parse(JSON.stringify(node.fills))
fills[0].color = { r: 1, g: 0, b: 0 }
node.fills = fills

类型索引文档 plugin-api-standalone.index.md 中同样以 CRITICAL 标注了“fills/strokes 是只读数组,需要克隆-修改-重赋”这一约束。

3. paint 的 color 里不能带 a —— 透明度属于 paint 层级的 opacity

paint 的 color 只接受 {r, g, b}。附加 a 会抛出 "Unrecognized key(s) in object: 'a' at [0].color"——这是从 CSS rgba() 写法的肌肉记忆带来的高频错误。Alpha/透明度应放在 paint 层级opacity 字段上。

// WRONG —— color 内部不能有 'a',会抛 validation error
node.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 0.1 } }]

// CORRECT —— opacity 放在 paint 层级
node.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 }, opacity: 0.1 }]

// CORRECT —— 完全不透明(无需 opacity)
node.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]

COLOR 变量值是唯一的例外——变量值确实使用 {r, g, b, a} 四通道:

// 变量值使用 {r, g, b, a} —— 仅变量场景下是正确的
const colorVar = figma.variables.createVariable("bg", collection, "COLOR")
colorVar.setValueForMode(modeId, { r: 1, g: 0, b: 0, a: 1 })  // 不透明红色
colorVar.setValueForMode(modeId, { r: 0, g: 0, b: 0, a: 0 })  // 完全透明

4. setBoundVariableForPaint 返回的是新 paint,必须接住返回值

该函数不是就地修改,而是返回一份已绑定变量的新 paint。忽略返回值继续使用旧 paint,绑定会静默丢失。

// WRONG —— 忽略返回值
figma.variables.setBoundVariableForPaint(paint, "color", colorVar)
node.fills = [paint]  // paint 根本没变!

// CORRECT —— 捕获返回的新 paint
const boundPaint = figma.variables.setBoundVariableForPaint(paint, "color", colorVar)
node.fills = [boundPaint]

类型声明 plugin-api-standalone.d.ts 明确注释“@returns a copy of the paint which is now bound to the provided variable”,传入 null 变量则可解除绑定。setBoundVariableForEffectsetBoundVariableForLayoutGrid 遵循同样模式。

5. paint 字段的变量绑定只对 SOLID paint 生效

setBoundVariableForPaint 只能把颜色变量绑定到 SOLID 类型的 paint 上;渐变、图片等 paint 会抛错。

// 仅 SOLID paint 支持颜色变量绑定
// 渐变 / 图片等 paint 会抛错
const solidPaint = { type: 'SOLID', color: { r: 0, g: 0, b: 0 } }
const bound = figma.variables.setBoundVariableForPaint(solidPaint, "color", colorVar)

6. 空 fills 的节点无法绑定颜色变量

对没有 fills 的节点(fills = [],透明)执行颜色绑定没有任何意义,也不会有任何效果。

// WRONG —— 绑定到没有 fills 的节点,什么都不会发生
const comp = figma.createComponent()
comp.fills = [] // transparent
// 无法把颜色变量绑定到不存在的 fills 上

// CORRECT —— 先放一个占位 SOLID paint,再绑定变量
const comp = figma.createComponent()
const basePaint = { type: 'SOLID', color: { r: 0, g: 0, b: 0 } }
const boundPaint = figma.variables.setBoundVariableForPaint(basePaint, "color", colorVar)
comp.fills = [boundPaint]
// 变量解析出的实际值(可能本身是透明的)将决定最终颜色

五、Variable 集合与作用域陷阱

1. 新建的 variable collection 自带 1 个 mode,先重命名而非新增

createVariableCollection 返回的集合已经内置一个名为 "Mode 1" 的 mode。正确做法是先把默认 mode 改名,再按需 addMode

// 新集合已自带一个 mode
const collection = figma.variables.createVariableCollection("Colors")
// collection.modes = [{ modeId: "...", name: "Mode 1" }]
collection.renameMode(collection.modes[0].modeId, "Light")
const darkModeId = collection.addMode("Dark")

2. mode 命名要有语义 —— 永远不要保留 'Mode 1'

每个新建 VariableCollection 都从名为 'Mode 1' 的单个 mode 开始,请立即重命名:单 mode 集合用 'Default';多 mode 集合使用来自设计源文件的命名(如 'Light'/'Dark''Desktop'/'Tablet'/'Mobile')。

// WRONG —— 通用名没有任何语义
const coll = figma.variables.createVariableCollection('Colors')
// coll.modes[0].name === 'Mode 1' —— 保持原样
const darkId = coll.addMode('Mode 2')

// CORRECT —— 立即按源命名重命名
const coll = figma.variables.createVariableCollection('Colors')
coll.renameMode(coll.modes[0].modeId, 'Light')   // 原为 'Mode 1'
const darkId = coll.addMode('Dark')

// 单 mode 集合(primitives、spacing 等)
const spacing = figma.variables.createVariableCollection('Spacing')
spacing.renameMode(spacing.modes[0].modeId, 'Default')  // 原为 'Mode 1'

3. Variable 默认 ALL_SCOPES —— 必须显式限定 scopes

新建变量若不设置 scopes,默认值是 ["ALL_SCOPES"],会让该变量出现在所有属性选择器(fills、text、strokes、spacing 等)中,污染整个选择面板——几乎从来不是你想要的结果。

// WRONG —— 变量出现在每一个属性 picker 中
const bgColor = figma.variables.createVariable("Background/Default", coll, "COLOR")
// bgColor.scopes 默认是 ["ALL_SCOPES"] —— 污染所有下拉框

// CORRECT —— 限制到相关的属性 picker
const bgColor = figma.variables.createVariable("Background/Default", coll, "COLOR")
bgColor.scopes = ["FRAME_FILL", "SHAPE_FILL"]  // 仅填充 picker

const textColor = figma.variables.createVariable("Text/Default", coll, "COLOR")
textColor.scopes = ["TEXT_FILL"]  // 仅文本颜色 picker

const borderColor = figma.variables.createVariable("Border/Default", coll, "COLOR")
borderColor.scopes = ["STROKE_COLOR"]  // 仅描边 picker

const spacing = figma.variables.createVariable("Space/400", coll, "FLOAT")
spacing.scopes = ["GAP"]  // 仅间距/gap picker

// 隐藏仅被别名引用的底层原始 token
const primitive = figma.variables.createVariable("Brand/500", coll, "COLOR")
primitive.scopes = []  // 从所有 picker 隐藏

4. 显式 variable mode 必须按组件逐个设置

多 mode 集合中,若不为组件显式指定 mode,所有变体都会渲染默认(第一个)mode 的值,导致“换肤”不生效。

// WRONG —— 所有变体渲染默认(第一个)mode
const colorCollection = figma.variables.createVariableCollection("Colors")
// ... 创建 variables 和 modes ...
// 默认情况下所有组件都显示第一个 mode 的值!

// CORRECT —— 在每个组件上显式设置 mode,以获得变体专属的值
component.setExplicitVariableModeForCollection(colorCollection, targetModeId)

类型声明支持以集合对象或 ID 字符串两种形式调用 setExplicitVariableModeForCollection,建议直接传入集合对象(见 plugin-api-standalone.d.ts)。

5. 每个 collection 的 mode 数量受套餐限制

Figma 按团队/组织套餐限制每个集合可创建的 mode 数,越界会静默失败或抛错:

  • Free:仅 1 个 mode(无法 addMode
  • Professional:最多 4 个 mode
  • Organization / Enterprise:可达 40+ 个 mode
// WRONG —— 在 Professional 套餐上创建 20 个 mode 会失败
const coll = figma.variables.createVariableCollection("Variants")
for (let i = 0; i < 20; i++) coll.addMode("mode" + i) // May fail!

// CORRECT —— 需要大量 mode 时拆分到多个 collection
// 例如不要用 1 个 collection 装 20 个 mode(variant × color):
//   Collection A:4 个 mode(variant:plain/outlined/soft/solid)
//   Collection B:5 个 mode(color:neutral/primary/danger/success/warning)
// 然后在每个组件上对两个 collection 都调用 setExplicitVariableModeForCollection

6. 由 Figma 变量名推导 CSS 变量名时,空格必须一并处理

从 Figma 变量名构造 var(--name) 时,需要把斜杠和空格都替换为连字符,并转小写。只替换斜杠会留下空格,产出 'var(--color-bg-brand secondary hover)' 这类非法 CSS。

// WRONG —— 只替换斜杠,空格残留
v.setVariableCodeSyntax('WEB', `var(--${figmaName.replace(/\//g, '-').toLowerCase()})`)

// CORRECT —— 一次同时替换所有空白与斜杠
v.setVariableCodeSyntax('WEB', `var(--${figmaName.replace(/[\s\/]+/g, '-').toLowerCase()})`)

最佳实践:优先保留来源 token 文件中已有的 CSS 变量名,而不是从 Figma 名现推:

// 推荐 —— 直接使用来源 CSS 名
v.setVariableCodeSyntax('WEB', `var(${token.cssVar})`)  // 例如 '--color-bg-brand-secondary-hover'

六、页面上下文与脚本生命周期陷阱

1. 同步页面 setter 不可用 —— 必须 await figma.setCurrentPageAsync(page)

use_figma 中,同步 setter figma.currentPage = page 不生效,会抛 "Setting figma.currentPage is not supported"必须改用 await figma.setCurrentPageAsync(page),它负责切换页面并加载页面内容。

注意:读取 figma.currentPage 是合法的,只有赋值会抛错。

// WRONG —— 抛 "Setting figma.currentPage is not supported"
figma.currentPage = targetPage

// CORRECT —— async 方法切换并加载内容
await figma.setCurrentPageAsync(targetPage)

// ALSO CORRECT —— 读取 currentPage 没有问题
const page = figma.currentPage  // works

更进一步,每次 use_figma 调用之间页面上下文会重置figma.currentPage 每次都从第一页开始。如果多步工作流面向非默认页,必须在每次调用开头重新 setCurrentPageAsync。技能规则第 8 条还提醒:不要漏掉 await,未 await 的异步调用会 fire-and-forget,造成静默失败或竞态。

2. get_metadata 只作用于单个子树 —— 页面需要显式枚举

一个 Figma 文件可能包含多个页面(canvas 节点)。get_metadata 只返回你传入节点的子树。要拿到所有页面的可用索引:

  • 不传 nodeId 调用 get_metadata:它返回文档顶层页面作为 {guid, name} 条目(不含 XML dump),这是发现页面的最廉价方式。
  • 需要每页更细的信息(如子节点数量、顶层节点类型)时,再回退到 use_figma
const pages = figma.root.children.map(p => `${p.name} id=${p.id} children=${p.children.length}`);
return pages.join('\n');

图标、变量、组件可能存放在第一页之外的其他页面。在得出“文件里没有可用资产”的结论之前,务必先枚举全部页面。

3. 脚本必须总是返回一个值

返回值是 Agent 与脚本之间唯一的通信通道。没有 return 时,调用方拿不到任何有效响应。

// WRONG —— 没有 return,调用方得不到有效反馈
figma.createRectangle()

// CORRECT —— 返回结果(对象自动序列化,错误自动捕获)
const rect = figma.createRectangle()
return { nodeId: rect.id }

4. 永远不要使用 figma.notify()

figma.notify()use_figma 中会抛 "not implemented" 错误。反馈信息请通过返回值传回给 Agent。

// WRONG —— 抛 "not implemented" error
figma.notify("Done!")

// CORRECT —— 返回值把数据回传给 Agent
return "Done!"

同理,console.log() 也不会被返回,输出一律走 return

5. getPluginData() / setPluginData() 不受支持

这两个 API 在 use_figma 中不可用。请改用 受支持的 getSharedPluginData() / setSharedPluginData()(需要命名空间),或通过返回节点 ID 在多次调用间跟踪节点。

// WRONG —— use_figma 不支持
node.setPluginData('my_key', 'my_value')
const val = node.getPluginData('my_key')

// CORRECT —— 使用 shared plugin data(需要命名空间)
node.setSharedPluginData('my_namespace', 'my_key', 'my_value')
const val = node.getSharedPluginData('my_namespace', 'my_key')

// ALSO CORRECT —— 返回节点 ID,跨调用跟踪
const rect = figma.createRectangle()
return { nodeId: rect.id }
// 然后在下一个 use_figma 调用中把 nodeId 作为字符串字面量传入

类型索引 plugin-api-standalone.index.mdPluginDataMixin 行也标注了同样的限制:getSharedPluginData/setSharedPluginData 受支持,getPluginData/setPluginData NOT supported

七、Auto Layout 与尺寸顺序陷阱(含 HUG/FILL 交互)

这是视觉 bug 最集中的一类。核心口诀:先入父容器,再设 FILL;先 resize,再设 sizing mode;不要用垃圾值喂 resize()。

1. 'FILL' 必须先有 auto-layout 父节点

在节点成为 auto-layout frame 的子节点之前设置 layoutSizingHorizontal/Vertical = 'FILL',会抛 "FILL can only be set on children of auto-layout frames"

// WRONG —— 节点尚未成为 auto-layout frame 的子节点就设 FILL
const child = figma.createFrame()
child.layoutSizingVertical = 'FILL'  // ERROR: "FILL can only be set on children of auto-layout frames"
parent.appendChild(child)

// CORRECT —— 先 append 到 auto-layout 父节点,再设 FILL
const child = figma.createFrame()
parent.appendChild(child)            // parent 需已设置 layoutMode
child.layoutSizingVertical = 'FILL'  // Works!

Tip:需要支持 FILL 子节点的父容器时,优先使用 figma.createAutoLayout()(或 figma.createAutoLayout('VERTICAL'))替代 figma.createFrame()。它返回的 frame 已设置 layoutMode 且两轴为 hug,无需手写属性顺序。

const parent = figma.createAutoLayout()  // layoutMode = 'HORIZONTAL', sizing = AUTO
const child = figma.createFrame()
parent.appendChild(child)
child.layoutSizingHorizontal = 'FILL'    // 立即生效

2. HUG 父容器会压扁 FILL 子节点

HUG 父容器无法给 FILL 子节点有意义的大小。若子节点 layoutSizingHorizontal = "FILL" 而父容器是 "HUG",子节点会塌缩到最小尺寸。要让 FILL 子节点真正撑开,父容器必须是 "FILL""FIXED"。这是 select 字段、输入框、操作行里文本被截断的常见根因。

// WRONG —— 父容器 hug,FILL 子节点分不到任何额外空间
const parent = figma.createAutoLayout()
parent.layoutSizingHorizontal = 'HUG'
const child = figma.createFrame()
parent.appendChild(child)
child.layoutSizingHorizontal = 'FILL'  // 塌缩到最小尺寸!

// CORRECT —— 父容器必须是 FIXED 或 FILL,FILL 子节点才能展开
const parent = figma.createAutoLayout()
parent.resize(400, 50)
parent.layoutSizingHorizontal = 'FIXED'  // 或处于另一 auto-layout 中时为 'FILL'
const child = figma.createFrame()
parent.appendChild(child)
child.layoutSizingHorizontal = 'FILL'  // 撑满剩余 400px

3. hug 父容器搭配 layoutGrow 会导致内容被压缩

当父容器 primaryAxisSizingMode='AUTO'(hug)时,给子节点设 layoutGrow = 1 会让子节点缩小到自然尺寸以下而不是展开,内容被隐藏。

// WRONG —— 父容器 hug 时给子节点 layoutGrow,内容被压缩、子节点被隐藏
const parent = figma.createComponent()
parent.layoutMode = 'VERTICAL'
parent.primaryAxisSizingMode = 'AUTO'  // hug contents
const content = figma.createAutoLayout('VERTICAL')
parent.appendChild(content)
content.layoutGrow = 1  // BUG: content 被压缩,子节点不可见!

// CORRECT —— 仅在父容器为 FIXED 且有富余空间时使用 layoutGrow
content.layoutGrow = 0  // 让 content 保持自然尺寸
// 或者:先把父容器改为 FIXED
parent.primaryAxisSizingMode = 'FIXED'
parent.resizeWithoutConstraints(300, 500)
content.layoutGrow = 1  // NOW 正确撑满剩余空间

4. width / height 是只读的 —— 用 resize()

node.widthnode.height 只读,直接赋值会抛 "TypeError: no setter for property"。请改用 resize()resizeWithoutConstraints()注意 x / y 不是只读的,可以直接赋值。

// WRONG —— 抛 "no setter for property"
node.width = 300
node.height = 64

// CORRECT —— 用 resize() 改变尺寸
node.resize(300, 64)           // 同时改宽高
node.resize(300, node.height)  // 只改宽度
node.resize(node.width, 64)    // 只改高度

// CORRECT —— x / y 可直接写
node.x = 100
node.y = 200

Sections 与 component sets 请使用 resizeWithoutConstraints() 而不是 resize()(见下文 Sections 小节)。

5. resize() 会把两轴 sizing mode 静默重置为 FIXED

resize(w, h)同时primaryAxisSizingModecounterAxisSizingMode 重置为 FIXED。如果你先设置了 HUG 再调用 resize(),frame 会被锁定到你传入的精确像素值——即使那只是个随手写的 1

// WRONG —— 设置 sizing mode 之后调用 resize(),会把模式覆盖回 FIXED
const frame = figma.createComponent()
frame.layoutMode = 'VERTICAL'
frame.primaryAxisSizingMode = 'AUTO'  // hug height
frame.counterAxisSizingMode = 'FIXED'
frame.resize(300, 10)  // BUG: 两轴都被重置为 'FIXED'!高度永远卡在 10px

// ESPECIALLY DANGEROUS —— 只想管一个轴,却传了垃圾值
const comp = figma.createComponent()
comp.layoutMode = 'VERTICAL'
comp.layoutSizingHorizontal = 'FIXED'
comp.layoutSizingVertical = 'HUG'
comp.resize(280, 1)  // BUG: “我只想宽度=280”,结果高度被锁成 1px!
// HUG 被 resize() 重置为 FIXED,frame 永久变成 280×1

// CORRECT —— 先调 resize(),再设置 sizing mode
const frame = figma.createComponent()
frame.layoutMode = 'VERTICAL'
frame.resize(300, 40)  // 使用合理默认值,永远不要用 0 或 1
frame.counterAxisSizingMode = 'FIXED'  // 宽度保持 300
frame.primaryAxisSizingMode = 'AUTO'   // NOW 高度设为 hug —— 这次能保持住!
// 或者用现代简写(等价):
// frame.layoutSizingHorizontal = 'FIXED'
// frame.layoutSizingVertical = 'HUG'

经验法则:对于你计划保持 HUG 的轴,永远不要把随手/垃圾值(如 10)传给 resize()。要么先 resize() 再设 sizing mode,要么用不会造成视觉 bug 的合理默认值(即使 mode 重置未被察觉)。

6. reparenting 后节点坐标不会自动重置

把一个节点移进新的父节点时,其相对坐标保持不变。若不做显式重置,节点会停留在旧的 (x, y) 位置,出现“错位漂移”。

// WRONG —— 以为移入新父节点后坐标会自动重置
const node = figma.createRectangle()
node.x = 500; node.y = 500;
figma.currentPage.appendChild(node)
section.appendChild(node)  // 节点相对 section 仍停留在 (500, 500)!

// CORRECT —— 任何 reparenting 操作之后显式设置 x/y
section.appendChild(node)
node.x = 80; node.y = 80;  // 重置到 section 内期望的位置

7. 混合宽度行做网格布局会导致重叠

用固定的“列间距”偏移去排版不同宽度的行,宽行必然溢出碰撞。例如竖版卡片(320px)与横版卡片(500px)混排时,按 370 步进只对 320px 卡片成立。

// WRONG —— 对不同宽度条目使用单一列偏移
// 例如 2 行网格中混排竖版卡片 (320px) 和横版卡片 (500px)
for (let i = 0; i < allCards.length; i++) {
  allCards[i].x = (i % 4) * 370  // 对 320px 卡片成立,对 500px 卡片不行!
}

// CORRECT —— 基于实际子节点宽度逐行独立计算间距
const gap = 50
let x = 0
for (const card of horizontalCards) {
  card.x = x
  x += card.width + gap  // 使用实际宽度,而非固定列宽
}

8. Sections 不会自动缩放以适应内容

createSection() 出来的节点保持默认尺寸,内容可能溢出边界。必须添加内容后显式 resize。

// WRONG —— section 保持默认大小,内容溢出
const section = figma.createSection()
section.name = "My Section"
section.appendChild(someNode) // node 可能落在 section 边界之外

// CORRECT —— 添加内容后显式 resize
const section = figma.createSection()
section.name = "My Section"
section.appendChild(someNode)
section.resize(
  Math.max(someNode.width + 100, 800),
  Math.max(someNode.height + 100, 600)
)

Sections 属于带约束的容器类型,请用 resizeWithoutConstraints() 而非 resize() 来规避约束干扰。

9. counterAxisAlignItems 不支持 'STRETCH'

counterAxisAlignItems 的合法枚举只有 'MIN' | 'MAX' | 'CENTER' | 'BASELINE',传入 'STRETCH' 会抛非法枚举值错误。想要“拉伸”效果的正确姿势是:父容器对齐方式设为 'MIN',再让子节点在交叉轴上 FILL

// WRONG —— 'STRETCH' 不是合法枚举值
comp.counterAxisAlignItems = 'STRETCH'
// Error: Invalid enum value. Expected 'MIN' | 'MAX' | 'CENTER' | 'BASELINE', received 'STRETCH'

// CORRECT —— 父容器用 'MIN',然后让子节点在交叉轴 FILL
comp.counterAxisAlignItems = 'MIN'
comp.appendChild(child)
// 竖排布局拉伸宽度:
child.layoutSizingHorizontal = 'FILL'
// 横排布局拉伸高度:
child.layoutSizingVertical = 'FILL'

类型声明 plugin-api-standalone.d.tscounterAxisAlignItems 的类型正是 'MIN' | 'MAX' | 'CENTER' | 'BASELINE',从类型层面就没有 'STRETCH'

八、组件与变体(Component & Variant)陷阱

1. combineAsVariants 只接受 ComponentNode

传入 Frame 会直接报错。先 createComponent(),组件名即变体属性("variant=primary, size=md" 这种命名),再合并。

// WRONG —— 传 frames
const f1 = figma.createFrame()
figma.combineAsVariants([f1], figma.currentPage) // Error!

// CORRECT —— 传 components
const c1 = figma.createComponent()
c1.name = "variant=primary, size=md"
const c2 = figma.createComponent()
c2.name = "variant=secondary, size=md"
figma.combineAsVariants([c1, c2], figma.currentPage)

2. combineAsVariants 在 use_figma 中不会自动布局

合并后所有变体都叠在 (0, 0),生成的 ComponentSet 会小得异常(宽高等于单个变体的尺寸)。必须手动把子变体排成网格,并从实际子节点边界(而非公式)反推 ComponentSet 尺寸——公式误差会让变体留在边界之外。

// WRONG —— 所有变体堆叠在 (0, 0),得到一个极小的 ComponentSet
const components = [comp1, comp2, comp3]
const cs = figma.combineAsVariants(components, figma.currentPage)
// cs.width/height 会是单个变体的大小!

// CORRECT —— 合并后手动把子节点排成网格
const cs = figma.combineAsVariants(components, figma.currentPage)
const colWidth = 120
const rowHeight = 56
cs.children.forEach((child, i) => {
  const col = i % numCols
  const row = Math.floor(i / numCols)
  child.x = col * colWidth
  child.y = row * rowHeight
})
// CRITICAL: 从实际子节点边界反推尺寸,而不是用公式
// 公式误差会让变体留在边界之外
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)

九、字体与排版陷阱

1. lineHeight / letterSpacing 必须是对象,不能是裸数字

两个属性的值都是对象结构。裸数字赋值会抛错或静默无效,同时适用于 TextStyleTextNode 上的属性,且贯穿 use_figma、交互式插件以及任何其他插件 API 上下文。

// WRONG —— 抛错或静默无效
style.lineHeight = 1.5
style.lineHeight = 24
style.letterSpacing = 0

// CORRECT
style.lineHeight = { unit: "AUTO" }                    // auto/intrinsic
style.lineHeight = { value: 24, unit: "PIXELS" }       // 固定像素行高
style.lineHeight = { value: 150, unit: "PERCENT" }     // 字号百分比

style.letterSpacing = { value: 0, unit: "PIXELS" }     // 无字距
style.letterSpacing = { value: -0.5, unit: "PIXELS" }  // 紧凑
style.letterSpacing = { value: 5, unit: "PERCENT" }    // 基于百分比

类型层面同样如此:plugin-api-standalone.d.tsLetterSpacing{ value: number, unit: 'PIXELS' | 'PERCENT' }LineHeight(L4856 起)为 { value, unit }{ unit: 'AUTO' } 的联合类型。

2. 字体样式名随文件而异 —— 用 listAvailableFontsAsync 发现

字体样式名因字体供应商、因每个 Figma 文件而异。永远先调用 figma.listAvailableFontsAsync() 发现精确的样式字符串,再加载——不要靠记忆猜测,也不要用 try/catch 去探测。例如 "SemiBold""Semi Bold" 的差异就是经典坑位。

完整“发现 + 加载”模式参见技能参考文档 text-style-patterns.md,其示例函数通过 listAvailableFontsAsync 过滤出指定 family 的全部 style:

async function getAvailableFontStyles(family) {
  const allFonts = await figma.listAvailableFontsAsync();
  return allFonts
    .filter(f => f.fontName.family === family)
    .map(f => f.fontName.style);
}

同时牢记:任何包含未加载字体节点的操作都需要先加载字体——不仅是设置文本的操作。appendChildinsertChildsetBoundVariablesetExplicitVariableModeForCollectionsetValueForMode,甚至 findAll 的回调都可能命中这条限制(技能规则第 8 条)。

十、类型安全与属性写入陷阱

1. 调用类型专属方法前必须做类型守卫

某些方法只存在于特定节点类型上,调用在错误类型上会抛 "TypeError: not a function"。调用前务必先做类型检查。

// WRONG —— node 可能不是 TextNode
const node = await figma.getNodeByIdAsync('952:1253');
const segments = node.getStyledTextSegments(['hyperlink']); // node 不是 TEXT 时抛 TypeError

// CORRECT —— 先检查类型
const node = await figma.getNodeByIdAsync('952:1253');
if (!node || node.type !== 'TEXT') return { error: `Expected TextNode, got ${node?.type ?? 'null'}` };
const segments = node.getStyledTextSegments(['hyperlink']);

常见的类型专属方法与对应节点类型对照:

方法 需要的节点类型
getStyledTextSegments() TEXT
setRangeFontName()setRangeFontSize() TEXT
createInstance() COMPONENT
addComponentProperty() COMPONENTCOMPONENT_SET
createVariant() COMPONENT_SET

2. 写入不存在的属性会抛 "object is not extensible"

Figma 插件 API 的节点对象不可扩展——不能添加新属性。对节点类型上不存在的属性名做写入会抛 "Cannot add property X, object is not extensible"(表现常为 "object is not extensible")。只有写操作会触发,且只针对该节点类型未定义的属性。

// WRONG —— VectorNode 上没有 'strokeDashes';抛 "object is not extensible"
const v = figma.createVector()
v.strokeDashes = [4, 8]  // Error!

// CORRECT —— 真正的属性是 dashPattern
v.dashPattern = [4, 8]

// WRONG —— 任何臆造属性名都会抛同样的错误
node.customColor = '#ff0000'  // Error —— 不是真实 API 属性

规避方法:设置任何属性前,先在 plugin-api-standalone.d.ts 中确认该属性存在于对应节点类型上——听起来合理但不在类型定义里的属性名一定会抛错。(注:类型文件中 strokeDashes 确实不存在于 Vector 图形节点,dashPattern 才是其描边虚线属性。)

3. detachInstance() 会使祖先节点 ID 失效

对嵌套在库组件实例内部的子实例调用 detachInstance() 时,父实例可能被隐式一并 detach(从 INSTANCE 变成 FRAME,并获得新 ID)。任何之前缓存的父 ID 都会随之失效,getNodeByIdAsync 将返回 null。

// WRONG —— 子节点 detach 后仍使用缓存的父 ID
const parentId = parentInstance.id;
nestedChild.detachInstance();
const parent = await figma.getNodeByIdAsync(parentId); // null! ID 已改变

// CORRECT —— 从稳定的(非 instance)frame 出发重新遍历发现
const stableFrame = await figma.getNodeByIdAsync(manualFrameId);
nestedChild.detachInstance();
const parent = stableFrame.findOne(n => n.name === "ParentName");

若要对多个同层嵌套实例执行 detach,请在单个 use_figma 调用内完成——在任一次 detach 变更节点树之前,先通过遍历把所有目标节点都发现好。

十一、使用前的自检清单与错误恢复

上述陷阱可以浓缩为一张每次提交 use_figma 调用前的速查清单(技能 SKILL.md 第 8 节的节选):

  • [ ] 代码用 return 回传数据(不要 figma.closePlugin(),也不要包 async IIFE,脚本已被自动包裹)
  • [ ] 颜色一律 0–1(不是 0–255);paint color 只含 {r, g, b}opacity 放 paint 层
  • [ ] fills/strokes 以新数组重新赋值,绝不在原处修改
  • [ ] 切页用 await figma.setCurrentPageAsync(page)(同步 setter 不可用)
  • [ ] layoutSizingX = 'FILL'parent.appendChild(child) 之后设置
  • [ ] resize() 在任何 sizing mode 设置之前调用(resize 会把模式重置为 FIXED)
  • [ ] loadFontAsync() 在一切文本相关操作前调用;样式名经 listAvailableFontsAsync 核实,不靠记忆
  • [ ] lineHeight/letterSpacing 使用 {unit, value} 对象(非裸数字)
  • [ ] 新建顶层节点避开 (0,0)
  • [ ] 返回值包含全部新建/变更节点 ID
  • [ ] 每个 Promise 都 await,杜绝 fire-and-forget

当脚本报错时,请遵循“原子性”认知:use_figma 失败脚本是原子的——出错时脚本完全不会执行,文件保持调用前状态,不会留下半成品节点。因此正确的做法是先 STOP,仔细阅读错误信息,必要时用 get_metadata/get_screenshot 观察当前文件状态,修复脚本后再重试。常见错误与对策映射(技能自愈模式节选):

错误信息 可能原因 修复方式
"not implemented" 使用了 figma.notify() 删除,改用 return 输出
"node must be an auto-layout frame..." 在 append 前设置了 FILL/HUG appendChild 移到 layoutSizingX = 'FILL' 之前
"Setting figma.currentPage is not supported" 使用了同步页面 setter 改用 await figma.setCurrentPageAsync(page)
属性值越界 颜色通道 > 1(0–255 误写成 0–1) 除以 255
"Cannot read properties of null" 节点不存在(ID 错误、页面错误) 检查页面上下文与 ID
脚本卡死 / 无响应 死循环或未 resolve 的 Promise 检查 while(true)、补齐 await
"The node with id X does not exist" 父实例被子节点 detachInstance() 隐式 detach,ID 已变 从稳定的(非 instance)父 frame 重新遍历发现

详细的逐文件说明、get_metadata/get_screenshot 校验工作流与恢复步骤,见技能参考文档 validation-and-recovery.md;组件/变体方向的更多示例见 component-patterns.md,变量方向见 variable-patterns.md,文本样式方向见 text-style-patterns.md。在仓库中按需查阅这些文档,配合 plugin-api-standalone.d.ts 的类型定义,即可把 Figma 脚本调试效率提升一个量级。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391