Figma 插件开发 - Vite 环境搭建

1,238 阅读2分钟

最近工作主要和 Figma 插件打交道,梳理一些踩坑的经验~

开始

官方的起始例子:www.figma.com/plugin-docs…

image.png

image.png

按步骤将插件文件保存到本地即可,调试时可以右键唤起插件,可以关注下几个功能入口:

  • Import plugin from manifest 导入本地插件
  • Open console 控制台调试
  • Run last plugin 加载最新的插件

目前 figma 插件开发流程没有有效的 hot reload 机制,【加载最新插件】在开发时比较常用,快捷键可以记一下。

image.png

image.png

插件架构

image.png

Figma 的插件架构比较简单,主要关注三部分:

  • manifest.json 插件清单
  • ui.html 入口
  • core.js 入口

manifest.json

{
  "name": "test",
  "id": "1095700741264679376",
  "api": "1.0.0",
  "main": "code.js",
  "editorType": [
    "figma"
  ],
  "ui": "ui.html"
}

ui.html

<h2>Rectangle Creator</h2>
<p>Count: <input id="count" value="5" /></p>
<button id="create">Create</button>
<button id="cancel">Cancel</button>
<script>
    document.getElementById('create').onclick = () => {
        const textbox = document.getElementById('count');
        const count = parseInt(textbox.value, 10);
        parent.postMessage({ pluginMessage: { type: 'create-rectangles', count } }, '*');
    };

    document.getElementById('cancel').onclick = () => {
        parent.postMessage({ pluginMessage: { type: 'cancel' } }, '*');
    };
</script>

core.js

figma.showUI(__html__);
figma.ui.onmessage = msg => {
  if (msg.type === 'create-rectangles') {
    const nodes: SceneNode[] = [];
    for (let i = 0; i < msg.count; i++) {
      const rect = figma.createRectangle();
      rect.x = i * 150;
      rect.fills = [{type: 'SOLID', color: {r: 1, g: 0.5, b: 0}}];
      figma.currentPage.appendChild(rect);
      nodes.push(rect);
    }
    figma.currentPage.selection = nodes;
    figma.viewport.scrollAndZoomIntoView(nodes);
  }
  figma.closePlugin();
};

manifest

清单描述文件主要描述应用入口、支持编辑器类型还有一些权限相关的配置:

  • main 用于指定 figma 主应用进程的脚本文件,有点类似 chrome 插件中 background 进程
  • ui 用于直指定插件交互面板 html 入口,类似 chrome 插件的 popup 页面
  • editorType 用于指定插件支持的编辑器类型,编辑的类型有两种,具体的看区别这里

core

core.js 运行在 figma 应用所提供的沙箱环境,其只能使用 figma 所提供 API 与一些基本的 Javascript 特性包括标准类型、JSON 和 Promise API、与 Uint8Array 一类的二进制对象。,DOM 一类的接口是无法使用的,在 core 中尽量使用 figma 提供的 api 来实现目标操作,一些依赖 DOM API 的操作可以考虑交由 ui 页面来处理。

ui

UI 主要用于插件与用户的交互,本质是一个 figma 主页面中内嵌的 iframe 页面,其使用 postmessage 与 core 进行交互。

image.png

附:之前为了方便 core 与 ui 通信还写了个页面通讯工具库 rpc-shooter 有兴趣的同学可以瞧瞧。

想具体了解插件的运行机制可以戳 How Plugins Run

环境搭建

Vite 这段时间使用下来体验十分友好,下面以 Vite 为例搭建一个 Figma 插件开发环境。

示例的仓库地址:github.com/kinglisky/f…

yarn create vite

image.png

这里选用的 Vue 与 TS,框架随意,Figma 插件开发建议使用 TS,接入 figma plugin 的 d.ts 可以方便的当文档使用。

figma plugin 的类型支持需要配置下 tsconfig.json

yarn add @figma/plugin-typings -D
{
  "compilerOptions": {
   ...other,
    "typeRoots": ["./node_modules/@types", "./node_modules/@figma"]
  }
}

多入口

Figma 插件中我们只关心两个入口文件:

  • ui.html
  • core.js

两个入口文件相互独立,需要将 ui 与 core 拆分成两个包来维护,整理下目录结构拆分出 ui 与 core 部分,示例中还拆分了一个 common 包用于提供公共配置:

.
├── lerna.json
├── manifest.json
├── package.json
├── packages
│   ├── common
│   │   ├── constants
│   │   │   └── ui.ts
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── core
│   │   ├── favicon.svg
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src
│   │   │   └── index.ts
│   │   ├── tsconfig.json
│   │   └── vite.config.ts
│   └── ui
│       ├── index.html
│       ├── package.json
│       ├── src
│       │   ├── App.vue
│       │   ├── env.d.ts
│       │   ├── main.ts
│       │   └── styles
│       │       └── index.css
│       ├── tsconfig.json
│       ├── tsconfig.node.json
│       └── vite.config.ts
├── scripts
│   └── build.js
├── tsconfig.json
└── yarn.lock

github.com/kinglisky/f…

workspaces

