设计稿静态缺交互,小部件接口咋整出会动的组件?

4,946字
21–31 分钟
in

Figma自带的设计功能像一堆不会动的积木块,拼来拼去都是死板的矩形。直到官方甩出Widgets API这个王炸,才让设计稿真正有了“活气”。啥是小部件?就是能用JavaScript写逻辑、带状态、能联网的交互组件,直接嵌在画板里跑起来。比如点一下刷新名言、做个投票器、甚至连个Jira任务看板。这篇就手把手还原一个设计名言随机生成器,从画框框到写代码再到全网发布,把整个踩坑经验抖落干净。

目录

啥是小部件

小部件本质上是一段跑在Figma沙箱里的React风格组件。和普通插件不同,它直接躺在画板里,能跟设计图层实时联动。官方提供了<AutoLayout /><Text /><SVG />这些组件,属性名跟Figma右侧面板几乎一致——比如direction控制横竖排,spacing调间距,fill给背景色。写起来就像在写JSX,但背后同步机制更狠:状态一变,所有协作者的屏幕上同时刷新。

整活前准备

开发小部件得用Windows或Mac版Figma桌面应用,Linux暂时没戏(虚拟机除外)。先下载Figma Desktop,打开后新建一个画板,按Shift+I调出小部件菜单,切到“Development”页,点“Create new widget”。弹窗让填名字、选偏向设计画板还是FigJam白板,这里选普通设计画板就行。下一步有三个模板:带计数器的示例、带iframe的(能调浏览器API)、还有空模板。因为后面要联网抓数据,选空模板更灵活,但得自己补上fetch能力。保存项目到本地文件夹后,终端切进去先别跑命令——后面故意制造个错误来理解沙箱机制。

扒皮目标网页样式

打开Chris Coyier的设计名言网站,按Ctrl+Shift+C(Mac是Cmd+Shift+C)调出元素选择器,点一下名言卡片。再按住Shift点颜色块,把色值转成HEX格式。扒下来三组关键数据:背景色#ffffff,正文字体Lora 36px颜色#545454,作者字体Raleway 26px加粗颜色#16B6DF。顺便把左右两个引号SVG源码抠出来存好。这一步虽然枯燥,但后面写样式时直接抄数值能省一半调试时间。

搭骨架

code.tsx里引入核心模块:

const { widget } = figma;
const { AutoLayout, Text, SVG } = widget;

函数组件返回JSX结构:

function QuotesWidget() {
  return (
    <AutoLayout name={"Quote"} direction={"horizontal"} spacing={54} padding={{horizontal: 61, vertical: 47}}>
      <SVG name={"LeftQuotationMark"} src={leftQuotationSvgSrc} />
      <AutoLayout name={"QuoteContent"} direction={"vertical"} spacing={10}>
        <Text name={"QuoteText"}>这里放名言</Text>
        <Text name={"QuoteAuthor"}>— 这里放作者</Text>
      </AutoLayout>
      <SVG name={"RightQuotationMark"} src={rightQuotationSvgSrc} />
    </AutoLayout>
  );
}
widget.register(QuotesWidget);

注意每个组件都要塞name属性,不然调试时图层树里全是一堆匿名块,改样式能改到眼瞎。<AutoLayout>相当于Figma里的自动布局容器,direction决定子元素是横着排还是竖着排,spacing控制子元素之间的缝隙,padding给内边距。引号SVG通过src属性塞进去,字符串里放那坨长xml。

热更新翻车现场

Shift+I打开小部件菜单,切到Development页,把刚写的小部件拖进画板。如果底部冒红条报错,点“Open console”看日志——大概率是因为TypeScript还没编译成JS。回到终端跑npm install && npm run watch(或用yarn)。编译成功后,画板里的小部件应该能显示一个白底框加俩引号图标。但修改代码后有时不会自动重绘,这时候对着小部件右键 → Widgets → Re-render widget,强制刷新。

填样式

给根<AutoLayout>加背景:fill={"#ffffff"}。内部文本组件套上从网页扒来的字体参数:

<Text name={'QuoteText'}
  fontFamily={'Lora'}
  fontSize={36}
  width={700}
  fill={'#545454'}
  fontWeight={'normal'}
>{quote}</Text>

<Text name={'QuoteAuthor'}
  fontFamily={'Raleway'}
  fontSize={26}
  width={700}
  fill={'#16B6DF'}
  fontWeight={'bold'}
  textCase={'upper'}
>— {author}</Text>

width={700}限制文本最大宽度防止折行撑爆布局,textCase={'upper'}把作者名强制转大写。改完保存回Figma看一眼,样式应该八九不离十了。

上状态

要让名言能刷新,得用useSyncedState钩子。这个比React的useState多一个字符串key,因为Figma需要在所有协作者屏幕之间同步状态值。

