記錄一個使用Vue 3開發Fimga外掛的過程

2022-04-11 13:00:22
如何用 3 開發 Figma 外掛?下面本篇文章給大家介紹一下Figma外掛原理,記錄下使用Vue 3開發Fimga外掛的過程,並附有開箱即用的程式碼,希望對大家有所幫助!

用 Vue 3 開發 Figma 外掛

Figma 是一款當下流行的設計工具,越來越多的設計團隊開始從 Sketch 轉向 Figma。Figma 最大的特點是使用Web技術開發,實現了完全的跨平臺。 Figma 外掛也是使用 Web 技術開發,只要會 htmljscss 就能動手寫一個 Figma 外掛。

Figma 外掛原理

Figma 架構簡介

介紹 Fimga 外掛之前,我們先來了解一下 Fimga 的技術架構。

Figma 整體是用 React 開發的,核心的畫布區是一塊 canvas ,使用WebGL來渲染。並且畫布引擎部分使用的是WebAssembly,這就是 Figma 能夠如此流暢的原因。桌面端的Figma App 使用了 Electron——一個使用Web技術開發桌面應用的框架。Electron 類似於一個瀏覽器,內部執行的其實還是一個Web應用。

Figma 外掛原理

在Web端開發一套安全、可靠的外掛系統, iframe 無疑是最直接的方案。 iframe 是標準的W3C規範,在瀏覽器上已經經過多年應用,它的特點是:

  • 安全,天然沙箱隔離環境,iframe內頁面無法操作主框架;

  • 可靠,相容性非常好,且經過了多年市場的檢驗;

但是它也有明顯的缺點:與主框架通訊只能通過 postMessage(STRING) 的方式,通訊效率非常低。如果要在外掛裡操作一個畫布元素,首先要將元素的節點資訊從主框架拷貝到 iframe 中,然後在 iframe 中操作完再更新節點資訊給主框架。這涉及到大量通訊,而且對於複雜的設計稿節點資訊是非常巨大的,可能超過通訊的限制。

為了保證操作畫布的能力,必須回到主執行緒。外掛在主執行緒執行的問題主要在於安全性上,於是Figma的開發人員在主執行緒實現了一個 js 沙箱環境,使用了Realm API。沙箱中只能執行純粹的 js 程式碼和Figma提供的API,無法存取瀏覽器API(例如網路、儲存等),這樣就保證了安全性。

1.png

感興趣的同學推薦閱讀官方團隊寫的《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桌面端

首先要下載並安裝好 Figma 桌面端。

編寫外掛的啟動檔案 manifest.json

新建一個程式碼工程,在根目錄中新建 manifest.json 檔案,內容如下:

{
  "name": "simple-demo",
  "api": "1.0.0",
  "main": "main.js",
  "ui": "index.html",
  "editorType": [
    "figjam",
    "figma"
  ]
}

編寫UI程式碼

根目錄新建 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 程式碼

根目錄新建 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 (外掛名),就可以開啟外掛。

2.png

測試點選按鈕,功能正常。只不過頁面上還未出現色塊(彆著急)。 通過 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' } }, '*')
}

重新啟動外掛,再試驗一下,發現已經可以成功加減色塊了。

3.png

使用 Vue 3 開發 Figma 外掛

通過前面的例子,我們已經清楚 Figma 外掛的執行原理。但是用這種「原生」的 jshtml 來編寫程式碼非常低效的。我們完全可以用最新的Web技術來編寫程式碼,只要打包產物包括一個執行在主框架的 js 檔案和一個給 iframe 執行的 html 檔案即可。我決定嘗試使用 Vue 3 來開發外掛。(學習視訊分享:)

關於 Vue 3 就不多做介紹了,懂的都懂,不懂的看到這裡可以先去學習一下再來。這裡的重點不在於用什麼框架(改成用vue 2、react過程也差不多),而在於構建工具。

Vite 啟動一個新專案

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程式碼

我們把前面的外掛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 目錄中新增 manifest.json 檔案

public 目錄中的檔案都會負責到構建產物 dist 目錄下。

{
  "name": "figma-plugin-vue3",
  "api": "1.0.0",
  "main": "code.js",
  "ui": "index.html",
  "editorType": [
    "figjam",
    "figma"
  ]
}

vite.config.ts 中增加構建入口

預設情況下 vite 會用 index.html 作為構建入口,裡面用到的資源會被打包構建。我們還需要一個入口,用來構建主執行緒 js 程式碼。

執行 npm i -D @types/node ,安裝 Node.js 的型別宣告,以便在 ts 中使用 Node.js API。 vite.config.tsbuild.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 builddist 目錄會有構建產物。然後我們按照前面的步驟,將 dist 目錄新增為 Figma 外掛。 Plugins -> Development -> Import plugin from manifest... ,選擇 dist/manifest.json 檔案路徑。

啟動外掛......怎麼外掛裡一片空白?好在 Figma 裡面有 devtools 偵錯工具,我們開啟瞧一瞧。

4.png

可以看到,我們的 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 外掛看看。果然,外掛已經正常了!

5.png

Figma 載入外掛只需要 index.htmlcode.js ,其他資源都可以通過網路載入。這意味著我們可以將 js、css 資源放在伺服器端,實現外掛的熱更?不知道釋出外掛的時候會不會有限制,這個我還沒試過。

開發模式

我們已經能成功通過 Vue 3 來構建 Figma 外掛了,但是我不想每次修改程式碼都要構建一遍,我們需要能夠自動構建程式碼的開發模式。

vite 自動的 dev 模式是啟動了一個服務,沒有構建產物(而且沒有類似webpack裡面的 writeToDisk 設定),所以無法使用。

watch 模式

vite 的 build 命令有watch模式,可以監聽檔案改動然後自動執行 build 。我們只需要修改 package.jsonscripts 裡新增 "watch": "vite build --watch"

npm run watch

# 同時要在另一個終端裡啟動靜態檔案服務
npm run preview

這種方式雖然修改程式碼後會自動編譯,但是每次還是要關閉外掛並重新開啟才能看到更新。這樣寫UI還是太低效了,能不能在外掛裡實現 HMR (模組熱過載)功能呢?

dev 模式

vite dev 的問題在於沒有構建產物。 code.js 是執行在 Fimga 主執行緒沙箱中的,這部分是無法熱過載的,所以可以利用 vite build --watch 實現來編譯。需要熱過載的是 index.html 以及相應的 js 、css 資源。 先來看一下 npm run dev 模式下的 html 資源有什麼內容:

6.png

理論上來說,我們只需要把這個 html 手動寫入到 dist 目錄就行,熱過載的時候 html 檔案不需要修改。直接寫入的話會遇到資源是相對路徑的問題,所以要麼把資源路徑都加上域名( http://localhost:3000 ),或者使用 <base>標籤。

手動生成 html 檔案

對比上面的 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 程式碼,發現外掛內容自動更新了!

7.png

最後在 package.json 中新建一個修改一下dev的內容為 "dev": "node scripts/dev.js" 就可以了。

通過請求來獲取 HTML

前面通過自己生產 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其它相關文章!