跳转至

Figma 插件开发笔记:组件查找、实例化与问题排查

功能概述

插件每次点击复制一个名为 box 的组件到画布下方,并将组件内名为 txt 的文本节点填充随机中文文字(20 字以内)。

组件的名称区分大小写,这个得注意。

组件绑定

查找组件

// 方法1:从已有实例反查组件(推荐,避免跨页加载)
const existingInstance = figma.currentPage.findOne(
  (n) => n.type === 'INSTANCE' && n.name === 'box'
);
if (existingInstance && existingInstance.type === 'INSTANCE') {
  component = await existingInstance.getMainComponentAsync();
}

// 方法2:遍历所有页面直接查找组件
await figma.loadAllPagesAsync();
for (const page of figma.root.children) {
  const found = page.findOne(
    (n) => n.type === 'COMPONENT' && n.name === 'box'
  );
  if (found && found.type === 'COMPONENT') {
    component = found;
    break;
  }
}

创建实例并操作子节点

// 创建组件实例
const instance = component.createInstance();
figma.currentPage.appendChild(instance);

// 查找实例内的子文本节点
const txtNode = instance.findOne(
  (n) => n.name === 'txt' && n.type === 'TEXT'
) as TextNode | null;

// 修改文本前必须先加载字体
if (txtNode) {
  await figma.loadFontAsync(txtNode.fontName as FontName);
  txtNode.characters = '新的文字内容';
}

定位到已有实例下方

const allInstances = figma.currentPage.findAll(
  (n) => n.type === 'INSTANCE' && n.name === 'box'
);
const others = allInstances.filter(n => n.id !== instance.id);

if (others.length > 0) {
  let bottom = others[0];
  for (const inst of others) {
    if (inst.y + inst.height > bottom.y + bottom.height) {
      bottom = inst;
    }
  }
  instance.x = bottom.x;
  instance.y = bottom.y + bottom.height + 20; // 20px 间距
} else {
  // 首个实例放在视口中心
  instance.x = figma.viewport.center.x - instance.width / 2;
  instance.y = figma.viewport.center.y - instance.height / 2;
}

踩坑记录

问题 1:无 UI 插件卡在 "Running" 状态

现象:插件没有调用 figma.showUI(),但 manifest.json"ui" 字段已删除,插件一直显示 "Running..."。

原因:无 UI 插件(headless)在某些 Figma 版本中初始化不稳定,即使代码逻辑正确也可能卡住。

解决:改为有 UI 的架构 — 打开一个小弹窗,点击按钮触发操作。插件保持打开状态支持连续点击。

问题 2:findOne() 报错 "page has not been explicitly loaded"

现象

Error: in findOne: Cannot access method 'findOne()' on a page
that has not been explicitly loaded.

原因documentAccess: "dynamic-page" 模式下,非当前页面默认未加载,不能直接调用 findOne()

解决:遍历页面之前先调用 await figma.loadAllPagesAsync()

问题 3:mainComponent 属性被 dynamic-page 拦截

现象

Error: in get_mainComponent: Cannot call with documentAccess: dynamic-page.
Use node.getMainComponentAsync instead.

原因documentAccess: "dynamic-page" 禁止直接访问 InstanceNode.mainComponent 属性,必须使用异步方法。

解决:两处修改: 1. existingInstance.mainComponentawait existingInstance.getMainComponentAsync() 2. findAll 回调中无法使用异步方法 → 改为按实例名称过滤(name === 'box'),因为所有实例都继承组件名称

问题 4:点击按钮无任何反馈

原因mainComponent 访问抛错但被安静吞掉,没有给到用户通知。

解决:在 UI 消息处理器外包裹 try-catch,捕获异常后通过 figma.notify() 给用户反馈。

figma.ui.onmessage = async (msg) => {
  if (msg.type === 'copy') {
    try {
      await doCopy();
    } catch (e: any) {
      figma.notify('出错: ' + (e?.message || String(e)), { error: true });
    }
  }
};

架构

manifest.json          →  声明 main: code.js, ui: ui.html
code.ts → code.js      →  主线程,findOne + createInstance + 字体加载 + 定位
ui.html                →  iframe 弹窗,按钮 + 键盘快捷键
文件 职责
manifest.json 插件注册,documentAccess: "dynamic-page"
code.ts 组件查找、实例化、文字填充、定位,消息监听
ui.html 用户界面:复制按钮 + Ctrl+Enter 快捷键

前置条件

在 Figma 画布上需要准备:

  • 一个组件,命名为 box
  • 组件内包含一个文本图层,命名为 txt

创建方式:选中一个 Frame/Group → 右键 → Create component → 在左侧面板双击名称改为 box,内部文本图层改名为 txt