如何构建交互式Figma小工具(附代码)

743 阅读15分钟

Figma 一直鼓励开发人员和设计师之间的合作。它致力于建立一个无穷无尽的社区制造的插件库。需要 3D 元素吗?有这样一个插件;需要抽象的 SVG 吗? 也有这样一个插件

也就是说,Figma 的设计部分一直都是相对静态的--总是用不可移动的矩形通过预定义的用户互动相互连接起来工作。但是,如果我告诉您,您的设计可以突然活起来--它们可以是动画的、互动的,甚至是有状态的呢?那么,是什么将概念与实施分开呢?

Figma在6月宣布,它将把由JavaScript驱动的小工具带到桌面上。现在,设计师可以直接在 Figma 中浏览和实现逻辑驱动的组件了

Widgets API问好!您想知道它是什么以及如何使用它吗?这正是我们在这篇文章中要做的事情。

Figma widgets 开辟了大量的可能性

想象一下,您正在与您的合作伙伴昼夜不停地工作,以设计一个大型餐厅应用程序。你们已经在同一个 Figma 板上进行了协作;你们两个人都在共享完全相同的文档,并且随时发生变化。

当然,你已经知道,协作涉及的不仅仅是设计过程:

  • 项目管理
  • 举办民意调查以收集投票
  • 导入和可视化模拟数据
  • 甚至可能在工作了许多小时后玩一个多人游戏来冷静一下

我们只需要一个人管理一切,并向小组的其他成员发送链接。但是,哦,这不是很有效率,是吗?

嗯,这就是小工具发挥作用的地方。可以想象,我们可以在不离开 Figma 的情况下完成所有这些工作 - 是的,所有工作。

以下是您可能想在 Figma 中使用小组件的几种方式:

这个清单不胜枚举。正如你所知道的,已经有大量的小工具,你可以在你的文件中自由使用。事实上,你可以从Widgets菜单中直接添加Widgets到你的黑板上 (Shift+I)。

但我们在这里不是要学习如何使用部件,因为那很容易。让我们做我们最擅长的事情:我们将创建我们自己的Figma小工具这个小工具的灵感来自Chris Coyier 的设计报价网站。我们将采用 API,将其输入小组件,然后在 Figma 中直接显示随机设计报价。

以下是我们需要的东西

我不喜欢做坏消息的承担者,但是为了开发部件,你必须在Windows或Mac上。Linux用户,我很抱歉,但你不走运了。(如果你想跟随,你仍然可以使用虚拟机)。

我们将下载Figma Desktop应用程序。最简单的方法是直接从应用程序中生成一个小组件模板,开始使用。

让我们通过打开部件菜单(Shift+I )创建一个新的板块,切换到开发标签,并创建一个新项目。

之后,Figma 会提示您为新的小组件命名,并决定它是否更适合设计板或 FigJam 板。就本文而言,前一个选项就足够了。

自定义并没有结束;Figma 还会让您选择从一个预制的计数器部件开始,或者选择一个支持 iFrame 的替代品,该替代品还允许您访问 Canvas 和 Fetch API(以及所有其他浏览器 API)。我们将使用简单的 "空 "选项,但我们最终会自己修改它以利用Fetch API。

然后你会被提示将你的新部件项目保存到你系统中的一个特殊目录。一旦完成,启动你的终端并将其指向该文件夹。先不要运行任何命令--我们稍后会这样做,故意出错,目的是学习更多关于Widgets API的知识。

设计小部件

我们直接从Chris Coyier的设计报价网站上提取设计。所以,让我们去那里,通过启动DevTools来潜心研究。

我在这里使用的两个快捷键是:Ctrl+Shift+C (或Cmd+Shift+C )来切换 "挑选元素 "工具,以及Shift+Click 来改变颜色格式为HEX代码。我们这样做是为了了解克里斯的网站中使用的颜色、字体、字体重量和字体大小。所有这些信息对于在Figma中建立一个近似的小部件至关重要,这将是我们的下一步工作您可以抓取设计好的组件,并在您自己的画布中使用它。

