Figma外掛開發
目的:介紹 Figma 外掛;figma 外掛開發從 0 到 1;
分享自己開發的想法
一、Figma 是什麼,figma 外掛是什麼
Figma 初印象
figma 是一個 基於瀏覽器 的協作式 UI 設計工具
figma 外掛初印象
加強功能:讓選中元素一起旋轉的同時,讓每個元素自己單獨旋轉 ...
組合功能:讓某批同類型元素先改變顏色,然後建立層次結構...
匯出功能:將一些想要的元素內容設定為一些資料結構匯出供其他程式使用...
匯入功能:與匯出功能相反...
替換重複工作
二、展示 lego-quiz-figma 初步功能
-
外掛將選中元素儲存為圖片
-
根據不同場景,轉化元素的位置
-
圖片上傳到後端
-
調整元素順序
-
將當前場景儲存,之後直接使用
-
將當前位置複製到剪下板
-
重新整理當前選中元素,重新執行 1-5
格式
{
"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": "http://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": "http://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": "http://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": "http://sf3-ttcdn-tos.pstatp.com/img/edux-data/16270092820316c732f80fe~0x0.png"
}
],
"index": [
0,
9,
1
]
}
],
"index": [
0,
9
]
}
],
"scene": "ER_L1"
}
三、Figma 外掛深入體會
組成元素
如上圖所示,整個外掛分為兩個部分,左邊成為沙箱執行緒,之後就叫做主執行緒,右邊為 iframe
執行緒,之後就叫 UI 執行緒
整個外掛的入口是主執行緒,主執行緒採用一種沙盒結構,其中存在類似 node
環境,可以執行 js 程式碼,訪問 ES6
的 API,可以去 操控
figma
裡面的內容
。
由主執行緒建立建立 iframe
,並將我們寫的 html
插入,由主執行緒控制整個 iframe
的大小,在 html
中可以使用瀏覽器的 API,比如說向後端發起請求,可以 和使用者進行交流
。
UI 執行緒與主執行緒通過 postMessage 相互通訊,所以這裡可以有個流程,主執行緒先執行,主執行緒設定接收UI執行緒資訊的方法,建立 UI,UI 設定接收資訊方法。注意:兩者通訊內容有限制,除了常見的JSON資料型別外,還有 blob,arraybuffer,像在 figma 中的一些物件都不能直接傳輸
這兩個部分分別就是我們需要提供的兩份檔案
Figma 外掛能做什麼
-
讀取本地檔案中的圖層和圖層屬性
外掛能暴露檔案的內容,這個內容指的是我們在 figma 外掛中看到的,比如某個元素的尺寸,位置,層次結構,顏色,文字內容等,我們不僅能獲取到,還能更改。
-
設計外掛的 UI(
iframe
) -
可以訪問一些瀏覽器 API (有些 API 例外,例如
indexedDB
)
有網路請求;開啟檔案;使用 canvas
, webgl(pixi)
;使用 WebAssembly
,使用音訊 API;
四、從 0 到釋出,走一遍流程
-
建立新外掛
開啟建立外掛的視窗
建立 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 中重啟外掛即可應用新的外掛內容。
-
建立 Main 和 ui 對應的檔案
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 ,接下來就是把我們寫的 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 內容
-
釋出外掛
找到外掛管理
釋出外掛
五、介紹 lego-quiz-figma 細節
主執行緒檔名為 code
Code 相關細節
-
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();
}
};
}
-
Figma 儲存和獲取資訊,用於儲存當前選中的場景,下次開啟時使用,這裡的 api 就需要使用到 id,沒有提供會報錯(不能使用
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 時,由於要記錄層級關係,因此要轉化為相對於父節點的座標,而非全域性座標
UI 相關細節
-
UI 接收 code 訊息
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
});
}
});
-
UI 向 code 傳送訊息
const sendMessageToCore = (message: any, type: UIToCodeType) => {
// pluginMessage, * 是不可變的
parent.postMessage({ pluginMessage: { message, type } }, '*');
};
-
業務場景區別
三個場景 x 軸正方向都是 向右:point_right:
通用:COMMON_DEV,錨點在 (0.5,0.5),y 軸正方向 向下:point_down:
ER L1:ER_L1,錨點在(0,0),y 軸正方向 向下:point_down:
ER 課後練習:ER_GAME,錨點在 (0.5,0.5),y 軸正方向 向上:point_up_2:
六、體會與交流
我的體會
-
Figma 外掛能做一些我們手動做的事,因此有些規律性工作可以使用 figma 外掛
-
我們不能總是向 UI 設計師索取內容,假如看到一些好的元素,好好利用 figma 外掛匯入一些資源,給 UI 一些驚喜也是好的
-
我做的功能是匯出自定義格式資料,可擴充套件性不高,後續可以調研如果匯出一種通用的格式
-
Figma 有些比較有趣的內容,比如可以直接匯出 Uint8Array 格式的圖片,可以細膩控制每一個元素
-
可以多多交流 製作外掛的體會
Figma 對外掛的展望
-
更多的訪問檔案、使用者、團隊資訊、評論
-
完全訪問團隊庫
-
在事件上觸發外掛程式碼,挑戰在於:效能下降,內部穩定性,外部穩定性
-
長時間允許外掛
-
屬性面板/工具欄中設定外掛
-
外掛的鍵盤快捷鍵
-
檔案瀏覽器中的外掛
-
訪問版本歷史記錄
-
輔助函式 API
-
外掛的 Figma UI 元件,能夠倒入 Figma 中的元素
-
外掛分析錯誤報告
不會做
-
桌面特定的API
-
載入外部字型
參考資料
figma 外掛案例: http://github.com/figma/plugin-samples/tree/master/webpack
- 為什麼說 WebAssembly 是 Web 的未來?
- 淺析TypeScript Compiler 原理
- 使用 Nginx 作為你的開發代理工具
- 動態列表元件 - 拖拽排序功能設計與實現
- Google V8引擎淺析-面向物件
- 淺談WebRTC技術原理與應用
- webpack模組熱更新原理
- Figma外掛開發
- 軟連結&硬連結在前端中的應用
- 登入態 & SSO
- Svelte 原理淺析與評測
- Lambda 演算基礎
- 前端模組依賴關係分析與應用
- TypeScript型別中的逆變協變
- Twitter和微博都在用的 @ 人的功能是如何設計與實現的?
- webpack loader 與plugin 開發實戰 —— 點選 vue 頁面元素跳轉到對應的 vscode 程式碼
- 從零實現並擴充套件可自由繪製的畫板
- 詳細瞭解前端模組化
- 從 ESLint 開啟專案格式化
- React 18 新特性之 startTransition