目的:介绍 Figma 插件;figma 插件开发从 0 到 1;
分享自己开发的想法
Figma 初印象
figma 是一个 基于浏览器 的协作式 UI 设计工具
figma 插件初印象
加强功能:让选中元素一起旋转的同时,让每个元素自己单独旋转 ...
组合功能:让某批同类型元素先改变颜色,然后建立层次结构...
导出功能:将一些想要的元素内容设置为一些数据结构导出供其他程序使用...
导入功能:与导出功能相反...
替换重复工作
{
"data": [
{
"type": "container",
"name": "container_01",
"width": 222.78260803222656,
"height": 172.21739196777344,
"x": 16,
"y": 204,
"children": [
{
"type": "image",
"name": "玩具车3",
"width": 75.78260803222656,
"height": 50.21739196777344,
"x": 0,
"y": 0,
"index": [
0,
9,
0
],
"url": "https://sf6-ttcdn-tos.pstatp.com/img/edux-data/1627009282549ca8bcf0b17~0x0.png"
},
{
"type": "container",
"name": "container_02",
"width": 170.78260803222656,
"height": 124.21739196777344,
"x": 52,
"y": 48,
"children": [
{
"type": "image",
"name": "Frame_662",
"width": 151.78260803222656,
"height": 124.21739196777344,
"x": 11,
"y": 0,
"index": [
0,
9,
1,
0
],
"url": "https://sf3-ttcdn-tos.pstatp.com/img/edux-data/1627009282384e5da6a2689~0x0.png"
},
{
"type": "image",
"name": "玩具车2",
"width": 75.78260803222656,
"height": 50.21739196777344,
"x": 95,
"y": 9,
"index": [
0,
9,
1,
1
],
"url": "https://sf6-ttcdn-tos.pstatp.com/img/edux-data/1627009282222dfb3a4ffa4~0x0.png"
},
{
"type": "image",
"name": "玩具车1",
"width": 75.78260803222656,
"height": 50.21739196777344,
"x": 0,
"y": 45,
"index": [
0,
9,
1,
2
],
"url": "https://sf3-ttcdn-tos.pstatp.com/img/edux-data/16270092820316c732f80fe~0x0.png"
}
],
"index": [
0,
9,
1
]
}
],
"index": [
0,
9
]
}
],
"scene": "ER_L1"
}
如上图所示,整个插件分为两个部分,左边成为沙箱线程,之后就叫做主线程,右边为 iframe
线程,之后就叫 UI 线程
整个插件的入口是主线程,主线程采用一种沙盒结构,其中存在类似 node
环境,可以运行 js 代码,访问 ES6
的 API,可以去操控 figma 里面的内容。
由主线程创建创建 iframe
,并将我们写的 html
插入,由主线程控制整个 iframe
的大小,在 html
中可以使用浏览器的 API,比如说向后端发起请求,可以和用户进行交流。
UI 线程与主线程通过 postMessage 相互通信,所以这里可以有个流程,主线程先运行,主线程设置接收UI线程信息的方法,创建 UI,UI 设置接收信息方法。注意:两者通信内容有限制,除了常见的JSON数据类型外,还有 blob,arraybuffer,像在 figma 中的一些对象都不能直接传输
这两个部分分别就是我们需要提供的两份文件
插件能暴露文件的内容,这个内容指的是我们在 figma 插件中看到的,比如某个元素的尺寸,位置,层次结构,颜色,文本内容等,我们不仅能获取到,还能更改。
iframe
)indexedDB
)有网络请求;打开文件;使用 canvas
,webgl(pixi)
;使用 WebAssembly
,使用音频 API;
打开创建插件的窗口
创建 manifest.json 文件
manifest.json 内容
{
"name": "lego-quiz-figma",
"id": "996264569045667578",
"api": "1.0.0",
"main": "dist/code.js",
"ui": "dist/ui.html"
}
注意:id,main,ui
id
有一些操作必须得用到这个 id,否则操作会被拒绝,目前我在让 figma 存储一些内容时,必须得在这里声明 id,否则报错。这个 id 生成的方式是,上图中选 "生成新的 manifest.json" ,在生成的 json 文件中就带有属于当前插件的 id
Main 和 ui
回顾 figma 插件两大组成,main 对应主线程内容,ui 对应 ui 线程内容。这里存放的只是路径,在开发模式下,意味着懒加载,即启动时才去相应路径取文件,所以后面我们开发时可以使用 webpack watch
特性,每次更新程序都会把更新程序打包到 dist 文件下,在之后打开插件取的就是更新后的插件,在发布模式下,文件会上传至公司内部,因此地址会发生变化,需要手动上传更新
总结
Figma 插件开发时,只需要向 figma 软件提供一个 manifest.json 文件即可,但是在 manifest.json 文件中必须带有 ui 线程和主线程需要的两份文件地址,当然相应文件得存在。在完成一些特别的操作,需要提供 id,id 声明在 manifest.json 文件中。在开发模式下,dist 文件夹中的内容可以实时更新,在 figma 中重启插件即可应用新的插件内容。
Figma 获取到 manifest.json 文件后,会先执行 main 中的内容
插件需要两个文件,主线程用于和 figma 进行交互,ui 线程用于和用户进行交互。我们开发肯定不是直接在这两个文件中写内容,而是将写的内容打包到这两个文件中。
webpack 配置在项目中,建议后续开发在我这个配置上更改,因为里面有些内容是 figma 官方提供,官方提供插件案例代码地址:figma 插件案例[1]
打包写入时有个细节,使用 HtmlWebpackInlineSourcePlugin
将代码嵌入到 ui.html 中,这里不能使用 link 或是 script 的 src 标签,因为 figma 只要 manifest.json 中声明的文件。
使用 ts 时安装 npm install --save-dev @figma/plugin-typings
获取 figma 中各种元素类型
/// <reference path="../../node_modules/@figma/plugin-typings/index.d.ts" />
import { receiveUIMessage, sendCurrentMode } from './ui-relation';
figma.showUI(__html__, { visible: true, width: 300, height: 180 });
function start() {
// 1. 设置接受 ui 方法,第一步
receiveUIMessage();
// 2. 获取当前模式, 并发送 UI
sendCurrentMode();
}
start();
说明
Reference
用作 figma 元素的类型提示具体内容后面再看,主线程代码完成,并且创建了 ui ,接下来就是把我们写的 ui 嵌入
return (
<div className="upload-image">
<div className="button-group">
<Button
loading={loading(imageInfo)}
icon={<CopyOutlined />}
id="copy-btn"
data-clipboard-text={addImageInfo(imageInfo)}
>
复制
</Button>
<Button onClick={updateEvent} icon={<RetweetOutlined />}>
刷新
</Button>
</div>
<p className="label-model">业务场景</p>
<Radio.Group onChange={onChange} value={model}>
{/* 中点y轴向下 */}
<Radio value={Models.COMMON_DEV}>通用</Radio>
{/* 0,0 y轴向下 */}
<Radio value={Models.ER_L1}>ER L1</Radio>
{/* 中点y轴向上 */}
<Radio value={Models.ER_GAME}>ER 课后练习</Radio>
</Radio.Group>
</div>
);
这是使用 react
生成一个 div 标签,作为 ui 中的子节点,与平常开发网页类似,最后将有关内容全都集合在一个 ui.html
中
导入 manifest.json 过程之前已经介绍,导入后执行过程如下
点击执行-->执行主线程文件-->创建 iframe
-->插入 UI 内容
找到插件管理
发布插件
主线程文件名为 code
postmessage
接收和传输消息// 发送消息到 UI
export function transferUIData(transferData: CodeToUIData) {
figma.ui.postMessage(transferData);
}
// 接受来自 UI 的消息
export function receiveUIMessage() {
figma.ui.onmessage = ({ message, type }) => {
if (type === UIToCodeType.UPDATE) {
figmaStorage.setData('model', message);
startDataTransform();
}
};
}
LocalStorage
和 indexedDB
)export const figmaStorage = {
cache: {},
async getData(key: string) {
if (this.cache[key]) {
return this.cache[key];
}
// 主要 api
const value = await figma.clientStorage.getAsync(key);
this.cache[key] = value;
return value;
},
// value 可以是任意类型数据
setData(key: string, value: any) {
if (this.cache[key] && this.cache[key] === value) {
return;
}
this.cache[key] = value;
// 主要 api
return figma.clientStorage.setAsync(key, value);
}
};
postmessage
传输到 ui 线程,获取选中元素的顺序是有问题的:
选中此页面上的节点。每个页面分别存储自己的选择。选择中的节点顺序是未指定的,您不应该依赖它
我的解决方案,记录下每个元素的层级信息 [0,8,4],[1,6,5],[0,8,5],[1,7],[2] 根据每位数值判定谁是上级谁是下级(先后顺序判断 )
获取选中节点 api: figma.currentPage.selection
,得到选中元素的数组
遍历每一个元素,将每个元素转化成预期数据结构并存入一个数组中
不同场景下,每个元素的坐标略有差异,每个元素经过转化后的类型
// 1. 创建对象
const nodeData = {
type: exportNodeType,
name: normalName,
width: width,
height: height,
x: x + pos.x,
y: y + pos.y,
bytes,
children: null
};
type:分为四种类型,zone:热区,locateDot:锚点,container:容器,image:图片
热区:表示一块区域,这块区域用于一些判断操作,最后预览时不会显示,比如某些点击事件只能在这里面进行操作,比如某些元素只能在区域内部移动等
锚点:给某些元素提供一个参考原点,便于计算
容器:专门用作多级目录使用,存储自己以及孩子的信息,container
自己没有需要显示的内容
图片:基本上能看到的内容都属于图片类型
转化过程需要注意:在 figma 中,frame 和 group 坐标计算方式是不同的
建立一个原点,让选中的所有元素的 x 和 y 都是基于这个原点,剔除 frame 对其孩子的影响
const pos = {
x: 0,
y: 0
};
let temp = findFrameNode(node.parent);
let ratio = 1;
while (temp) {
ratio = temp.width / CONVENTION_SIZE.width;
if (frameMayBeContainer(temp, ratio)) {
break;
}
if (temp.parent.type !== 'PAGE') {
// 当前 frame 在 page 中的地方
pos.x += temp.x;
pos.y += temp.y;
}
temp = findFrameNode(temp.parent);
}
如果元素是图片,将图片导出成 Uint8Array
格式存入 bytes 属性中
// 遍历 node
if (exportNodeType === 'image') {
nodeData.bytes = await node.exportAsync({
format: 'PNG',
constraint: {
type: 'SCALE',
value: 1.5
}
});
delete nodeData.children;
}
如果元素是 container 并且有孩子,那么递归调用自己
if ('children' in node) {
for (const child of node.children) {
nodeData.children.push(await getImageInfo(child, originNode));
}
}
前面工作完成后,就可以开始转换每个元素的坐标,影响坐标的元素有两大点,第一大点是当前场景,第二大点是选中节点是 container 类型
转化方法如下
// nodeInfo 为节点转化后的信息,position 相对原点
// originInfo 是离节点最近的参考点,主要用于 container,如果没有 container,最近参考点就是原点
function getModelPosition(nodeInfo: ExportNodeInfo, originInfo) {
return {
[Models.COMMON_DEV]: () => ({
x: normalNum(
nodeInfo.x - originInfo.originX - originInfo.w + nodeInfo.width / 2
),
y: normalNum(
nodeInfo.y - originInfo.originY - originInfo.h + nodeInfo.height / 2
)
}),
[Models.ER_L1]: () => ({
x: normalNum(nodeInfo.x - originInfo.originX),
y: normalNum(nodeInfo.y - originInfo.originY)
}),
[Models.ER_GAME]: () => ({
x: normalNum(
nodeInfo.x - originInfo.originX - originInfo.w + nodeInfo.width / 2
),
y: normalNum(
-nodeInfo.y + originInfo.originY + originInfo.h - nodeInfo.height / 2
)
})
};
}
这里还要考虑在 edit 中,三个场景的区别,方便后续将数据传入并解析
首先分析上述代码,以 ER_L1 为例,同时这也是最容易转换的情况
ER_L1 坐标计算方式与 figma 相同,因此不需要进行坐标变换,减去最近参考点坐标的原因如下
有同学肯定会问,之前把参考点都置为原点,现在又把参考点置为最近参考点,有必要吗?
有,置为原点原因在于:可能直接复制 frame 下的元素,此时该元素的 X 和 Y 的参考点就应该是原点,而不是 父节点frame。还原的原因是复制 container 时,由于要记录层级关系,因此要转化为相对于父节点的坐标,而非全局坐标
window.addEventListener('message', (event) => {
const { type, data } = event.data.pluginMessage;
switch (type) {
case CodeToUIType.UploadImage:
uploadHandler(data);
break;
// 初始化 模式
case CodeToUIType.ModelData:
setModel(data);
break;
case CodeToUIType.COMMON_MESSAGE:
message[data[0]]({
content: data[1],
className: 'message-style',
duration: 2
});
}
});
const sendMessageToCore = (message: any, type: UIToCodeType) => {
// pluginMessage, * 是不可变的
parent.postMessage({ pluginMessage: { message, type } }, '*');
};
三个场景 x 轴正方向都是 向右👉
通用:COMMON_DEV,锚点在 (0.5,0.5),y 轴正方向 向下👇
ER L1:ER_L1,锚点在(0,0),y 轴正方向 向下👇
ER 课后练习:ER_GAME,锚点在 (0.5,0.5),y 轴正方向 向上👆
不会做
figma 插件案例: https://github.com/figma/plugin-samples/tree/master/webpack