Heim > Artikel > Web-Frontend > Zeichnen Sie einen Prozess der Entwicklung eines Fimga-Plug-Ins mit Vue 3 auf
Wie verwende ich Vue 3, um das Figma-Plug-in zu entwickeln? Der folgende Artikel führt Sie in das Prinzip des Figma-Plug-Ins ein, zeichnet den Prozess der Entwicklung des Fimga-Plug-Ins mit Vue 3 auf und hängt den sofort einsatzbereiten Code an.
Figma ist heutzutage ein beliebtes Designtool, und immer mehr Designteams beginnen, von Sketch auf Figma umzusteigen. Das größte Merkmal von Figma ist, dass es mithilfe von Web-Technologie entwickelt wurde und vollständig plattformübergreifend ist. Figma-Plugins werden auch mit Web-Technologie entwickelt. Solange Sie html
, js
und css
kennen, können Sie ein Figma-Plug-in schreiben -In. Figma-Plug-in-Prinziphtml
、 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
Leinwand
, gerendert mit 🎜WebGL🎜. Und der Canvas-Engine-Teil verwendet 🎜WebAssembly🎜, weshalb Figma so flüssig sein kann. Die Desktop-Figma-App verwendet 🎜Electron🎜🎜 – eine Web-Technologie. Ein Framework für Entwicklung von Desktop-Anwendungen. Electron ähnelt einem Browser, führt jedoch tatsächlich intern eine Webanwendung aus. 🎜iframe
zweifellos das Richtige direkteste Lösung. iframe
ist eine Standard-W3C-Spezifikation, die seit vielen Jahren in Browsern verwendet wird. Seine Eigenschaften sind: 🎜postMessage(STRING)
erfolgen, die Kommunikationseffizienz ist sehr gering. Wenn Sie ein Canvas-Element im Plug-In bedienen möchten, müssen Sie zunächst die Knoteninformationen des Elements vom Hauptrahmen nach iframe
kopieren und dann die Knoteninformationen nach Abschluss des Vorgangs in iframe Hauptrahmen. Dies erfordert viel Kommunikation, und bei komplexen Designentwürfen sind die Knoteninformationen sehr umfangreich, was die Kommunikationsgrenze überschreiten kann. 🎜🎜Um sicherzustellen, dass die Leinwand bedient werden kann, müssen Sie zum Hauptthread zurückkehren. Das Hauptproblem bei Plug-Ins, die im Hauptthread ausgeführt werden, ist die Sicherheit. Daher haben die Figma-Entwickler eine js
-Sandbox-Umgebung im Hauptthread implementiert und dabei Realm API🎜. In der Sandbox können nur reiner JS-Code und die von Figma bereitgestellte API ausgeführt werden. Auf Browser-APIs (wie Netzwerk, Speicher usw.) kann nicht zugegriffen werden, wodurch die Sicherheit gewährleistet ist. 🎜🎜🎜🎜 Gefühl Interessierten Studenten wird empfohlen, den Artikel des offiziellen Teams zu lesen "Wie man ein Plugin-System im Web aufbaut und auch nachts gut schläft"🎜, das den Auswahlprozess der Figma-Plug-in-Lösung im Detail vorstellt, und ich Ich profitiere sehr davon, es zu lesen. 🎜🎜Nach umfassender Überlegung unterteilt Figma das Plug-in in zwei Teile: Die Plug-in-Benutzeroberfläche wird in iframe
ausgeführt, und der Code, der die Leinwand betreibt, wird in der Isolationssandbox des Hauptthreads ausgeführt. Der UI-Thread und der Haupt-Thread kommunizieren über postMessage
. 🎜🎜Konfigurieren Sie das Feld main
in der Plug-in-Konfigurationsdatei manifest.json
so, dass es auf die in den Hauptthread geladene Datei js
verweist ui
Feldkonfigurationen werden in html
-Dateien innerhalb von iframe
geladen. Beim Öffnen des Plug-Ins ruft der Hauptthread die Methode figma.showUI()
auf, um den iframe
zu laden. 🎜🎜🎜Schreiben Sie das einfachste Figma-Plug-in🎜🎜🎜Um den laufenden Prozess des Plug-ins zu verstehen, schreiben wir zunächst das einfachste Figma-Plug-in -In. Die Funktion ist einfach: Sie können quadratische Farbblöcke hinzufügen oder entfernen. 🎜manifest.json
-Datei im Stammverzeichnis. Der Inhalt ist wie folgt: 🎜{ "name": "simple-demo", "api": "1.0.0", "main": "main.js", "ui": "index.html", "editorType": [ "figjam", "figma" ] }
Erstellen Sie ein neues Stammverzeichnis index.html
, 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
来开发插件。(学习视频分享:vuejs教程)
关于 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" ] }
main.js
, der Inhalt lautet wie folgt:🎜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', }, }, }, })
Plugins
-> Entwicklung
-> aus Der zuvor erstellte Dateipfad manifest.json führt zum erfolgreichen Import des Plugins.
Klicken Sie dann mit der rechten Maustaste auf Plugins
-> simple-demo
(Plugin-Name), um das Plug-in zu öffnen. 🎜🎜🎜🎜Test Klicken Sie auf die Schaltfläche und es funktioniert normal. Es ist nur so, dass die Farbblöcke noch nicht auf der Seite erscheinen (keine Sorge).
Die Debugging-Konsole kann über Plugins
-> Entwicklung
-> geöffnet werden. Sie können das von uns gedruckte Protokoll sehen. 🎜main.js
. main.js
macht das Objekt der obersten Ebene figma
verfügbar, das eine Reihe von APIs kapselt, die zum Betrieb der Leinwand verwendet werden. Weitere Informationen finden Sie unter Offizielle Website-Dokumentation. Wir verwenden figma.createRectangle()
, um ein Rechteck zu erstellen. Der Hauptthread muss über figma.ui.onmessage
auf Ereignisse vom UI-Thread warten, um zu antworten. Der geänderte main.js
-Code lautet wie folgt: 🎜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, }, })🎜Gleichzeitig sollten einige Codes in
index.html
geändert und über parent.postMessage
Thread-Sendeereignis: 🎜npm run watch # 同时要在另一个终端里启动静态文件服务 npm run preview🎜 Starten Sie das Plug-in neu, versuchen Sie es erneut und stellen Sie fest, dass Sie Farbblöcke erfolgreich hinzufügen und entfernen können. 🎜🎜🎜🎜🎜 Verwenden Sie Vue 3, um das Figma-Plug-in zu entwickeln🎜🎜🎜Durch die vorherigen Beispiele haben wir das Funktionsprinzip des Figma-Plug-ins bereits verstanden. Es ist jedoch sehr ineffizient, Code mit diesen „nativen“
js
und html
zu schreiben. Wir können die neueste Web-Technologie zum Schreiben von Code verwenden, solange das verpackte Produkt eine js
-Datei enthält, die im Hauptframe ausgeführt wird, und eine html
-Datei, die für den iframe ausgeführt wird
code>-Datei. Ich habe beschlossen, Vue 3
zum Entwickeln von Plugins zu verwenden. (Teilen von Lernvideos: vuejs Tutorial) 🎜🎜Über Vue 3 Ich werde es nicht vorstellen Jeder, der es versteht, versteht es. Wenn Sie es nicht verstehen, können Sie es zuerst studieren, bevor Sie zurückkommen. Der Fokus liegt hier nicht darauf, welches Framework verwendet werden soll (Wechsel zu Vue 2, der Reaktionsprozess ist ähnlich), sondern auf den Konstruktionstools. 🎜Vue
+ TypeScript
erstellen. 🎜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); }🎜Dann öffnen Sie
http://localhost:3000
über den Browser, um die Seite anzuzeigen. 🎜src/App.vue
Die Codeänderung ist wie folgt: 🎜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();🎜Wir speichern den in der Haupt-Thread-Sandbox ausgeführten JS-Code im Verzeichnis
src/worker
. Erstellen Sie eine neue src/worker/code.ts
mit folgendem Inhalt: 🎜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();🎜Dem obigen Code fehlt die ts-Typdeklaration von
figma
, daher müssen wir ihn installieren. 🎜🎜npm i -D @figma/plugin-typings
🎜🎜Ändern Sie tsconfig.json
und fügen Sie typeRoots
hinzu, damit der ts-Code nicht gemeldet wird Fehler. Fügen Sie außerdem "skipLibCheck": true
hinzu, um Typdeklarationskonflikte zu lösen. 🎜rrreeemanifest. json
Datei als Plug-in-Konfiguration🎜index.html
als UI-Code🎜code.js code> als Haupt-Thread-JS-Code 🎜
public Das Verzeichnis
ist für das Build-Produktverzeichnis dist
verantwortlich. 🎜{ "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
作者:大料园
(学习视频分享:web前端开发)
Das obige ist der detaillierte Inhalt vonZeichnen Sie einen Prozess der Entwicklung eines Fimga-Plug-Ins mit Vue 3 auf. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!