本地进行多包开发时,workspaces 是个很好用的功能,可以将本地包作为依赖来使用,有点类似 webpack 的中 alias 配置将依赖指向本地。

将 packages 目录下的所有包做作为本地依赖:

{
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  ...
}

插件中实际 core 与 ui 都会从 common 中读配置,common 包的 package.json 配置下 name 与 module 即可提供给 core 与 ui 使用:

{
  "name": "figma-vite-common",
  "private": false,
  "version": "0.0.0",
  "module": "./",
  "types": "./"
}

core 与 ui 使用时:

import { VIEW_WIDTH, VIEW_HEIGHT } from 'figma-vite-common/constants/ui';

构建配置

有一点需要注意一下,core.js 与 ui.html 作为插件的入口文件只能是个单文件,简单来说就是不能拆分文件,不能使用异步模块导出多个文件。

  • core 中所有依赖的资源都打包到 core.js
  • ui.html 中所有的资源都会以内联形式存在

core 配置

core 的配置比较简单:

import { resolve } from 'path';
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      formats: ['iife'],
      name: 'core',
      fileName: 'core',
    },
    outDir: resolve(__dirname, '../../dist'),
  },
});

由于不需要发包使用,将最终打包的 core 入口格式配置为 iife (立即执行的函数表达式模块),简单的包一层立即调用函数即可:

// 构建结果
(function () {
  'use strict';
  figma.showUI(__html__, { width: 400, height: 490 }),
    (figma.ui.onmessage = (o) => {
      if (o.type === 'create-rectangles') {
        const t = [];
        for (let i = 0; i < o.count; i++) {
          const e = figma.createRectangle();
          (e.x = i * 150),
            (e.fills = [{ type: 'SOLID', color: { r: 1, g: 0.5, b: 0 } }]),
            figma.currentPage.appendChild(e),
            t.push(e);
        }
        (figma.currentPage.selection = t),
          figma.viewport.scrollAndZoomIntoView(t);
      }
      figma.closePlugin();
    });
})();

对应的配置下 package scripts:

{
  "name": "figma-vite-core",
  "private": false,
  "version": "0.0.0",
  "scripts": {
    "start": "tsc && vite build -w",
    "build": "tsc && vite build"
  },
  "dependencies": {
    "figma-vite-common": "*"
  },
  "devDependencies": {
    "@figma/plugin-typings": "^1.42.1",
    "typescript": "^4.5.4",
    "vite": "^2.8.0"
  }
}

开发时会以 yarn start 启动, -w 监听文件变动触发构建。

ui 配置

ui 由于最终的构建产物是一个 html 文件,其他的构建的资源需要内联,这里需要将各种代码拆分的配置关闭,内联资源的阈值调大:

import { resolve } from 'path';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { viteSingleFile } from 'vite-plugin-singlefile';

export default defineConfig({
  plugins: [vue(), viteSingleFile()],
  build: {
    target: ['es6'],
    assetsInlineLimit: 100000000,
    chunkSizeWarningLimit: 100000000,
    cssCodeSplit: false,
    brotliSize: false,
    outDir: resolve(__dirname, '../../dist'),
    rollupOptions: {
      inlineDynamicImports: true,
      output: {
        format: 'iife',
        manualChunks: () => 'everything.js',
      },
    },
  },
});

vite-plugin-singlefile 插件用于将构建出的 js 资源内联到 html 中,构建的包格式还是 iife

package scripts 配置:

{
  "name": "figma-vite-ui",
  "private": false,
  "version": "0.0.0",
  "scripts": {
    "start": "vue-tsc --noEmit && vite build -w",
    "build": "vue-tsc --noEmit && vite build"
  },
  "dependencies": {
    "figma-vite-common": "*",
    "vue": "^3.2.25"
  },
  "devDependencies": {
    "@figma/plugin-typings": "^1.42.1",
    "@vitejs/plugin-vue": "^2.2.0",
    "sass": "^1.49.7",
    "typescript": "^4.5.4",
    "vite": "^2.8.0",
    "vite-plugin-singlefile": "^0.6.3",
    "vue-tsc": "^0.29.8"
  }
}

同样使用 yarn start 启动,-w 监听变动构建。

启动入口

实际在插件开发时需要同时启动 core 与 ui,所以可以在根目录下配置个启动命令,可以直接使用 lerna 进行启动。

配置 lerna.json

{
  "packages": ["packages/*"],
  "version": "0.0.0",
  "npmClient": "yarn",
  "useWorkspaces": true
}

配置 package.json

{
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "start": "lerna run --stream --scope figma-vite-ui --scope figma-vite-core start"
  },
  "publishConfig": {
    "access": "public"
  },
  "devDependencies": {
    "lerna": "^4.0.0"
  },
  "name": "fgima-vite"
}

image.png

最后改一下插件manifest.json 中入口文件的路径:

{
  "name": "figma-vite-demo",
  "id": "",
  "api": "1.0.0",
  "main": "dist/core.iife.js",
  "ui": "dist/index.html",
  "editorType": ["figma"]
}

自此一个简单 Figma 插件开发环境完成,仓库在这 github.com/kinglisky/f…

其他

有空会梳理下 Figma 文档数据结构与一些解析技巧,先这样~