Figma 플러그인을 개발하기 위해 Vue 3를 사용하는 방법은 무엇입니까? 다음 글에서는 Figma 플러그인의 원리를 소개하고, Vue 3를 사용하여 Fimga 플러그인을 개발하는 과정을 기록하고, Out-of-box 코드를 첨부해 보도록 하겠습니다.
Figma는 요즘 인기 있는 디자인 도구이며 점점 더 많은 디자인 팀이 Sketch에서 Figma로 전환하기 시작했습니다. Figma의 가장 큰 특징은 웹 기술을 사용하여 개발되었으며 완전한 크로스 플랫폼이라는 점입니다. Figma 플러그인도 웹 기술을 사용하여 개발됩니다. html
, js
및 css
를 알고 있으면 Figma 플러그를 작성할 수 있습니다. -안에. Figma 플러그인 원리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
캔버스
조각입니다. 그리고 캔버스 엔진 부분은 🎜WebAssembly🎜를 사용하기 때문에 Figma가 매우 매끄러울 수 있습니다. 데스크탑 Figma 앱은 🎜Electron🎜🎜 - 웹 기술 프레임워크를 사용합니다. 데스크톱 애플리케이션 개발. Electron은 브라우저와 유사하지만 실제로는 내부적으로 웹 애플리케이션을 실행합니다. 🎜iframe
이 의심할 여지 없이 최고입니다. 가장 직접적인 해결책. iframe
은 수년 동안 브라우저에서 사용되어 온 표준 W3C 사양입니다. 그 특징은 다음과 같습니다. 🎜postMessage(STRING)
메소드를 통해서만 가능하므로 통신 효율성이 매우 낮습니다. 플러그인에서 캔버스 요소를 조작하려면 먼저 메인 프레임에서 해당 요소의 노드 정보를 iframe
에 복사한 후 iframe
에서 작업을 완료한 후 노드 정보를 업데이트해야 합니다. code>iframe 메인 프레임. 여기에는 많은 의사소통이 필요하며, 복잡한 설계 초안의 경우 노드 정보가 매우 커서 의사소통 한계를 초과할 수 있습니다. 🎜🎜캔버스 작동 기능을 보장하려면 메인 스레드로 돌아가야 합니다. 메인 스레드에서 실행되는 플러그인의 주요 문제는 보안이므로 Figma 개발자는 영역 API🎜. 샌드박스에서는 순수한 js 코드와 Figma에서 제공하는 API만 실행할 수 있으며, 브라우저 API(네트워크, 스토리지 등)에 접근할 수 없어 보안이 보장됩니다. 🎜🎜🎜🎜 느낌 관심 있는 학생은 공식 팀 "웹에서 플러그인 시스템 구축하고 밤에도 푹 자는 방법"🎜, Figma 플러그인 솔루션 선택 과정을 자세히 소개하고, 그것을 읽으면 많은 유익을 얻을 수 있습니다. 🎜🎜종합적인 고려 끝에 Figma는 플러그인을 두 부분으로 나눕니다. 플러그인 UI는 iframe
에서 실행되고, 캔버스를 작동하는 코드는 메인 스레드의 격리 샌드박스에서 실행됩니다. UI 스레드와 메인 스레드는 postMessage
를 통해 통신합니다. 🎜🎜메인 스레드에 로드된 js
파일을 가리키도록 플러그인 구성 파일 manifest.json
의 main
필드를 구성하고 ui
필드 구성은 iframe
내의 html
파일에 로드됩니다. 플러그인을 열 때 메인 스레드는 figma.showUI()
메서드를 호출하여 iframe
을 로드합니다. 🎜🎜🎜가장 간단한 Figma 플러그인 작성🎜🎜🎜플러그인의 실행 프로세스를 이해하기 위해 먼저 가장 간단한 Figma 플러그인을 작성합니다. -안에. 기능은 간단합니다. 정사각형 색상 블록을 추가하거나 뺄 수 있습니다. 🎜manifest.json
파일을 만듭니다. 루트 디렉터리에 내용은 다음과 같습니다. 🎜{ "name": "simple-demo", "api": "1.0.0", "main": "main.js", "ui": "index.html", "editorType": [ "figjam", "figma" ] }
새 루트 디렉터리 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
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); };
Plugins
-> Development
-> simple-demo
기본 js 코드 편집새 루트 디렉터리 만들기 루트 디렉토리main.js
, 내용은 다음과 같습니다:
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' } }, '*') }
플러그인 시작Plugins
-> Development
-> Open console
可以打开调试控制台。可以看到我们打印的日志。
前面讲了,画布代码是运行在主线程的,为了执行效率,插件要操作画布内容也只能在主线程执行,即在 main.js
中。 main.js
中暴露了顶级对象 figma
,封装了用来操作画布的一系列API,具体可以去看官网文档。我们用 figma.createRectangle()
来创建一个矩形。主线程需要通过 figma.ui.onmessage
监听来自UI线程的事件,从而做出响应。修改后的 main.js
代码如下:
npm init vite@latest figma-plugin-vue3 --template vue-ts cd figma-plugin-vue3 npm install npm run dev
同时要修改 index.html
中的部分代码,通过 parent.postMessage
给主线程发送事件:
<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>
重新启动插件,再试验一下,发现已经可以成功加减色块了。
使用 Vue 3 开发 Figma 插件
通过前面的例子,我们已经清楚 Figma 插件的运行原理。但是用这种“原生”的 js
、 html
来编写代码非常低效的。我们完全可以用最新的Web技术来编写代码,只要打包产物包括一个运行在主框架的 js
文件和一个给 iframe
运行的 html
文件即可。我决定尝试使用 Vue 3
Figma 데스크톱 앱, 아무 곳이나 마우스 오른쪽 버튼으로 클릭 캔버스에서 플러그인
-> 개발
-> 매니페스트에서 플러그인 가져오기...
메뉴를 열고 이전에 생성된 Manifest.json
파일 경로를 입력하면 플러그인 가져오기에 성공하게 됩니다.
그런 다음
플러그인
-> 개발
-> 콘솔 열기
를 통해 열 수 있습니다. 우리가 인쇄한 로그를 볼 수 있습니다. 작업 캔버스앞서 언급했듯이 캔버스 코드는 실행 효율성을 위해 메인 스레드에서만 실행됩니다. 캔버스 콘텐츠, 즉 main.js
를 작동합니다. main.js
는 캔버스를 작동하는 데 사용되는 일련의 API를 캡슐화하는 최상위 개체 figma
를 노출합니다. 자세한 내용은 공식 웹사이트 문서 . figma.createRectangle()
을 사용하여 직사각형을 만듭니다. 메인 스레드는 figma.ui.onmessage
를 통해 UI 스레드의 이벤트를 수신하여 응답해야 합니다. 수정된 main.js
코드는 다음과 같습니다. Vue
+ TypeScript
的模板项目。
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); };
然后通过浏览器打开 http://localhost:3000
就能看到页面。
我们把前面的插件demo移植到 Vue 3 中。 src/App.vue
代码修改如下:
{ "compilerOptions": { // ... "skipLibCheck": true, "typeRoots": [ "./node_modules/@types", "./node_modules/@figma" ] }, }
我们在 src/worker
目录存放运行在主线程沙箱中的js代码。新建 src/worker/code.ts
,内容如下:
{ "name": "figma-plugin-vue3", "api": "1.0.0", "main": "code.js", "ui": "index.html", "editorType": [ "figjam", "figma" ] }
上述代码中缺少 figma
的 ts 类型声明,所以我们需要安装一下。
npm i -D @figma/plugin-typings
修改 tsconfig.json
,添加 typeRoots
,这样 ts 代码就不会报错了。同时也要加上 "skipLibCheck": true
,解决类型声明冲突问题。
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', }, }, }, })
Figma 插件需要的构建产物有:
manifest.json
文件作为插件配置
index.html
作为UI代码
code.js
作为主线程js代码
public
目录中的文件都会负责到构建产物 dist
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, }, })
index.html
의 일부 코드도 수정하여 parent.postMessage
스레드 전송 이벤트: 🎜npm run watch # 同时要在另一个终端里启动静态文件服务 npm run preview
js
및 html
을 사용하여 코드를 작성하는 것은 매우 비효율적입니다. 패키지 제품에 메인 프레임에서 실행되는 js
파일과 iframe<에서 실행되는 <code>html
이 포함되어 있는 한 최신 웹 기술을 사용하여 코드를 작성할 수 있습니다. /코드> 코드> 파일. 플러그인을 개발하기 위해 Vue 3
를 사용해 보기로 결정했습니다. (학습 영상 공유: 🎜vuejs 튜토리얼🎜) 🎜🎜 🎜Vue 3에 대해🎜 너무 많은 소개는 하지 않겠습니다. 아는 사람이라면 누구나 이해할 수 있을 것입니다. 돌아옵니다. 여기서 초점은 사용할 프레임워크가 아니라(vue 2로 변경, 반응 프로세스가 유사함) 구성 도구에 있습니다. 🎜🎜🎜Vite 새 프로젝트 시작🎜🎜🎜🎜Vite🎜은 Vue 작성자가 개발한 차세대 빌드 도구이며 Vue 3에 권장되는 빌드 도구이기도 합니다.
먼저 Vue
+ TypeScript
의 템플릿 프로젝트를 빌드해 보겠습니다. 🎜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); }
http://localhost:3000
을 열어 페이지를 확인하세요. 🎜src/App.vue
코드 수정은 다음과 같습니다. 🎜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();
src/worker
디렉터리에 저장합니다. 다음 내용으로 새 src/worker/code.ts
를 만듭니다. 🎜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
의 ts 유형 선언이 없으므로 이를 설치해야 합니다. 🎜🎜npm i -D @figma/plugin-typings
🎜🎜 tsconfig.json
을 수정하고 typeRoots
를 추가하여 ts 코드가 보고하지 않도록 합니다. 오류. 또한 유형 선언 충돌을 해결하려면 "skipLibCheck": true
를 추가하세요. 🎜rrreeemanifest.json</ code> 파일을 플러그인으로 구성 🎜</li><li>🎜<code>index.html
을 UI code🎜code.js
로 구성 메인 스레드로 js 코드🎜 li>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
作者:大料园
(学习视频分享:web前端开发)
위 내용은 Vue 3를 사용하여 Fimga 플러그인을 개발하는 과정을 기록합니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!