低代码平台前端的设计与实现(三)设计态画布DesignCanvas的设计与实现

上一篇文章,我们分析并设计了关于构建引擎BuildEngine的切面设计。本文我们将基于BuildEngine所提供的切面处理能力,在ComponentBuildAspectHandler中通过一些逻辑,来完成一个轻量级的设计器画布。

这个画布能够实现如下的一个简单的效果。对于所有渲染出来的元素,都会有一个灰色的边框,当我们选中某个元素的时候,就会高亮显示。

ElementNodeDesignWrapper

要做到上述效果,对于通过ElementNode创建出来的组件,我们可以使用一个元素来进行包裹,我们暂时对这个组件取名为ElementNodeDesignWrapper,它的作用就是能够给每一个元素添加边框。

这个wrapper组件,我们至少会设计以下几个属性:

  • nodePath:一个基本信息,作为外部传入;
  • isSelected:决定该wrapper是否被选中;
  • onClick:wrapper组件被点击时候,触发的onClick事件;

有了isSelectedonClick以后,我们就可以让上层代码来控制多个元素究竟是哪个元素需要高亮。

export type ElementNodeDesignWrapperProps = {
    /**
     * 标识当前节点path
     */
    nodePath: string;
    /**
     * 是否被选中
     */
    isSelected?: boolean;
    /**
     * 点击事件
     */
    onClick?: () => void;
}

对于这个wrapper,我们考虑使用div元素来包裹子元素,也就是说,wrapper的本质是div。这个div元素我们通过isSelected(是否选中)来控制其CSS中的outline样式配置。之所以选择outline,是因为outline在显示的时候,是不会影响元素的位置大小的,但缺点则是无论其元素是什么外形,outline总是矩形。

其次,我们还需要考虑这样一种问题,如果wrapper div包裹的实际HTML是<button><a><span><b>以及<i>元素,如果我们不将这个作为wrapper div的display设置为inline-block,那么wrapper div则会变成宽度占据一行的元素,会变成如下效果:

我们需要做的就是,检测wrapper div内部的元素是button、a、span、b或i元素的时候,则将wrapper div的样式中display属性置为inline-block,这样wrapper div就可以贴合这些元素。

那么,如何检测呢?我们可以采用这样一种方式:通过useRef这个Hook来创建一个ref,交给我们的wrapper div;然后,在useEffect的回调中,拿到类型为HTMLDivElement的ref.current。这个current我们可以通过访问firstChild就是div的唯一一个子元素,也就是wrapper包裹的元素。并且,我们可以访问firstChild.nodeName就能知道wrapper的HTML元素名称。存放到一个名为targetNodeHtmlType的state中;最后,我们按照上面的需求,让wrapper div的样式中的display属性,根据targetNodeHtmlType是否属于button、a、span、b或i元素中的一种来决定是否是inline-block

最后,我们还需要对wrapper div的onClick事件进行“代理”,并阻止冒泡。

综合以上的分析,我们Wrapper div最终的样式核心代码:

export const ElementNodeDesignWrapper: FC<PropsWithChildren<ElementNodeDesignWrapperProps>> = (props) => {

    const {
        nodePath,
        isSelected = false,
        children,
        onClick = () => {
        }
    } = props;

    const ref = useRef<HTMLDivElement | null>(null);

    const [
        targetNodeHtmlType,
        setTargetNodeHtmlType
    ] = useState<string>();

    useEffect(() => {
        if (!ref || !ref.current) {
            return;
        }
        const currentEle: HTMLDivElement = ref.current;
        const eleNodeName = currentEle.firstChild.nodeName;
        setTargetNodeHtmlType(eleNodeName);
    });

    const style: CSSProperties = useMemo(() => {
        // Wrapper内部以下实际的HTML元素在展示的过程中,需要使用inline-block
        // 否则会显示异常
        const inlineBlockEle = ['A', 'SPAN', 'BUTTON', 'B', 'I'];
        return {
            boxSizing: 'border-box',
            // 元素被选中,则使用蓝色高亮边框,否则使用灰色虚线
            outline: isSelected ? '2px solid blue' : '1px dashed gray',
            display: inlineBlockEle.includes(targetNodeHtmlType) ? 'inline-block' : '',
            padding: '3px',
            margin: '3px'
        }
    }, [isSelected, targetNodeHtmlType]);

    return (
        <div key={nodePath + '_wrapper_key'}
             style={style}
             ref={ref}
             onClick={(event) => {
                 event.stopPropagation();
                 onClick();
             }}>
            {children}
        </div>
    )
}

DesignCanvas

