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

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

最后看下现在实现的效果

独立圆角

阴影效果

边框

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

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

相关推荐
Cutey9163 分钟前
前端如何实现菜单的权限控制(RBAC)
前端·javascript·设计模式
yannick_liu5 分钟前
不引入第三方库,绘制圆环
前端
无名之逆6 分钟前
告别死锁!Hyperlane:Rust 异步 Web 框架的终极解决方案
服务器·开发语言·前端·网络·http·rust
公谨8 分钟前
MasterGo AI 生成设计图及代码
前端·人工智能
不会Android潘潘8 分钟前
HTTP2.0之Header 入门版、面试版 一看就懂
前端
心态与习惯9 分钟前
c++ 中的可变参数模板与折叠表达式
前端·c++·可变参数模板·折叠表达式
高端章鱼哥10 分钟前
Java的volatile和sychronized底层实现
前端
前端叭叭说12 分钟前
下去沉淀一下,最近开发中遇到的技术问题分享【前端】
前端
小璞15 分钟前
webpack 国际化Loader
前端·webpack
小藤神16 分钟前
鸿蒙ArkTS 关于相册,二维码生成、截图保存、真机扫码、上传相册内的图片(重点) ,语音识别搜索功能实现
前端·harmonyos·arkts