由于本文的主要话题是通过编写代码来构建小部件,所以我不会在这里多说什么。但我不能不强调,好好照顾你的小部件的风格是多么重要......CSS-Tricks 已经有大量面向设计的 Figma 教程;你不会后悔把它们加入你的阅读列表。

为我们的小工具创建布局

随着设计的完成,现在是时候把我们的编程手指拿出来,开始建立我们的小工具的齿轮。

Figma 如何将其设计构件转化为类似 React 的组件,这非常有趣。例如,具有自动布局功能的框架元素,在代码中被表示为<AutoLayout /> 组件。除此以外,我们还将使用另外两个组件:<Text /><SVG />.

看看我的Figma板......我正是要求你把注意力放在对象树上。这是我们需要的,以便能够将我们的小部件设计转化为JSX代码。

正如你所看到的,我们的设计引用小部件需要导入三个组件。考虑到完整的API只包含八个基于层的节点,这是一个体面的组件数量。但你很快就会看到,这些模块对于制作各种布局是绰绰有余的。

// code.tsx
const { widget } = figma;
const { AutoLayout, Text, SVG } = widget;

有了这些,我们就可以像在React中那样,继续构建我们的部件的骨架了。

function QuotesWidget() {
  const quote = `...`;
  const author = `...`;

  return (
    <AutoLayout>
      <SVG />
      <AutoLayout>
        <Text>{quote}</Text>
        <Text>— {author}</Text>
      </AutoLayout>
      <SVG />
    </AutoLayout>
  );
}

widget.register(QuotesWidget);

至少可以说,这段代码是非常混乱的。现在,我们无法将设计层区分开来。值得庆幸的是,通过使用name 属性,我们能够轻松解决这个问题。

<AutoLayout name={"Quote"}>
  <SVG name={"LeftQuotationMark"} />
  <AutoLayout name={"QuoteContent"}>
    <Text name={"QuoteText"}>{quote}</Text>
    <Text name={"QuoteAuthor"}>— {author}</Text>
  </AutoLayout>
  <SVG name={"RightQuotationMark"} />
</AutoLayout>;

当然,我们仍然无法看到我们的引号SVG,所以让我们努力解决这个问题。<SVG/> 组件接受一个src属性,该属性接受一个SVG元素的源代码。在这个问题上没有什么可说的,所以让我们保持简单,直接跳回代码。

const leftQuotationSvgSrc = `<svg width="117" height="103" viewBox="0 0 117 103" fill="none" xmlns="<http://www.w3.org/2000/svg>">
  // shortened for brevity
</svg>`;
const rightQuotationSvgSrc = `<svg width="118" height="103" viewBox="0 0 118 103" fill="none" xmlns="<http://www.w3.org/2000/svg>">
// shortened for brevity
</svg>`;

function QuotesWidget() {
  return (
    <SVG name={"LeftQuotationMark"} src={leftQuotationSvgSrc} />
    <SVG name={"RightQuotationMark"} src={rightQuotationSvgSrc} />
  );
}

我想我们都可以同意,现在一切都清楚多了当我们给事物命名时,它们的目的对于我们代码的读者来说突然变得更加明显。

实时预览我们的小部件

Figma 在构建小组件时提供了很好的开发者体验,包括(但不限于)热重载。有了这个功能,我们就可以实时地对我们的小组件进行编码和预览修改。

通过打开小部件菜单(Shift+I),切换到开发标签,点击或拖动你的新小部件到板上,就可以开始了。无法找到你的小部件?别担心,只需点击三点菜单并导入你的小部件的manifest.json 文件。是的,这就是让它恢复存在的全部方法

等等,你是否在屏幕底部收到一个错误信息?

