Figma插件开发(二)

3,213 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

Figma插件开发(二)

前文《用Vue3开发Figma插件》中介绍了Figma插件原理,并用Vue3搭建了一套插件开发框架。本文继续讲述一些常用功能的开发实践。

Figma插件的原理和限制

先回顾一下Figma插件的运行机制。

Figma 将插件分成两个部分:插件UI运行在 iframe 中,操作画布的代码运行在主线程的隔离沙箱中。UI线程和主线程通过 postMessage 通信。

blog-figma-plugin_b114V2Tca3QkCHj6kHFCVv.png

这种隔离带来了安全性的保证,同时也带来了一些操作上的麻烦。

如果要将画布内容导出,需要利用浏览器API(例如:下载、通过网络上传等),则需要先在JS沙箱中获取到画布元素,然后通过postMessage传递到iframe中。iframe调用浏览器API来导出数据。

iframe

通过打开Firma的devtools,可以看到插件iframe的url是 Data URLs 格式。这是Figma将插件html 内容转成了base64格式。 截屏2022-04-10 上午11.19.53_ggX67RaH3HidNUUv2qa8S3.png

这会带来什么问题呢?我们在console面板里调试一下,可以看到在iframe中调用localStoragecookie 都会报错。localStoragecookie 都是根据域名来存储数据的,而使用了Data URLs 格式就没有域名了,所以无法存储数据。 截屏2022-04-10 上午11.48.11_gBWFbcQDFJPrNhknF6bhAn.png

于是 Figma提供了一个存储API figma.clientStorage 。这个API是在JS沙箱中调用的,也就是说数据是存储在主线程,而不是在iframe中。iframe中要存取数据的话,就必须通过 postMessage 接口与主线程通信。

window.postMessage

window.postMessage() 是 W3C 标准提供的接口,可以安全地实现iframe跨源通信。例如下面是 iframe向父窗口传递消息的示例:

//index.html
function receiveMessageFromIframePage (event) {
    console.log('receiveMessageFromIframePage', event)
}
window.addEventListener("message", receiveMessageFromIframePage, false);



// iframePage.html
parent.postMessage( {msg: 'MessageFromIframePage'}, '*');

postMessage中的消息只能是字符串类型,其他格式内容都必须先转换成字符串。我们有很多场景需要用到主线程和插件线程的通信,例如主线程调用网络、插件线程获取画布、插件线程存储数据等,都使用 postMessage 的话导致代码难以维护。

总结一下,由于Figma 插件系统的限制,我们在开发插件中会遇到下面几个问题:

  1. 登录鉴权。由于无法使用cookie,导致http请求无法携带登录态;

  2. 大量的操作需要使用 postMessage,代码维护困难;

通信接口的封装

首先要解决 postMessage 通信的问题。我们需要封装一个更高级的接口,要让插件线程和主线程能够非常方便地互相调用,最好有完善的 ts 类型支持。

经过一番搜索,我找到了 rpct-js 这个库。可以说非常完美。这个库是 rpc 通信的一个实现。什么是rpc ?简单来说就是“像调用本地方法一样调用远程方法”。可用于客户端与服务器之间、DOM Window之间互相调用。这个库还做了Figma Plugin的适配!

远程过程调用(英语:Remote Procedure Call,RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。

我们通过前文里的例子来看下用法。这个例子是通过插件里的两个按钮,实现在画布中加减色块。原代码如下:

// code.js, 在主线程执行

const nodes = [];

figma.ui.onmessage = (msg) => {
  if (msg.type === "add-block") {
    // 处理添加色块消息
    const rect = figma.createRectangle();
    rect.x = nodes.length * 150;
    rect.fills = [{ type: "SOLID", color: { r: 1, g: 0.5, b: 0 } }];
    figma.currentPage.appendChild(rect);
    nodes.push(rect);
  } else if (msg.type === "sub-block") {
    // 处理减少色块消息
    const rect = nodes.pop();
    if (rect) {
      rect.remove();
    }
  }
  figma.viewport.scrollAndZoomIntoView(nodes);
};


// index.html, 在插件里执行

// 添加色块
function addBlock() {
  var num = +blockNumEle.innerText;
  num += 1;
  blockNumEle.innerText = num;
  parent.postMessage({ pluginMessage: { type: 'add-block' } }, '*')
}

// 减少色块
function subBlock() {
  var num = +blockNumEle.innerText;
  if (num === 0) return;
  num -= 1;
  blockNumEle.innerText = num;
  parent.postMessage({ pluginMessage: { type: 'sub-block' } }, '*')
}


接入 rpct-js 后,主线程代码:

// code.ts

const nodes = [];

export class PluginMethods {
  addBlock() {
    const rect = figma.createRectangle();
    rect.x = nodes.length * 150;
    rect.fills = [{ type: "SOLID", color: { r: 1, g: 0.5, b: 0 } }];
    figma.currentPage.appendChild(rect);
    nodes.push(rect);
  }

  subBlock() {
    const rect = nodes.pop();
    if (rect) {
      rect.remove();
    }
  }
}

调用方(index.html内)的使用就变得非常简单了,可以直接通过pluginApi 调用定义在code.js 中的函数:

// index.html

function addBlock() {
  pluginApi.addBlock();
}

function subBlock() {
  pluginApi.subBlock();
}

其中pluginApi 是通过下面方式建立连接的:

import { connectToPlugin, proxyMapRemote, ProxyMapRemoteApi } from 'rpct/browser';
import { PluginMethods } from './code.ts';

class UIMethods {
  // ...
}

const rpctapi = await connectToPlugin<PluginMethods, UIMethods>(new UIMethods());
pluginApi = proxyMapRemote(rpctapi);

插件中可以通过 pluginApi 直接调用定义在 PluginMethods中的方法。同样的,主线程可以直接调用定义在 UIMethods 中的方法。

详细完整代码可以看figma-plugin-vue3

未完待续...

今天先写到这,后面会继续写网络请求、登录鉴权、图片上传等相关操作。