0

0

记录一个使用Vue 3开发Fimga插件的过程

青灯夜游

青灯夜游

发布时间:2022-04-11 11:27:11

|

4305人浏览过

|

来源于掘金社区

转载

如何用 vue 3 开发 figma 插件?下面本篇文章给大家介绍一下figma插件原理,记录下使用vue 3开发fimga插件的过程,并附有开箱即用的代码,希望对大家有所帮助!

记录一个使用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




  
  
  
  Demo
  
 


  

Figma 插件 Demo

当前色块数量:0

编辑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 来开发插件。(学习视频分享:vuejs教程

关于 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 代码修改如下:





我们在 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

ETsale产品销售简单报表管理系统3.0
ETsale产品销售简单报表管理系统3.0

ETsale3.0是采用php+mysql+smarty+jquery开发的一个很小很小的管理程序,适用于中小企业对产品销售后的报表管理以及客户资料和联通记录管理。由于采用smarty做模板,所以更方便修改外观以适用于自己的公司。程序加入简单的srm管理系统安装方法:1、以二进制上传所有文件到服务器目录2、修改cache,templates_c,目录为可写(0777)3、把include\conf

下载

修改 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.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 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.html  和  code.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 ),或者使用 React0标签。

手动生成 html 文件

对比上面的 html 代码和根目录的 index.html  文件,发现只是增加了一个 。所以我们可以自己解析  index.html ,然后插入相应这个标签,以及一个   标签。解析 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 提供了 React1,所以我们可以代码组织起来,写一个 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 仓库中,需要的同学可以直接下载使用:React2。

开发 Figma 插件还会遇到一些其他问题,例如如何进行网络请求、本地存储等,有空再继续分享我的实践心得。

本文转载自:https://juejin.cn/post/7084639146915921956

作者:大料园

(学习视频分享:React3)

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

556

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

374

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

733

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

477

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

414

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

991

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

658

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

553

2023.09.20

PHP WebSocket 实时通信开发
PHP WebSocket 实时通信开发

本专题系统讲解 PHP 在实时通信与长连接场景中的应用实践,涵盖 WebSocket 协议原理、服务端连接管理、消息推送机制、心跳检测、断线重连以及与前端的实时交互实现。通过聊天系统、实时通知等案例,帮助开发者掌握 使用 PHP 构建实时通信与推送服务的完整开发流程,适用于即时消息与高互动性应用场景。

11

2026.01.19

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Vue 教程
Vue 教程

共42课时 | 6.7万人学习

Vue3.x 工具篇--十天技能课堂
Vue3.x 工具篇--十天技能课堂

共26课时 | 1.4万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号