如果是的话,让我们来调查一下。点击 "打开控制台"并阅读它所说的内容。如果 "打开控制台"按钮没有了,还有一种方法可以打开调试控制台。点击Figma标志,跳到小工具类别,揭示开发菜单。

这个错误可能是由于我们还没有把 TypeScript 编译成 JavaScript。我们可以在命令行中通过运行npm installnpm run watch.(或yarnyarn watch )来做到这一点。这次没有错误了

你可能遇到的另一个障碍是,当代码被改变时,小部件不能重新渲染。我们可以使用下面的上下文菜单命令轻松地强迫我们的小组件更新:小组件重新渲染小组件

小工具的样式

就目前而言,我们的小组件的外观离我们的最终目标还很远。

那么,我们如何从代码中对 Figma 组件进行造型呢?也许像我们在 React 项目中那样用 CSS?不可能。在 Figma 部件中,所有的造型都是通过一组有据可查的道具进行的。我们很幸运,这些项目的名称与 Figma 中的对应项目几乎完全相同

我们将从配置我们的两个<AutoLayout /> 组件开始。正如你在上面的信息图中所看到的,道具名称对它们的用途有很好的描述。这使得我们可以很容易地直接跳到代码中,开始做一些改变。我不会再展示整个代码,所以请依靠组件名称来指导你的代码片段的归属。

<AutoLayout
  name={"Quote"}
  direction={"horizontal"}
  verticalAlignItems={"start"}
  horizontalAlignItems={"center"}
  spacing={54}
  padding={{
    horizontal: 61,
    vertical: 47,
  }}
>
  <AutoLayout
    name={"QuoteContent"}
    direction={"vertical"}
    verticalAlignItems={"end"}
    horizontalAlignItems={"start"}
    spacing={10}
    padding={{
      horizontal: 0,
      vertical: 0,
    }}
  ></AutoLayout>
</AutoLayout>;

我们刚刚取得了很大的进展!让我们保存并跳回 Figma,看看我们的小组件看起来如何。还记得 Figma 是如何在有新变化时自动重新加载小组件的吗?

但它还没有完全实现。我们还必须为根组件添加背景色。

<AutoLayout name={"Quote"} fill={"#ffffff"}>

再一次,看看您的 Figma 板,注意到变化几乎可以立即反映到小组件中。

让我们沿着这个指南前进,对<Text> 组件进行样式设计。

在看了Widgets API文档后,我们再次清楚地看到,属性名称与Figma应用程序中的对应名称几乎完全相同,从上面的信息图中可以看出。我们还将使用上一节中检查克里斯的网站时的数值。

<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>

向小组件添加状态

我们的小组件目前显示相同的报价,但我们想从整个报价池中随机抽取。我们必须给我们的widget添加*状态*,所有React开发者都知道这是一个变量,其变化会触发我们组件的重新渲染。

