从零开发设计稿转代码插件(二)

在上文,脚手架已经搭建好了,成功的运行项目并且消息传递成功,那么现在就开始实现转化代码。 首先我让 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 {}
}

最后看下现在实现的效果

独立圆角

阴影效果

边框

看到这里是不是感觉代码生成的过程非常简单,想要看源码的朋友可以直接去仓库查看

后面的章节会继续打磨细节,并且对支持自动布局,欢迎持续关注

相关推荐
泯泷1 小时前
那段文字是谁删的?Yjs 14 正式版之前,一套删除归属方案的实现与边界
前端·javascript·算法
野槐4 小时前
前端项目优化(有了我,会发生...)
前端
aa小小4 小时前
【无标题】
前端
计算机魔术师5 小时前
还在单线程跟 AI 聊天?Claude Code 并行多会话让你一个人干三个人的活
前端
志尊宝5 小时前
Vue3 零基础每日笔记(038):亲手封装 useDebounceFn 与 useThrottleFn——高频事件的流量阀
前端·javascript·vue·html·vue3
JavaGuide5 小时前
对标 MinIO!全新一代分布式文件系统正式发布
前端·后端
风骏时光牛马5 小时前
AI驱动自动化业务工作流搭建实践
前端
码云之上6 小时前
Skill 里的脚本终于能跑了,星悟接 CubeSandbox 的纪实
前端·人工智能·前端框架
IT_陈寒6 小时前
Vue computed属性这个坑,我居然踩了三次才爬出来
前端·人工智能·后端
烈风逍遥6 小时前
第三篇:组件化实践,SpTable 通用表格组件设计
前端·架构