接下来,我们开始设计一个名为DesignCanvas的设计态画布,这个画布我们先暂时先不考虑比较复杂的功能,先考虑如何结合上面的Wrapper组件进行基本的效果呈现。考虑到对外屏蔽DesignCanvas的细节,我们只暴露一个属性,就是传入 JSON schema:

interface DesignCanvasProps {
    /**
     * Schema JSON字符串
     */
    rootNodeSchemaJson: string;
}

export const DesignCanvas = (props: DesignCanvasProps) => {
    const {
        rootNodeSchemaJson
    } = props;

    // 1. 存储单机选中的path的state
    const [selectedNodePath, setSelectedNodePath] = useState<string>('');

    // 2. 经过切面绑定的buildEngine
    const buildEngine = ... ...

    // 3. 经过buildEngine + schema 创建的React组件(已经考虑的基本的异常处理)
    const renderComponent = ... ...

    return (
        <div style={{
            width: '100%',
            height: '100%',
            padding: '5px'
        }}>
            {renderComponent}
        </div>
    )
}

(1)selectedNodePath用以存储当前选中的path。在后续的切面处理中,构建元素节点的时候,如果切面正在处理的节点path与selectedNodePath一致,则wrapper组件需要高亮,否则虚线。

(2)buildEngine的代码具体如下:

		// 经过切面绑定的buildEngine
    const buildEngine = useMemo(() => {
        const engine = new BuildEngine();
        engine.componentBuildAspectHandler = (reactNode, ctx) => {
            const {path} = ctx;

            const wrapperProps: ElementNodeDesignWrapperProps = {
                nodePath: path,
                isSelected: path === selectedNodePath,
                onClick: () => {
                    console.debug('wrapper onClick')
                    setSelectedNodePath(path)
                }
            }

            return (
                <ElementNodeDesignWrapper {...wrapperProps}>
                    {reactNode}
                </ElementNodeDesignWrapper>
            )
        }
        return engine;
    }, [selectedNodePath]);

上面的buildEngine中的componentBuildAspectHandler切面处理,我们编写了我们自己的实现,原本默认返回的组件,我们使用ElementNodeDesignWrapper进行包裹返回。其中:

  1. isSelected属性来自于当前正处理节点path与第1点DesignCanvas组件存储的path的比对,如果当前正在处理及的几点就是已经选中的节点path,那么这个wrapper组件则被“选中”。

  2. onClick属性的实现代码则是当wrapper组件点击后,更新selectedNodePath。

(3)renderComponent的实现:

  // 经过buildEngine + schema 创建的React组件(已经考虑的基本的异常处理)
    const renderComponent = useMemo(() => {
        try {
            const eleNode = JSON.parse(rootNodeSchemaJson);
            return buildEngine.build(eleNode);
        } catch (e) {
            return <div>构建出错:{e.message}</div>
        }
    }, [rootNodeSchemaJson, selectedNodePath]);

对于这个渲染React组件,主要是将schema解析为ElementNode结构,并交给构建引擎build;如果报错则返回一个异常组件。

样例

在编写样例之前,我们先导出DesignCanvas,然后适当修改样例代码:

import {ChangeEvent, useState} from "react";
import {Input} from 'antd';
import {DesignCanvas} from "@lite-lc/core";

export function SimpleExample() {

    // 使用state存储一个schema的字符串
    const [elementNodeJson, setElementNodeJson] = useState(JSON.stringify({
        "type": "page",
        "props": {
            "backgroundColor": "pink", // page的 backgroundColor 配置
        },
        "children": [
            {
                "type": "button",
                "props": {
                    "size": "blue" // button的size配置
                },
            },
            {
                "type": "input"
            }
        ]
    }, null, 2))

    return (
        <div style={{width: '100%', height: '100%', padding: '10px'}}>
            <div style={{width: '100%', height: 'calc(50%)'}}>
                <Input.TextArea
                    autoSize={{minRows: 2, maxRows: 10}}
                    value={elementNodeJson}
                    onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
                        const value = e.target.value;
                        // 编辑框发生修改,重新设置JSON
                        setElementNodeJson(value);
                    }}/>
            </div>
            <div style={{width: '100%', height: 'calc(50%)', border: '1px solid gray'}}>
                <DesignCanvas rootNodeSchemaJson={elementNodeJson}/>
            </div>
        </div>
    );
}

效果:

附录

本次相关代码已经提交至github,对应分支为chapter_03:

w4ngzhen/lite-lc at chapter_03 (github.com)