在Figma中,状态是通过[useSyncedState](https://www.figma.com/widget-docs/widget-state/)钩子创建的;它几乎是React的useState但它要求程序员指定一个唯一的密钥。这一要求源于这样一个事实:Figma 必须在所有可能正在查看同一设计板但通过不同电脑的客户端之间同步我们的小组件的状态。

const { useSyncedState } = widget;

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

这就是我们现在需要的所有变化。在下一节,我们将弄清楚如何从互联网上获取数据。剧透一下:这并不像看起来那么简单。

从网络上获取数据

记得Figma让我们选择从一个支持iFrame的小部件开始。虽然我们没有选择这个选项,但我们仍然必须实现它的一些功能。让我解释一下为什么我们不能简单地在我们的小组件代码中调用fetch()

当你使用一个部件时,你是在你自己的计算机上运行由其他人编写的JavaScript代码。虽然所有小组件都经过 Figma 工作人员的彻底审查,但这仍然是一个巨大的安全漏洞,因为我们都知道哪怕是一行 JavaScript 就能造成多大的破坏

因此,Figma 不能简单地[eval()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval)任何由匿名程序员编写的小部件代码。长话短说,团队决定,最好的解决方案是在一个严密保护的沙盒环境中运行第三方代码。而你可能已经猜到了,浏览器 API 在这样的环境中是不可用的。

但是不要着急,Figma 对这第二个问题的解决方案是<iframe>s。我们在一个文件中编写的任何 HTML 代码,最好称为ui.html ,都可以访问所有浏览器的 API。您可能想知道我们如何从小组件中触发这段代码,但我们稍后会研究这个问题。现在,让我们跳回到代码中。

// manifest.json
{
  "ui": "ui.html"
}
<!-- ui.html -->
<script>
window.onmessage = async (event) => {
  if (event.data.pluginMessage.type === 'networkRequest') {
    // TODO: fetch data from the server

    window.parent.postMessage({
      pluginMessage: {
        // TODO: return fetched data
      }
    }, '*')
  }
}
</script>

这就是小部件到iframe 通信的一般模板。让我们用它来从服务器上获取数据。

<!-- ui.html -->
<script>
window.onmessage = async (event) => {
  if (event.data.pluginMessage.type === 'networkRequest') {
    // Get random number from 0 to 100
    const randomPage = Math.round(Math.random() * 100)

    // Get a random quote from the Design Quotes API
    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()

    // Extract author name and quote content from response
    const authorName = data[0].title.rendered
    const quoteContent = data[0].yoast_head_json.og_description

    window.parent.postMessage({
      pluginMessage: {
        authorName,
        quoteContent
      }
    }, '*')
  }
}
</script>

我们撇开错误处理,以保持简单和重点。让我们跳回到widget的代码中,看看我们如何访问在<iframe> 中定义的函数。

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

    figma.ui.onmessage = async ({authorName, quoteContent}) => {
      setAuthor(authorName)
      setQuote(quoteContent)

      resolve()
    }
  })
}

正如你所看到的,我们首先告诉 Figma 公开对我们的隐藏<iframe> 的访问,并触发一个名称为"networkRequest" 的事件。我们在ui.html 文件中通过检查event.data.pluginMessage.type === 'networkRequest', 来处理这个事件,然后将数据发回给小组件。

但是现在还没有发生什么......我们仍然没有调用fetchData() 函数。如果我们在组件函数中直接调用它,控制台中会出现以下错误。

Cannot use showUI during widget rendering.

