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

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

ComponentNodeDesignWrapper

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

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

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

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

export type ComponentNodeDesignWrapperProps = {
/**
* 标识当前节点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 ComponentNodeDesignWrapper: FC<PropsWithChildren<ComponentNodeDesignWrapperProps>> = (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 {
/**
* 传入的合法 ComponentNode
*/
componentNode: ComponentNode;
} export const DesignCanvas = (props: DesignCanvasProps) => {
const {
componentNode
} = 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的代码具体如下:

		// 我们用useMemo来缓存一个无状态的BuildEngine
const buildEngine = useMemo(() => {
return new BuildEngine();
}, []);

(3)renderComponent的实现(重点):

    const renderComponent = useMemo(() => {
try {
return buildEngine.build(componentNode, {
onCustomCreateElement: ctx => {
const {componentNode,
path, ComponentConstructor, props, children} = ctx; // 不加Wrapper的原始构造后的组件
const originReactComp = (
<ComponentConstructor {...props}>
{children}
</ComponentConstructor>
) const wrapperProps: ComponentNodeDesignWrapperProps = {
nodePath: path,
isSelected: path === selectedNodePath,
onClick: () => {
console.debug('wrapper onClick')
setSelectedNodePath(path)
}
}
return (
<ComponentNodeDesignWrapper {...wrapperProps}>
{originReactComp}
</ComponentNodeDesignWrapper>
)
}
});
} catch (e) {
return <div>构建出错:{e.message}</div>
}
}, [componentNode, selectedNodePath]);

对于这个渲染React组件,主要是将schema解析为ComponentNode结构,并交给构建引擎build,同时,我们还传入了自定义的创建过程:

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

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

样例

在编写样例之前,我们先导出DesignCanvas,然后编写一份测试代码DesignCanvasExample:

import {ChangeEvent, useMemo, useState} from "react";
import {Input} from 'antd';
import {ComponentNode, DesignCanvas} from "@lite-lc/core"; export function DesignCanvasExample() { // 使用state存储一个schema的字符串
const [componentNodeJson, setComponentNodeJson] = useState(JSON.stringify({
"componentName": "page",
"children": [
{
"componentName": "button",
"props": {
"size": "small",
"type": "primary"
},
"children": [
{
"componentName": "text",
"props": {
"value": "hello, my button."
}
}
]
},
{
"componentName": "input"
}
]
}, null, 2)) const componentNode = useMemo(() => {
return JSON.parse(componentNodeJson) as ComponentNode;
}, [componentNodeJson]) return (
<div style={{width: '100%', height: '100%', padding: '10px'}}>
<div style={{width: '100%', height: 'calc(50%)'}}>
<Input.TextArea
autoSize={{minRows: 2, maxRows: 10}}
value={componentNodeJson}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
// 编辑框发生修改,重新设置JSON
setComponentNodeJson(value);
}}/>
</div>
<div style={{width: '100%', height: 'calc(50%)', border: '1px solid gray'}}>
<DesignCanvas componentNode={componentNode}/>
</div>
</div>
);
}

效果:

当然,细心的伙伴已经发现了问题了。因为在我们的框架中,文本也是一个ComponentNode,会导致这个文本组件节点也被Wrapper包裹了。这个我们后续会通过对Wrapper进行优化来完成。这里不再赘述。

附录

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

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