0 条评论
请不要发布违法违规有害信息,如发现请及时举报或反馈
还没有人评论呢,速度抢占沙发!
相关文章
  • OAuthApp 是一个前端发布工具,用于快速开发前端网页项目,并发布到服务器。具有引入脚本库就能使用服务端 API 在线发布 H5,数据独立存储的特性 更新内容: 1,新增角色 / 权限管理,可自定...

  • 在实现 form 表单数据采集和提交之前,我们首先了解一下前后端协作的流程。我们前端工程师做好表单后,交给用户在浏览器上填报,用户填完信息后,点击提交按钮,数据通过互联网发给了服务器,后端工程师在服务...

  • 漏洞简介      影响版本 0.13.0 >

  • 在现代的B/S架构应用中,我们会做前后端分离,某些前端Web服务会将编译完成的静态文件放到一个web服务器进行部署。例如,我的博客也是基于Hugo编译的静态文件来进行部署的。那在容器化部署模式下,我们...

  • 2023年值得收藏的开源或免费的web应用防火墙 2023年,数字经济将强势崛起,并且成为新一轮经济发展的动力,传统的黑客破坏性攻击如CC,转为更隐蔽的如0day进行APT渗透。所以无论私有服务器还是...

  • 前端项目修改了很多东西:比如bug啊,样式啊。当你把前端项目打包之后满心欢喜的在 Nginx(测试环境)换上它,然后在 Jira 上修改bug状态@测试人员复测。然后测试人员开始找你battle了,你...

  • 多病用一个词总结我的2022 ,毫无疑问是【多病】。翻看挂号记录,今年累计跑了19次医院,除去定期的脱发复查、尿常规复查外,其他还得了皮肤病、急性咽炎、筋膜炎、结膜炎、肾结石、慢性胃炎、胸闷,体验过了...

  • 1.安装sudo cnpm i js-export-excel12.使用 //导入ExportJsonExcel包 const ExportJsonExcel = require('js-e...

  • 首先就是要下载Visual Studio,具体可以登录官网查看下载教程。 首次打开Visual Studio,就会显示新建的页面,若是使用过的则根据以下操作进行:  以下界面则与新使用的新建界面一致...

  • 写一些demo的时候使用vue/react脚手架来初始项目太小题大做,直接在html中写代码需要找到一些框架和库的cdn,这里做下推荐,仅限在测试环境用。 bootcdn 优点:是国内速度快,使用简单...

  • Mac/Windows 浏览器开发者工具远程调试 iPhone/Android 页面 在移动端 Web 开发中,有时候只通过模拟器进行调试是不够的,需要在真机环境下进行调试才能发现并解决一些问题。而移...

  • 工作中所涉及到的工作,也有一些PM的工作,比如:协调人员、拆分任务并分配给相关人员,把控工作进度、评审、变更管理等等。 项目立项,资源申请(服务器资源、人员) 需求评审(业需、软需) 接...

  • 前端开发 获取剪切板的图片 获取剪切板的图片 -> File -> Base64 -> Blob -> url -> Image,以及它们之间的各种相互转换 浏览器导出(下载)文件,并导入(...

  • 懒加载和预加载的目的都是为了提高用户的体验,二者行为是相反的,一个是延迟加载,另一个是提前加载。懒加载对缓解服务器压力有一定作用,预加载则会增长服务器前端压力缓存。懒加载 lazyload懒加载:又叫...

  • 说起转义字符,大家最先想到的肯定是使用反斜杠,这也是我们最常见的,很多编程语言都支持。 转义字符从字面上讲,就是能够转变字符原本的意义,得到新的字符。常用在特殊字符的显示以及特定的编码环境中。 除了反...

  • 解决网站图标问题的最佳方案——SVG!SVG 是一种基于 XML 语法的图像格式,英文全称是: Scalable Vector Graphics,即可缩放矢量图,是 W3C 的一项建议。我们用手机拍摄...

  • WEB开发会话技术04 14.Session生命周期 14.1生命周期说明 public void setMaxInactiveInterval(int interval):设置session的超...

  • 打开编辑器,继续在 case_form.html 页面中编写代码。在“基本信息” fieldset 分组标签   结束的位置处,回车换行。再添加一组 fieldset 与 legend 标签,在 le...

  • 作者:京东科技 郝梁 前言:作为 C 端前端研发,除了攻克业务难点以外,也要有更深层的自我目标,那就是性能优化。这事儿说大不大,说小也不小,但难度绝对不一般,所涉及的范围优化点深入工程每个细胞。做好前...

  • 前言 ASP.NET Core Web API 接口限流、限制接口并发数量,我也不知道自己写的有没有问题,抛砖引玉、欢迎来喷! 需求 写了一个接口,参数可以传多个人员,也可以传单个人员,时间范围限制...