FigmaからReactコンポーネントを生成しよう(Scripter編)
notahotel.comドメイン下には、物件の販売、キャンペーン、採用サイトといった静的ページを公開しています。2024年には、月3から8ページ、海外サイトを公開した7月には60ページを超える規模のページを制作しました。
フルタイムで開発するのは、僕一人。アルバイト一名と、業務委託が期間により数名という規模で開発しました。デザイナーが作成したデザインデータをもとに、愚直にコードに落とすこともできるが、コンポーネント化、データモデリングをしても、大変な規模になってきています。
そこで、FigmaからReactのコンポーネントを生成する仕組みを検討することにしました。
Animaあたりが有名なプラグインだが、
- divタグだらけである
- cssの値を少し変更したい時にできない
- デザインシステムを徹底的にやることで、変数からタグが求めれるはず
- オーナー向けアプリや社内ツールで使用しているTheme UIやAnt Designに対応していない
といった点が気になり、いい機会なので、seyaさんのZenn記事を見て、自前の変換プログラムを育てることにしました。
コードを作ろう
デベロッパーツールで触ってみる
Figmaを開いた状態で、適当にフレームを選択し、デベロッパーツールを開いて、以下のコードを実行すると、
figma.currentPage.selection
選択したフレームの情報が取れます。parent, childrenで、親子関係。typeを見ると、TextNodeであればpタグ。fillsを見れば、imgタグ, videoタグと判断できそうですね。
CSSについては、getCssAsyncで取得することができます。 Reactコンポーネントを作れそうですね。Scripterを使って、生成してみる
ここからは、Scripterというプラグインを使います
こんな感じで、コードサジェストが効くので、気軽に使えます
選択したフレームからの木構造を生成し・・・
const getTree = async (node) => {
if (node.visible === false) {
return
}
if (node.type === 'VECTOR') {
return
}
const style = await node.getCSSAsync()
let children: any[] = []
if (node.children) {
for (let child of node.children) {
const res = await getTree(child)
children.push(res)
}
}
const sourceId = node.type === 'TEXT' ? node.id : node.name
// NOTE: コード生成したものと判断しやすくするため、N始まりにしている
let id = 'N' + sourceId.replaceAll(/[ :;/()]+/g, '')
let tagName = node.type === 'TEXT' ? 'p' : 'div'
if (
node.fills &&
node.fills.length > 0 &&
node.fills.find((fill) => fill.type === 'IMAGE')
) {
tagName = 'img'
}
return {
type: node.type,
id: id,
name: node.characters ?? node.name,
style: convertStyle(node, style),
tagName: tagName,
children: children.filter((node) => node),
}
}
let children: any[] = []
for (let node of selected) {
const res = await getTree(node)
children.push(res)
}
convertStyle部分は、Figmaから取得したCSSを改造したいので、入れています。
const convertStyle = (node, style) => {
const after = { ...style }
// NOTE: font-styleは使わない
if (style['font-style'] === 'normal') {
delete after['font-style']
}
// NOTE: backgroundは、まだ変数に落としていない
if ('background' in style) {
after['background'] = style['background'].replace(
/var\(.+, (#[a-fA-F0-9]+)\)/,
'$1'
)
}
// NOTE: line-heightは、px指定をしたくない
if ('line-height' in style) {
const percent = style['line-height'].match(/\d+\.{0,1}\d*%/)
if (percent?.length > 0) {
const num = parseFloat(percent[0].replace('%', '')) / 100
after['line-height'] = num
}
}
// NOTE: 段落の場合、日本語をちょうどよく改行して欲しい
if (node.type === 'TEXT') {
after['word-break'] = 'auto-phrase'
}
return after
}
LPでは、ReactとStyledComponentsを利用しているので、コードに変換します。
const STYLED_COMPONENTS_TEMPLATE = `
const %id% = styled.%tag%\`
%style%
\`
`
const generateStyledComponents = (node) => {
let array: string[] = []
for (let k in node.style) {
array.push(`${k}: ${node.style[k]}`)
}
const code = STYLED_COMPONENTS_TEMPLATE.replaceAll('%id%', node.id)
.replaceAll('%tag%', node.tagName)
.replaceAll('%style%', [...array, ''].join(';\n'))
codes.push(code)
node.children.forEach((node) => {
generateStyledComponents(codes, node)
})
}
const REACT_TEMPLATE = `
'use client'
import styled from 'styled-components'
const Component = () => {
return (
<>
%code%
</>
)
}
export default Component
%styledCode%
`
const codeText = REACT_TEMPLATE.replace('%code%', children.join('')).replace(
'%styledCode%',
codes.join('')
)
インデントが気になるが、そこはVSCodeなり各エディター側で保存時にコード整形を走らせればOKとしています。コード追加は手間がかかるので、コード削除だけで済むレベルにすると良いです。
コードは、こちら
まとめ
こういったコード生成のプラグインは、大体React, Tailwind CSSなので、他のライブラリを使っている場合でも、カバーできるのが良いですね。また、Scripterでコード実行することは、デザインシステムを進めるための調査、AIに読み取らせるための構造化のチェックでも使えると思います。
まずはScripterからコードでいじってみることから(デベロッパーツールでfigmaと叩くところから)、効率化を図ってみては、どうでしょうか。
静的ページの開発において、Figmaでできてしまうところはがっつり依存して、Figmaではできないところにクリエイティビティを注ぎこみたいですね。
ブランドサイトを開発するデベロッパー絶賛募集中です(副業は受け付けておりません)。
Discussion