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的骚操作都能塞进去。