Figma告诉我们不要在函数体中直接调用showUI......那么,我们应该把它放在哪里?答案是一个新钩子和一个新函数。[useEffect](https://www.figma.com/widget-docs/api/properties/widget-useeffect/)[waitForTask](https://www.figma.com/widget-docs/api/properties/widget-waitfortask/).如果你是React的开发者,你可能已经熟悉了useEffect ,但我们在这里要用它来从服务器上获取小部件组件安装时的数据。

const { useEffect, waitForTask } = widget;

function QuotesWidget() {
  useEffect(() => {
    waitForTask(fetchData());
  });
}

但这将导致另一个 "错误",我们的小部件将一直用一个新的引号重新渲染,永远如此。这是因为useEffect ,根据定义,每当小组件的状态发生变化时就会再次触发,也就是当我们调用fetchData 。虽然在 React 中有一种技术可以只调用useEffect 一次,但它在 Figma 的实现中不起作用。来自Figma的文档。

由于Widgets的运行方式,useEffect 应该处理在相同状态下被多次调用。

值得庆幸的是,我们可以利用一个简单的变通方法,在组件第一次安装时只调用一次useEffect ,它是通过检查状态的值是否仍然是空的。

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

你可能会遇到一个可怕的 "**内存访问越界 "**的错误。这在插件和部件开发中很常见。只要重新启动 Figma,它就不会再出现了。

您可能已经注意到,有时报价文本包含奇怪的字符。

这些都是Unicode 字符,我们必须在代码中正确格式化它们。

<!-- ui.html -->
<script>
window.onmessage = async (event) => {
  // ...
  const quoteContent = decodeEntities(data[0].yoast_head_json.og_description);
};

// <https://stackoverflow.com/a/9609450>
var decodeEntities = (function () {
  // this prevents any overhead from creating the object each time
  var element = document.createElement("div");

  function decodeHTMLEntities(str) {
    if (str && typeof str === "string") {
      // strip script/html tags
      str = str.replace(/<script[^>]*>([\\\\S\\\\s]*?)<\\\\/script>/gim, "");
      str = str.replace(/<\\\\/?\\\\w(?:[^"'>]|"[^"]*"|'[^']*')*>/gim, "");
      element.innerHTML = str;
      str = element.textContent;
      element.textContent = "";
    }

    return str;
  }

  return decodeHTMLEntities;
})();
</script>

这样,我们的小组件每次添加到设计板时都会获取一个全新的设计报价。

为我们的小组件添加一个属性菜单

虽然我们的小组件在实例化时获取了一个新的报价,但如果我们能再做一次这个过程,但不删除它,那就更实用了。这一节会很短,因为这个解决方案是相当了不起的。通过属性菜单,我们可以为我们的部件添加交互性,只需调用[usePropertyMenu](https://www.figma.com/widget-docs/api/type-PropertyMenu)钩子。

信用:Figma 文档

const { usePropertyMenu } = widget;

function QuotesWidget() {
  usePropertyMenu(
    [
      {
        itemType: "action",
        propertyName: "generate",
	tooltip: "Generate",
        icon: `<svg width="22" height="15" viewBox="0 0 22 15" fill="none" xmlns="<http://www.w3.org/2000/svg>">
          <!-- Shortened for brevity -->
        </svg>`,
      },
    ],
    () => fetchData()
  );
}

通过一个简单的钩子,我们就能创建一个按钮,当我们的小组件被选中时,它就会出现在附近。这是我们为完成这个项目而需要添加的最后一块。

向公众发布我们的小部件

如果没有人使用它,那么建立一个小组件就没有什么用处。虽然Figma 允许组织选择推出供内部使用的 私人小组件,但向全世界发布这些小程序则更为常见。

Figma 有一个微妙的小程序审查过程,可能需要 5 到 10 个工作日。虽然我们一起建立的设计报价小工具已经在小工具库中,但我仍将演示它是如何到达那里的。请不要试图再次发布这个小工具,因为这只会导致被删除。但是,如果你给它做了一些重大的改动,请继续与社区分享你自己的小工具吧

通过点击小组件菜单(Shift+I)开始,并切换到 "开发"标签,查看我们的小组件。点击三点式菜单,然后按发布

Figma 会提示您输入有关您的小组件的一些细节,如标题、描述和一些标签。我们还需要一个128×128的图标图像和一个1920×960的横幅图像。

在导入所有这些资产后,我们仍然需要一张我们的小组件的屏幕截图。关闭发布模式(别担心,你不会丢失你的数据),右键单击小组件,显示一个有趣的上下文菜单。找到 "复制/粘贴 "类别,选择 "复制为PNG"。

完成后,让我们回到发布模式,粘贴小组件的屏幕截图。

向下滚动,最后发布你的模版。庆祝吧!🎉

Figma会在几天后联系你,告诉你模版的审核情况。如果被拒绝,你将有机会进行修改并再次提交

结论

我们刚刚从头开始建立了一个Figma小部件!这里还有很多东西没有涉及,比如点击事件输入表单等等您可以在这个GitHub repo中挖掘该部件的完整源代码。

对于那些渴望将自己的 Figma 技能提升到更高层次的人来说,我建议探索 Widgets 社区,并将吸引您的注意力的东西作为灵感。继续构建更多的部件,继续磨练你的 React 技能,在你意识到之前,你会教我如何做这一切。