一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情。
Figma插件开发(二)
前文《用Vue3开发Figma插件》中介绍了Figma插件原理,并用Vue3搭建了一套插件开发框架。本文继续讲述一些常用功能的开发实践。
Figma插件的原理和限制
先回顾一下Figma插件的运行机制。
Figma 将插件分成两个部分:插件UI运行在 iframe 中,操作画布的代码运行在主线程的隔离沙箱中。UI线程和主线程通过 postMessage 通信。
这种隔离带来了安全性的保证,同时也带来了一些操作上的麻烦。
如果要将画布内容导出,需要利用浏览器API(例如:下载、通过网络上传等),则需要先在JS沙箱中获取到画布元素,然后通过postMessage传递到iframe中。iframe调用浏览器API来导出数据。
iframe
通过打开Firma的devtools,可以看到插件iframe的url是 Data URLs 格式。这是Figma将插件html 内容转成了base64格式。
这会带来什么问题呢?我们在console面板里调试一下,可以看到在iframe中调用localStorage和cookie 都会报错。localStorage和cookie 都是根据域名来存储数据的,而使用了Data URLs 格式就没有域名了,所以无法存储数据。
于是 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 插件系统的限制,我们在开发插件中会遇到下面几个问题:
-
登录鉴权。由于无法使用cookie,导致http请求无法携带登录态;
-
大量的操作需要使用
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。
未完待续...
今天先写到这,后面会继续写网络请求、登录鉴权、图片上传等相关操作。