はじめに
前回の記事「FigmaのデザインからReactコードを生成するプラグインをブラッシュアップした話」では開発したFigmaプラグインのVer1からVer2へのバージョンアップの話をしました。
本記事では、Figmaプラグイン開発を通して得た知見を書いていきます。
Figmaプラグインの作り方
Figma標準機能で作る場合
Figmaには、標準でプラグインのテンプレート作成機能があり、このテンプレートを使うと簡単にプラグインを作成することができます。
使い方は、Figma上で右クリック「プラグイン>開発>プラグインの新規作成」のメニューを選択し、ウィザード画面を選択すると、Typescriptのプロジェクトがダウンロードできます。
出力されたコードをビルドすると code.js と manifest.json が作成されます。
npm install
npm run build
この manifest.jsonを Figma上で右クリック「プラグイン>開発>マニュフェストからプラグインのインポート」で指定すると、プラグインのインストールができます。
プラグインの実行は Figma上で右クリック「プラグイン>開発>[プラグインの名前]」をクリックすると実行できます。
サンプルプロジェクトをそのまま動かすと、入力した数の shape を書いてくれるプラグインが動きます。通常は、このサンプルプログラムを変更して プラグインを作成していくことになります。
このサンプルプロジェクトをベースに実装進めたところ、サンプルプロジェクトは、フレームワークを何も使っていないため、かなり使い勝手が悪いことが分かりました。
Code.ts
// This file holds the main code for plugins. Code in this file has access to
// the *figma document* via the figma global object.
// You can access browser APIs in the <script> tag inside "ui.html" which has a
// full browser environment (See https://www.figma.com/plugin-docs/how-plugins-run).
// Runs this code if the plugin is run in Figma
if (figma.editorType === 'figma') {
// This plugin will open a window to prompt the user to enter a number, and
// it will then create that many rectangles on the screen.
// This shows the HTML page in "ui.html".
figma.showUI(__html__);
// Calls to "parent.postMessage" from within the HTML page will trigger this
// callback. The callback will be passed the "pluginMessage" property of the
// posted message.
figma.ui.onmessage = (msg: {type: string, count: number}) => {
// One way of distinguishing between different types of messages sent from
// your HTML page is to use an object with a "type" property like this.
if (msg.type === 'create-shapes') {
// This plugin creates rectangles on the screen.
const numberOfRectangles = msg.count;
const nodes: SceneNode[] = [];
for (let i = 0; i < numberOfRectangles; 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-Reactで作る
色々調べたところ、Reactを使えるプラグインプロジェクト作成するモジュールをgithubで見つけしました。
Reactを使うことで、次のメリットが得られそうだったのでこのモジュールを使うことにしました。
- getElementById() や addEventListener() を書かなくて良い
- プラグインの中でも、フロントエンドの処理と、バックエンドの処理に分けて実装できる
- フロントエンドの処理では、nodeモジュールの資産が使用できる
DesignToCodeの仕組み
画面上の単なる四角形を <button>
に変換する
Figmaプラグインでは、Figmaが持つ画像の情報をノード情報として取得できます。
[
{
"type": "COMPONENT",
"name": "Modoru",
"properties": {
"fills": [
{
"type": "SOLID",
"visible": true,
"opacity": 1,
"blendMode": "NORMAL",
"color": {
"r": 1,
"g": 1,
"b": 1
},
"boundVariables": {}
}
],
"strokes": [],
"effects": [],
"layoutMode": "HORIZONTAL",
"primaryAxisSizingMode": "FIXED",
"counterAxisSizingMode": "FIXED",
"primaryAxisAlignItems": "MIN",
"counterAxisAlignItems": "MIN",
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 0,
"paddingBottom": 0,
"itemSpacing": 0,
"width": 80,
"height": 48,
"cornerRadius": 8,
"backgroundColor": "#ffffff"
},
"children": [
{
"type": "FRAME",
"name": "Modoru",
"properties": {
"fills": [
{
"type": "SOLID",
"visible": true,
"opacity": 1,
"blendMode": "NORMAL",
"color": {
"r": 1,
"g": 1,
"b": 1
},
"boundVariables": {}
}
],
"strokes": [
{
"type": "SOLID",
"visible": true,
"opacity": 1,
"blendMode": "NORMAL",
"color": {
"r": 0.6823529601097107,
"g": 0.7019608020782471,
"b": 0.7176470756530762
},
"boundVariables": {}
}
],
プラグインでは、このノード情報と「Figmaのノード情報からReactのコンポーネントコードを作って」のようなプロンプトとコンポーネントの画像をAIに送ることで、Reactのコード生成を実現しました。
しかし、最初は四角形のノード情報からAIでコードを生成すると、
const Modoru: React.FC<ModoruProps> = ({ label = '戻る' }) => {
return (
<div className={styles.modoru}>
<div className={styles.label}>{label}</div>
</div>
);
};
作業を進めていくと、Figmaのコンポーネント名やnameに「Button」の文字を入れると、AIが忖度してタグを作ってくれることが分かったため、ゆるいFigmaのデザイン制約を作るようにしました。
const Modoru: React.FC<ModoruProps> = ({ label = '戻る' }) => {
return (
<button className={styles.modoru}>
<div className={styles.label}>{label}</div>
</button>
);
};
開発で苦労した点
フロントエンドではReactが使えるが、バックエンドでは使えない
Figmaのプラグイン開発では、フロントエンドはReactを使用できますが、バックエンド(プラグインのメインスクリプト部分)では標準的なJavaScriptのみが利用可能です。そのため、バックエンドとフロントエンドのやりとりを工夫する必要がありました。
npmモジュールはフロントエンドでしか利用できない
npmの便利なモジュールはフロントエンドでのみ使用可能であり、バックエンドでは直接使用できません。
そのため、npmモジュールを使用したい場合は、バックエンドからフロントエンドに情報を渡し、フロントエンドでモジュール処理して、バックエンドに情報を戻すような処理になりました。
Figmaの情報取得はバックエンドでしかできない
Figmaのドキュメント情報を取得する処理はバックエンド(プラグインのメインスクリプト)でしか実行できません。そのため、フロントエンドとバックエンドの間でメッセージ通信を行い、データをやり取りするようにました。
フロントエンドとバックエンドはメッセージ通信のみでやり取り
Figmaのプラグインでは、フロントエンドとバックエンドが直接データを共有できず、「emit」でメッセージを送信して、「On」で受けるような処理になりました。
// コンポーネント選択変更時の処理を共通化
async function handleSelectionChange() {
const selection = figma.currentPage.selection;
selectNode = selection;
const tree = getComponentsStructure(selectNode);
// selectNodeに値が入っていたら、emitする
if (selectNode && selectNode.length > 0) {
emit<InitHandler>('INIT', figma.root.name, tree);
}
// 選択されたノードのプレビュー画像を生成
previewImage = await generatePreviewImage(selectNode[0]);
// 選択されたノードの情報を収集
const componentInfoPromises = selectNode.map(node => getSelectedNodeInfo(node, selectSceneNode));
const componentInfo = await Promise.all(componentInfoPromises);
// プレビュー画像を表示するイベントを発火
emit<DisplayImageHandler>('DISPLAY_IMAGE', previewImage, componentInfo);
}
// 'DISPLAY_IMAGE'イベントをリッスンし、プレビュー画像を設定
on<DisplayImageHandler>("DISPLAY_IMAGE", async (value, component) => {
setError(undefined);
setComponentInfo(component[0]);
setPreview(value);
});
ファイルのローカルへの読み書きができない
Figmaのプラグインでは、セキュリティ上の理由からローカルファイルへの読み書きができません。そのため、データの保存や読み込みはクラウド経由で AWS S3 で行う必要がありました。
※この部分は前回記事を参照ください
コード生成結果
お見積もり画面(Figmaデザイン)
Figmaで作成したお見積もり画面のデザインを元に、Reactコンポーネントを自動生成しました。思った以上にデザイン通りのコンポーネントが作成され、とても気持ちよかったです。開発の効率が大幅に向上できるのではないかと夢が広がりました。
生成されたコード
実際に生成されたReactコンポーネントのコードを以下に示します。
import React from 'react';
import styles from './PlanSelectionArea.module.css';
import CheckArea from './CheckArea';
import PriceArea from './PriceArea';
import StatusLabel from './StatusLabel';
import PlanButtonArea from './PlanButtonArea';
import PlanSelectionNotificationArea from './PlanSelectionNotificationArea';
interface PlanSelectionAreaProps {
label: string;
checked: boolean;
priceText: string;
priceUnitText: string;
statusAppend: boolean;
statusLabel: '追加中' | '追加する';
selectedPlan: 'お手軽' | '標準' | '保証充実';
notificationText: string;
onCheckChange: (checked: boolean) => void;
onPlanChange: (plan: 'お手軽' | '標準' | '保証充実') => void;
}
const PlanSelectionArea: React.FC<PlanSelectionAreaProps> = ({
label,
checked,
priceText,
priceUnitText,
statusAppend,
statusLabel,
selectedPlan,
notificationText,
onCheckChange,
onPlanChange,
}) => {
return (
<div className={styles.planSelectionArea}>
<div className={styles.topSection}>
<CheckArea checked={checked} label={label} onChange={onCheckChange} />
<PriceArea label="保険料" priceText={priceText} priceUnitText={priceUnitText} />
</div>
<div className={styles.middleSection}>
<StatusLabel append={statusAppend} label={statusLabel} />
<div className={styles.description}>
入院・通院の保障に加え、生活習慣病等による
<br />
様々なリスクに備えられる医療保険
<br />
保険期間:終身
</div>
</div>
<PlanButtonArea selectedPlan={selectedPlan} onPlanChange={onPlanChange} />
<PlanSelectionNotificationArea text={notificationText} />
<div className={styles.medicalIconArea}>
<img src="/images/MedicalIcon.svg" alt="Medical Icon" width={100} />
</div>
<div className={styles.medicalLinkArea}>
<a href="#" className={styles.link}>
保障内容を確認/カスタマイズ
<span className={styles.arrow}>></span>
</a>
</div>
</div>
);
};
export default PlanSelectionArea;
.planSelectionArea {
width: 345px;
padding: 10px;
border: 2px solid #0077ff;
border-radius: 12px;
background-color: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
}
.topSection {
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: 10px;
}
.middleSection {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
margin-bottom: 10px;
}
.description {
font-size: 12px;
color: #555555;
margin-top: 10px;
}
.medicalIconArea {
background-color: #faf7f0;
width: 100%;
display: flex;
justify-content: center;
padding: 10px 0;
margin-top: 10px;
}
.medicalLinkArea {
width: 100%;
display: flex;
justify-content: flex-end;
padding: 10px 0;
}
.link {
color: #0077ff;
text-decoration: none;
font-size: 15px;
}
.arrow {
margin-left: 5px;
}
@media (max-width: 768px) {
.planSelectionArea {
width: 100%;
}
}
課題
デザインが整っていないと、生成された画面が崩れる
ノード情報を使ってコードを生成するため、デザイン(ノード情報)が適切に作成されていない場合、期待通りのコードが出力されないことがあります。
AIのレスポンスが一定ではなく、生成コードがエラーになる場合がある
AIの実行結果は都度変動するため、生成されたコードにエラーが含まれることがあります。この問題を回避するために、何度かコード生成の操作が繰り返し必要になることもあります。
まとめ
完全な自動化は難しいものの、適切に手作業を組み合わせることで、バランスの良いワークフローを実現できると考えています。FigmaのデザインをReactコードに変換するこのプラグインを活用することで、デザインと開発の連携をスムーズにし、効率的な開発を目指したいです。
今後もさらなる改善を加えながら、より実用的なツールへと進化させていきたいと思います。