低代码平台前端的设计与实现(三)设计态画布DesignCanvas的设计与实现的更多相关文章

  1. 使用WtmPlus低代码平台提高生产力

    低代码平台的概念很火爆,产品也是鱼龙混杂. 对于开发人员来说,在使用绝大部分低代码平台的时候都会遇到一个致命的问题:我在上面做的项目无法得到源码,完全黑盒.一旦我的需求平台满足不了,那就是无解.   ...

  2. vivo 低代码平台【后羿】的探索与实践

    作者:vivo 互联网前端团队- Wang Ning 本文根据王宁老师在"2022 vivo开发者大会"现场演讲内容整理而成.公众号回复[2022 VDC]获取互联网技术分会场议题 ...

  3. 基于低代码平台(Low Code Platform)开发中小企业信息化项目

    前言:中小企业信息化需求强烈,对于开发中小企业信息化项目的软件工作和程序员来说,如何根据中小企业的特点,快速理解其信息化项目的需求并及时交付项目,是一个值得关注和研讨的话题. 最近几年来,随着全球经济 ...

  4. 低代码平台--基于surging开发微服务编排流程引擎构思

    前言 微服务对于各位并不陌生,在互联网浪潮下不是在学习微服务的路上,就是在使用改造的路上,每个人对于微服务都有自己理解,有用k8s 就说自己是微服务,有用一些第三方框架spring cloud, du ...

  5. 2021年哪个低代码平台更值得关注?T媒体盘点国内主流低代码厂商

    2020年圣诞前夜,国内知名创投科技媒体T媒体旗下的T研究发布了2020中国低代码平台指数测评报告.报告除了对国内低代码行业现状进行总结外,还对主流低代码厂商的市场渗透和曝光进行测评. 报告认为,低代 ...

  6. OpenDataV低代码平台增加自定义属性编辑

    上一篇我们讲到了怎么在OpenDataV中添加自己的组件,为了让大家更快的上手我们的平台,这一次针对自定义属性编辑,我们再来加一篇说明.我们先来看一下OpenDataV中的属性编辑功能. 当我们拖动一 ...

  7. vivo 游戏中心低代码平台的提效秘诀

    作者:vivo 互联网服务器团队- Chen Wenyang 本文根据陈文洋老师在"2022 vivo开发者大会"现场演讲内容整理而成.公众号回复[2022 VDC]获取互联网技术 ...

  8. 干货!可以使用低代码平台代替Excel吗?

    低代码开发平台可以代替Excel?不用惊讶,答案是肯定的,而且,低代码开发平台可以完全代替Excel.例如Zoho Creator低代码平台,可以围绕数据存储.管理和创建工作流程.期间不需要IT人员介 ...

  9. 分析师机构发布中国低代码平台现状分析报告,华为云AppCube为数字化转型加码

    摘要:Forrester指出,中国企业数字化转型过程中,有58%的决策者正在采用低代码工具进行软件构建,另有16%的决策者计划采用低代码. 华为消息,知名研究与分析机构Forrester Resear ...

  10. 开源低代码平台开发实践二:从 0 构建一个基于 ER 图的低代码后端

    前后端分离了! 第一次知道这个事情的时候,内心是困惑的. 前端都出去搞 SPA,SEO 们同意吗? 后来,SSR 来了. 他说:"SEO 们同意了!" 任何人的反对,都没用了,时代 ...

随机推荐

  1. 力扣只写函数好怪WWWW,转战洛谷!

    转战!目标刷完官方推荐题集

  2. 《ASP.NET Core 与 RESTful API 开发实战》-- (第9章)-- 读书笔记(上)

    第 9 章 测试和文档 9.1 测试 测试是软件生命周期中的一个非常重要的阶段,对于保证软件的可靠性具有极其重要的意义 常见的测试方法有很多,根据不同的维度,可以把测试方法分为不同的类别 从观察结构的 ...

  3. 《ASP.NET Core 微服务实战》-- 读书笔记(第6章)

    第 6 章 事件溯源与 CQRS 在本章,我们来了解一下随着云平台一同出现的设计模式 我们先探讨事件溯源和命令查询职责分离(CQRS)背后的动机与哲学 事件溯源简介 事实由事件溯源而来 我们大脑就是一 ...

  4. Blazor OIDC 单点登录授权实例5 - 独立SSR App (net8 webapp ) 端授权

    目录: OpenID 与 OAuth2 基础知识 Blazor wasm Google 登录 Blazor wasm Gitee 码云登录 Blazor OIDC 单点登录授权实例1-建立和配置IDS ...

  5. STM32 printf 方法重定向到串口UART

    在嵌入式系统中调试代码是很麻烦的一件事, 如果能方便地输出调试信息(与调试者交互), 能使极大加快问题排查的过程. 串口在嵌入式领域是一个比较重要的通讯接口. 因为没有显示设备, 在单片机的程序里调用 ...

  6. 【Unity3D】使用GL绘制线段

    1 前言 ​ 线段渲染器LineRenderer.拖尾TrailRenderer.绘制物体表面三角形网格从不同角度介绍了绘制线段的方法,本文再介绍一种新的绘制线段的方法:使用 GL 绘制线段. ​ G ...

  7. Js中Math对象

    Js中Math对象 Math是一个内置对象,它拥有一些数学常数属性和数学函数方法,Math用于Number类型,其不支持BigInt. 描述 Math不是一个函数对象,也就是说Math不是一个构造器, ...

  8. RCE代码执行漏和命令执行漏洞

    前置知识: 漏洞检测: 在了解漏洞概念前,应该先知道一下这个漏洞如何检测的,我们应该或多或少听过白盒测试(白盒),黑盒测试(黑盒). 白盒测试: 白盒测试是对源代码和内部结构的测试,测试人员是可以知道 ...

  9. Innodb存储引擎之锁

    目录 一.概述 二.lock 与 latch 三.Innodb存储引擎中的锁 锁 一致性非锁定读 一致性锁定读 自增长与锁 外键与锁 四.锁的算法 锁的算法 Phantom Problem 幻读问题 ...

  10. SpringCloud SpringBoot 组件使用:使用Nacos作为服务的注册中心和配置中心

    基础篇 一.什么是Nacos? 官方介绍是这样的: Nacos 致力于帮助您发现.配置和管理微服务.Nacos 提供了一组简单易用的特性集,帮助您实现动态服务发现.服务配置管理.服务及流量管理. Na ...