Figma 是一款當下流行的設計工具,越來越多的設計團隊開始從 Sketch 轉向 Figma。Figma 最大的特點是使用Web技術開發,實現了完全的跨平臺。 Figma 外掛也是使用 Web 技術開發,只要會 html
、 js
、 css
就能動手寫一個 Figma 外掛。
Figma 外掛原理
介紹 Fimga 外掛之前,我們先來了解一下 Fimga 的技術架構。
Figma 整體是用 React 開發的,核心的畫布區是一塊 canvas
,使用WebGL來渲染。並且畫布引擎部分使用的是WebAssembly,這就是 Figma 能夠如此流暢的原因。桌面端的Figma App 使用了 Electron——一個使用Web技術開發桌面應用的框架。Electron 類似於一個瀏覽器,內部執行的其實還是一個Web應用。
在Web端開發一套安全、可靠的外掛系統, iframe
無疑是最直接的方案。 iframe
是標準的W3C規範,在瀏覽器上已經經過多年應用,它的特點是:
安全,天然沙箱隔離環境,iframe內頁面無法操作主框架;
可靠,相容性非常好,且經過了多年市場的檢驗;
但是它也有明顯的缺點:與主框架通訊只能通過 postMessage(STRING)
的方式,通訊效率非常低。如果要在外掛裡操作一個畫布元素,首先要將元素的節點資訊從主框架拷貝到 iframe
中,然後在 iframe
中操作完再更新節點資訊給主框架。這涉及到大量通訊,而且對於複雜的設計稿節點資訊是非常巨大的,可能超過通訊的限制。
為了保證操作畫布的能力,必須回到主執行緒。外掛在主執行緒執行的問題主要在於安全性上,於是Figma的開發人員在主執行緒實現了一個 js
沙箱環境,使用了Realm API。沙箱中只能執行純粹的 js 程式碼和Figma提供的API,無法存取瀏覽器API(例如網路、儲存等),這樣就保證了安全性。
感興趣的同學推薦閱讀官方團隊寫的《How to build a plugin system on the web and also sleep well at night》,詳細介紹了 Figma 外掛方案的選擇過程,讀來獲益良多。
經過綜合考慮,Figma 將外掛分成兩個部分:外掛UI執行在 iframe
中,操作畫布的程式碼執行在主執行緒的隔離沙箱中。UI執行緒和主執行緒通過 postMessage
通訊。
外掛組態檔 manifest.json
中分別設定 main
欄位指向載入到主執行緒的 js
檔案, ui
欄位設定載入到 iframe
中的 html
檔案。開啟外掛時,主執行緒呼叫 figma.showUI()
方法載入 iframe
。
寫一個最簡單的 Figma 外掛
為了瞭解外掛的執行過程,我們先寫一個最簡單的 Figma 外掛。功能很簡單:可以加減正方形色塊。
首先要下載並安裝好 Figma 桌面端。
新建一個程式碼工程,在根目錄中新建 manifest.json
檔案,內容如下:
{ "name": "simple-demo", "api": "1.0.0", "main": "main.js", "ui": "index.html", "editorType": [ "figjam", "figma" ] }
根目錄新建 index.html
,
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Demo</title> <style> h1 { text-align: center; } p { color: red; } .buttons { margin-top: 20px; text-align: center; } .buttons button { width: 40px; } #block-num { font-size: 20px; } </style> </head> <body> <h1>Figma 外掛 Demo</h1> <p>當前色塊數量:<span id="block-num">0</span></p> <div> <button id="btn-add" onclick="addBlock()">+</button> <button id="btn-sub" onclick="subBlock()">-</button> </div> <script> console.log('ui code runs!'); var blockNumEle = document.getElementById('block-num'); function addBlock() { console.log('add'); var num = +blockNumEle.innerText; num += 1; blockNumEle.innerText = num; } function subBlock() { console.log('substract'); var num = +blockNumEle.innerText; if (num === 0) return; num -= 1; blockNumEle.innerText = num; } </script> </body> </html>
根目錄新建 main.js
,內容如下:
console.log('from code 2'); figma.showUI(__html__, { width: 400, height: 400, });
Figma桌面APP,畫布任意地方右鍵開啟選單, Plugins
-> Development
-> Import plugin from manifest...
,選擇前面建立的 manifest.json
檔案路徑,即可成功匯入外掛。
然後通過右鍵, Plugins
-> Development
-> simple-demo
(外掛名),就可以開啟外掛。
測試點選按鈕,功能正常。只不過頁面上還未出現色塊(彆著急)。
通過 Plugins
-> Development
-> Open console
可以開啟偵錯控制檯。可以看到我們列印的紀錄檔。
前面講了,畫布程式碼是執行在主執行緒的,為了執行效率,外掛要操作畫布內容也只能在主執行緒執行,即在 main.js
中。 main.js
中暴露了頂級物件 figma
,封裝了用來操作畫布的一系列API,具體可以去看官網檔案。我們用 figma.createRectangle()
來建立一個矩形。主執行緒需要通過 figma.ui.onmessage
監聽來自UI執行緒的事件,從而做出響應。修改後的 main.js
程式碼如下:
console.log('figma plugin code runs!') figma.showUI(__html__, { width: 400, height: 400, }); 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
中的部分程式碼,通過 parent.postMessage
給主執行緒傳送事件:
function addBlock() { console.log('add'); var num = +blockNumEle.innerText; num += 1; blockNumEle.innerText = num; parent.postMessage({ pluginMessage: { type: 'add-block' } }, '*') } function subBlock() { console.log('substract'); var num = +blockNumEle.innerText; if (num === 0) return; num -= 1; blockNumEle.innerText = num; parent.postMessage({ pluginMessage: { type: 'sub-block' } }, '*') }
重新啟動外掛,再試驗一下,發現已經可以成功加減色塊了。
使用 Vue 3 開發 Figma 外掛
通過前面的例子,我們已經清楚 Figma 外掛的執行原理。但是用這種「原生」的 js
、 html
來編寫程式碼非常低效的。我們完全可以用最新的Web技術來編寫程式碼,只要打包產物包括一個執行在主框架的 js
檔案和一個給 iframe
執行的 html
檔案即可。我決定嘗試使用 Vue 3
來開發外掛。(學習視訊分享:)
關於 Vue 3 就不多做介紹了,懂的都懂,不懂的看到這裡可以先去學習一下再來。這裡的重點不在於用什麼框架(改成用vue 2、react過程也差不多),而在於構建工具。
Vite 是Vue的作者開發的新一代構建工具,也是 Vue 3推薦的構建工具。
我們先建一個 Vue
+ TypeScript
的模板專案。
npm init vite@latest figma-plugin-vue3 --template vue-ts cd figma-plugin-vue3 npm install npm run dev
然後通過瀏覽器開啟 http://localhost:3000
就能看到頁面。
我們把前面的外掛demo移植到 Vue 3 中。 src/App.vue
程式碼修改如下:
<script setup> import { ref } from 'vue'; const num = ref(0); console.log('ui code runs!'); function addBlock() { console.log('add'); num.value += 1; parent.postMessage({ pluginMessage: { type: 'add-block' } }, '*') } function subBlock() { console.log('substract'); if (num .value=== 0) return; num.value -= 1; parent.postMessage({ pluginMessage: { type: 'sub-block' } }, '*') } </script> <template> <h1>Figma 外掛 Demo</h1> <p>當前色塊數量:<span id="block-num">{{ num }}</span></p> <div> <button id="btn-add" @click="addBlock">+</button> <button id="btn-sub" @click="subBlock">-</button> </div> </template> <style scoped> h1 { text-align: center; } p { color: red; } .buttons { margin-top: 20px; text-align: center; } .buttons button { width: 40px; } #block-num { font-size: 20px; } </style>
我們在 src/worker
目錄存放執行在主執行緒沙箱中的js程式碼。新建 src/worker/code.ts
,內容如下:
console.log('figma plugin code runs!') figma.showUI(__html__, { width: 400, height: 400, }); const nodes: RectangleNode[] = []; 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); };
上述程式碼中缺少 figma
的 ts 型別宣告,所以我們需要安裝一下。
npm i -D @figma/plugin-typings
修改 tsconfig.json
,新增 typeRoots
,這樣 ts 程式碼就不會報錯了。同時也要加上 "skipLibCheck": true
,解決型別宣告衝突問題。
{ "compilerOptions": { // ... "skipLibCheck": true, "typeRoots": [ "./node_modules/@types", "./node_modules/@figma" ] }, }
Figma 外掛需要的構建產物有:
manifest.json
檔案作為外掛設定
index.html
作為UI程式碼
code.js
作為主執行緒js程式碼
public
目錄中的檔案都會負責到構建產物 dist
目錄下。
{ "name": "figma-plugin-vue3", "api": "1.0.0", "main": "code.js", "ui": "index.html", "editorType": [ "figjam", "figma" ] }
預設情況下 vite
會用 index.html
作為構建入口,裡面用到的資源會被打包構建。我們還需要一個入口,用來構建主執行緒 js 程式碼。
執行 npm i -D @types/node
,安裝 Node.js
的型別宣告,以便在 ts 中使用 Node.js
API。 vite.config.ts
的 build.rollupOptions
中增加 input
。預設情況下輸出產物會帶上檔案 hash
,所以也要修改 output
設定:
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], build: { sourcemap: 'inline', rollupOptions: { input:{ main: resolve(__dirname, 'index.html'), code: resolve(__dirname, 'src/worker/code.ts'), }, output: { entryFileNames: '[name].js', }, }, }, })
執行 npm run build
, dist
目錄會有構建產物。然後我們按照前面的步驟,將 dist
目錄新增為 Figma 外掛。 Plugins
-> Development
-> Import plugin from manifest...
,選擇 dist/manifest.json
檔案路徑。
啟動外掛......怎麼外掛裡一片空白?好在 Figma 裡面有 devtools 偵錯工具,我們開啟瞧一瞧。
可以看到,我們的 index.html
已經成功載入,但是 js 程式碼沒載入所以頁面空白。js、css 等資源是通過相對路徑參照的,而我們的 iframe
中的 src
是一個 base64
格式內容,在尋找 js 資源的時候因為沒有域名,所以找不到資源。
解決辦法也很簡單,我們給資源加上域名,然後本地起一個靜態資源伺服器就行了。修改 vite.config.ts
,加上 base: 'http://127.0.0.1:3000'
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], base: 'http://127.0.0.1:3000', build: { sourcemap: 'inline', rollupOptions: { input: { main: resolve(__dirname, 'index.html'), code: resolve(__dirname, 'src/worker/code.ts'), }, output: { entryFileNames: '[name].js', }, }, }, preview: { port: 3000, }, })
重新構建程式碼 npm run build
。然後啟動靜態資源伺服器 npm run preview
。通過瀏覽器存取 http://localhost:3000/
可以看到內容。
然後重新開啟 Figma 外掛看看。果然,外掛已經正常了!
Figma 載入外掛只需要
index.html
和code.js
,其他資源都可以通過網路載入。這意味著我們可以將 js、css 資源放在伺服器端,實現外掛的熱更?不知道釋出外掛的時候會不會有限制,這個我還沒試過。
我們已經能成功通過 Vue 3 來構建 Figma 外掛了,但是我不想每次修改程式碼都要構建一遍,我們需要能夠自動構建程式碼的開發模式。
vite 自動的 dev 模式是啟動了一個服務,沒有構建產物(而且沒有類似webpack裡面的 writeToDisk
設定),所以無法使用。
vite
的 build 命令有watch模式,可以監聽檔案改動然後自動執行 build
。我們只需要修改 package.json
, scripts
裡新增 "watch": "vite build --watch"
。
npm run watch # 同時要在另一個終端裡啟動靜態檔案服務 npm run preview
這種方式雖然修改程式碼後會自動編譯,但是每次還是要關閉外掛並重新開啟才能看到更新。這樣寫UI還是太低效了,能不能在外掛裡實現 HMR
(模組熱過載)功能呢?
vite dev 的問題在於沒有構建產物。 code.js
是執行在 Fimga 主執行緒沙箱中的,這部分是無法熱過載的,所以可以利用 vite build --watch
實現來編譯。需要熱過載的是 index.html
以及相應的 js 、css 資源。
先來看一下 npm run dev
模式下的 html 資源有什麼內容:
理論上來說,我們只需要把這個 html 手動寫入到 dist
目錄就行,熱過載的時候 html 檔案不需要修改。直接寫入的話會遇到資源是相對路徑的問題,所以要麼把資源路徑都加上域名( http://localhost:3000
),或者使用 <base>標籤。
對比上面的 html 程式碼和根目錄的 index.html
檔案,發現只是增加了一個 <script type="module" src="/@vite/client"></script>
。所以我們可以自己解析 index.html
,然後插入相應這個標籤,以及一個 <base>
標籤。解析 HTML 我們用 jsdom
。
const JSDOM = require('jsdom'); const fs = require('fs'); // 生成 html 檔案 function genIndexHtml(sourceHTMLPath, targetHTMLPath) { const htmlContent = fs.readFileSync(sourceHTMLPath, 'utf-8'); const dom = new JSDOM(htmlContent); const { document } = dom.window; const script = document.createElement('script'); script.setAttribute('type', 'module'); script.setAttribute('src', '/@vite/client'); dom.window.document.head.insertBefore(script, document.head.firstChild); const base = document.createElement('base'); base.setAttribute('href', 'http://127.0.0.1:3000/'); dom.window.document.head.insertBefore(base, document.head.firstChild); const result = dom.serialize(); fs.writeFileSync(targetHTMLPath, result); }
同時 vite 提供了 JavaScript API,所以我們可以程式碼組織起來,寫一個 js 指令碼來啟動開發模式。新建檔案 scripts/dev.js
,完整內容如下:
const { JSDOM } = require('jsdom'); const fs = require('fs'); const path = require('path'); const vite = require('vite'); const rootDir = path.resolve(__dirname, '../'); function dev() { const htmlPath = path.resolve(rootDir, 'index.html'); const targetHTMLPath = path.resolve(rootDir, 'dist/index.html'); genIndexHtml(htmlPath, targetHTMLPath); buildMainCode(); startDevServer(); } // 生成 html 檔案 function genIndexHtml(sourceHTMLPath, targetHTMLPath) { const htmlContent = fs.readFileSync(sourceHTMLPath, 'utf-8'); const dom = new JSDOM(htmlContent); const { document } = dom.window; const script = document.createElement('script'); script.setAttribute('type', 'module'); script.setAttribute('src', '/@vite/client'); dom.window.document.head.insertBefore(script, document.head.firstChild); const base = document.createElement('base'); base.setAttribute('href', 'http://127.0.0.1:3000/'); dom.window.document.head.insertBefore(base, document.head.firstChild); const result = dom.serialize(); fs.writeFileSync(targetHTMLPath, result); } // 構建 code.js 入口 async function buildMainCode() { const config = vite.defineConfig({ configFile: false, // 關閉預設使用的組態檔 build: { emptyOutDir: false, // 不要清空 dist 目錄 lib: { // 使用庫模式構建 entry: path.resolve(rootDir, 'src/worker/code.ts'), name: 'code', formats: ['es'], fileName: (format) => `code.js`, }, sourcemap: 'inline', watch: {}, }, }); return vite.build(config); } // 開啟 devServer async function startDevServer() { const config = vite.defineConfig({ configFile: path.resolve(rootDir, 'vite.config.ts'), root: rootDir, server: { hmr: { host: '127.0.0.1', // 必須加上這個,否則 HMR 會報錯 }, port: 3000, }, build: { emptyOutDir: false, // 不要清空 dist 目錄 watch: {}, // 使用 watch 模式 } }); const server = await vite.createServer(config); await server.listen() server.printUrls() } dev();
執行 node scripts/dev.js
,然後在 Figma 中重新啟動外掛。試試修改一下 Vue 程式碼,發現外掛內容自動更新了!
最後在 package.json
中新建一個修改一下dev的內容為 "dev": "node scripts/dev.js"
就可以了。
前面通過自己生產 index.html
的方式有很大的弊端:萬一後續 vite 更新後修改了預設 html 的內容,那我們的指令碼也要跟著修改。有沒有更健壯的方式呢?我想到可以通過請求 devServer
來獲取 html 內容,然後寫入本地。話不多說,修改後程式碼如下:
const { JSDOM } = require('jsdom'); const fs = require('fs'); const path = require('path'); const vite = require('vite'); const axios = require('axios'); const rootDir = path.resolve(__dirname, '../'); async function dev() { // const htmlPath = path.resolve(rootDir, 'index.html'); const targetHTMLPath = path.resolve(rootDir, 'dist/index.html'); await buildMainCode(); await startDevServer(); // 必須放到 startDevServer 後面執行 await genIndexHtml(targetHTMLPath); } // 生成 html 檔案 async function genIndexHtml(/* sourceHTMLPath,*/ targetHTMLPath) { const htmlContent = await getHTMLfromDevServer(); const dom = new JSDOM(htmlContent); // ... const result = dom.serialize(); fs.writeFileSync(targetHTMLPath, result); } // ... // 通過請求 devServer 獲取HTML async function getHTMLfromDevServer () { const rsp = await axios.get('http://localhost:3000/index.html'); return rsp.data; } dev();
Figma 基於Web平臺的特性使之能成為真正跨平臺的設計工具,只要有瀏覽器就能使用。同時也使得開發外掛變得非常簡單,非專業人士經過簡單的學習也可以上手開發一個外掛。而Web社群有數量龐大的開發者,相信 Figma 的外掛市場也會越來越繁榮。
本文通過一個例子,詳細講述了使用 Vue 3 開發 Figma 外掛的過程,並且完美解決了開發模式下熱過載的問題。我將模板程式碼提交到了 Git 倉庫中,需要的同學可以直接下載使用:figma-plugin-vue3。
開發 Figma 外掛還會遇到一些其他問題,例如如何進行網路請求、本地儲存等,有空再繼續分享我的實踐心得。
本文轉載自:https://juejin.cn/post/7084639146915921956
作者:大料園
(學習視訊分享:)
以上就是記錄一個使用Vue 3開發Fimga外掛的過程的詳細內容,更多請關注TW511.COM其它相關文章!