在上文,脚手架已经搭建好了,成功的运行项目并且消息传递成功,那么现在就开始实现转化代码。 首先我让 cursor 帮我写了个 ui 界面,支持修改展示尺寸,界面就直接变成这样了。看起来还不错,所以我也没怎么改,因为这不是重点
tsx
function App() {
const [width, setWidth] = useState(490)
return (
<div className='relative h-full w-full'>
// 预览区域
<RenderView width={width} />
// 尺寸按钮
<ResizeButton setWidth={setWidth} />
</div>
)
}
export default App
然后到RenderView
组件中监听一下从 plugin 传来的消息,因为预期是代码生成的部分完全由 plugin 控制。RenderView
所做的事情就是接受html 字符串,用dangerouslySetInnerHTML
的方式插入父容器。
tsx
export const RenderView: FC<{
width: number
}> = ({ width }) => {
const [html, setHtml] = useState('')
useEffect(() => {
window.addEventListener('message', (event) => {
const message = event.data.pluginMessage as MessageFromPlugin
if (message.type === 'html') {
setHtml(message.data)
}
})
}, [])
return <div className="w-full p-1 box-border flex h-full justify-center">
<div style={{ width: width }} className="h-8/9 ring-1 ring-gray-200 rounded-lg">
<div dangerouslySetInnerHTML={{ __html: html }} />
</div>
</div>
}
接下来打开plugin/index.ts
,修改代码,调用遍历图层的函数traverseLayer
,返回的实例有个 render 方法,调用render 来生成 html string
ts
if (selection.length == 1) {
const div = traverseLayer(selection[0])
sendMessageFromPlugin({
type: 'html',
data: div.render()
})
}
接下来实现traverseLayer
,核心逻辑就是一个 node生成一个 div 实例之后 renturn 出去。递归遍历 children。
ts
export function traverseLayer(layer: SceneNode) {
const div = new DIV()
// ...
if ('children' in layer) {
for (const child of layer.children) {
const childDiv = traverseLayer(child)
div.addChild(childDiv)
}
}
return div
}
看下 DIV 这个类的代码,同样也非常简单,说白了就是只有一个字符串拼接的功能。根据传入的 children 和 style 最终拼接一段正确的 html div string。
ts
import { CSSProperties } from "react"
// 驼峰转横杠
const camelToDash = (str: string) => {
return str.replace(/([A-Z])/g, '-$1').toLowerCase()
}
export class DIV {
private style: CSSProperties = {}
private children: DIV[] = []
public addStyle(style: CSSProperties) {
this.style = Object.assign({}, this.style, style)
}
public addChild(child: DIV) {
this.children.push(child)
}
public render(): string {
return `
<div style="${Object.entries(this.style).map(([key, value]) => `${camelToDash(key)}: ${value}`).join('; ')}">
${this.children.map(child => child.render()).join('')}
</div>
`
}
}
到此代码生成的主题框架搭好了,接下来要做的事情就是无脑的写规则,熟悉 figma 的 api。我这次写了这些 css 属性。
由于大同小异,我就不全展开,简单介绍几个
宽度
ts
import { CSSProperties } from "react"
export const convertWidth = (layer: SceneNode): CSSProperties => {
// 如果图层有 width 属性,就转为 css width
if ('width' in layer) {
return { width: layer.width + 'px' }
}
return {}
}
边框宽度
ts
// border-width.ts
import { CSSProperties } from "react"
import { filterVisiabelPaints } from "./util"
export const convertBorderWidth = (layer: SceneNode): CSSProperties => {
// 如果图层不支持设置边框,直接 return
if (!('strokes' in layer)) {
return {}
}
// 过滤出可见的边框
const strokes = filterVisiabelPaints(layer.strokes)
if (strokes.length === 0) {
return {}
}
if ('strokeWeight' in layer) {
if (layer.strokeWeight !== figma.mixed) {
return { borderWidth: layer.strokeWeight + 'px' }
} else {
// figma 支持独立边框,所以我们也要支持
if ('strokeTopWeight' in layer) {
const { strokeTopWeight, strokeRightWeight, strokeBottomWeight, strokeLeftWeight } = layer
return {
borderWidth: `${strokeTopWeight}px ${strokeRightWeight}px ${strokeBottomWeight}px ${strokeLeftWeight}px`
}
} else {
return {}
}
}
}
return {}
}
阴影效果
ts
import { CSSProperties } from "react"
import { filterVisibleEffect, RGBAtoRGBA } from "./util"
export const convertBoxShadow = (layer: SceneNode): CSSProperties => {
if (!('effects' in layer)) {
return {}
}
const effects = filterVisibleEffect(layer.effects)
if (effects.length === 0) {
return {}
}
let code = ''
for (const effect of effects) {
// figma 支持外阴影
if (effect.type === 'DROP_SHADOW') {
code += `${effect.offset.x}px ${effect.offset.y}px ${effect.radius}px ${effect.spread}px ${RGBAtoRGBA(effect.color)},`
} else if (effect.type === 'INNER_SHADOW') {
// figma 支持内阴影
code += `inset ${effect.offset.x}px ${effect.offset.y}px ${effect.radius}px ${effect.spread}px ${RGBAtoRGBA(effect.color)},`
}
}
// figma 可以叠加阴影效果
// css 中阴影叠加需要写到一行用逗号分割
if (code.length > 0) {
code = code.slice(0, -1)
return { boxShadow: code }
}
return {}
}
最后看下现在实现的效果
独立圆角
阴影效果
边框
看到这里是不是感觉代码生成的过程非常简单,想要看源码的朋友可以直接去仓库查看
后面的章节会继续打磨细节,并且对支持自动布局,欢迎持续关注