const { useSyncedState } = widget;

function QuotesWidget() {
  const [quote, setQuote] = useSyncedState("quote-text", "");
  const [author, setAuthor] = useSyncedState("quote-author", "");
}

key必须全局唯一,比如"quote-text""quote-author"。初始值给空字符串,后面联网抓数据后再更新。

跨沙箱偷数据

Figma把第三方代码关在沙箱里,不能直接调fetch()。解决方案是开一个隐藏的<iframe>,让iframe里的HTML代码去发网络请求。先在manifest.json里声明UI文件:

{
  "ui": "ui.html"
}

然后创建ui.html,里面写一个消息监听器:

<script>
window.onmessage = async (event) => {
  if (event.data.pluginMessage.type === 'networkRequest') {
    const randomPage = Math.round(Math.random() * 100);
    const res = await fetch(`https://quotesondesign.com/wp-json/wp/v2/posts/?orderby=rand&per_page=1&page=${randomPage}&_fields=title,yoast_head_json`);
    const data = await res.json();
    const authorName = data[0].title.rendered;
    let quoteContent = data[0].yoast_head_json.og_description;
    // 清洗HTML实体
    quoteContent = decodeEntities(quoteContent);
    window.parent.postMessage({ pluginMessage: { authorName, quoteContent } }, '*');
  }
};
// 解码函数省略具体实现,参考原stackoverflow方案
</script>

回到code.tsx写一个fetchData函数,通过figma.showUI触发iframe里的请求:

function fetchData() {
  return new Promise<void>(resolve => {
    figma.showUI(__html__, {visible: false});
    figma.ui.postMessage({type: 'networkRequest'});
    figma.ui.onmessage = ({authorName, quoteContent}) => {
      setAuthor(authorName);
      setQuote(quoteContent);
      resolve();
    };
  });
}

注意__html__是Figma内置变量,会自动读ui.html的内容。visible: false让iframe隐藏着干活,不干扰画板。

只抓一次不无限循环

如果在组件函数体里直接调用fetchData(),Figma会报错“Cannot use showUI during widget rendering”。得用useEffect配合waitForTask,并且加个判断防止状态更新后反复触发:

const { useEffect, waitForTask } = widget;

function QuotesWidget() {
  useEffect(() => {
    if (!author.length && !quote.length) {
      waitForTask(fetchData());
    }
  });
}

这里不传依赖数组,因为Figma版的useEffect行为跟React不一样——它会在每次重绘后都跑一遍,所以靠检查author和quote是否为空来决定要不要重新抓数据。如果遇到“memory access out of bounds”的报错,别慌,关掉Figma重启就能消掉。

加个刷新按钮

每次想换名言总不能删掉小部件重新拖,太憨了。用usePropertyMenu钩子能在选中小部件时弹出一个工具栏菜单,塞一个刷新图标:

const { usePropertyMenu } = widget;

function QuotesWidget() {
  usePropertyMenu(
    [
      {
        itemType: "action",
        propertyName: "generate",
        tooltip: "换一句",
        icon: `<svg width="22" height="15" viewBox="0 0 22 15" ...省略... </svg>`,
      },
    ],
    () => fetchData()
  );
}

itemType: "action"表示这是一个按钮,propertyName是唯一标识,tooltip鼠标悬停提示,icon塞一个SVG字符串。第二个参数是点击后的回调函数,直接调用fetchData重新抓名言。这个钩子一行代码就把交互加上了,比写右键菜单省心太多。

打包发布到社区

小部件做完了不能自己偷着乐。按Shift+I切到Development页,找到自己的小部件,点三个点的菜单选“Publish”。弹窗里填标题、描述、打标签(比如“设计工具”、“名言生成器”)。需要上传一张128×128的图标和1920×960的横幅图。关掉发布弹窗(数据不会丢),在画板里右键小部件 → Copy/Paste → Copy as PNG,截一张截图。重新打开发布弹窗粘贴截图,滚动到底部点“Publish”。之后Figma官方会人工审核,大概5-10个工作日给结果。被拒了也没事,按反馈改完重新提交就行。

组件名主要作用常用属性
AutoLayout自动布局容器direction, spacing, padding
Text文本渲染fontFamily, fontSize, fill
SVG矢量图形src
useSyncedState跨端状态同步key, 初始值
usePropertyMenu工具栏菜单itemType, propertyName

这套流程走下来,一个会联网、能刷新、样式跟网页一模一样的Figma小部件就落地了。剩下能折腾的方向多得离谱——搞个投票器、接个天气API、甚至做个井字棋摸鱼小游戏。关键是把<iframe>那套通信机制吃透,任何需要浏览器API的骚操作都能塞进去。