前言
为什么要写这篇文章?最近在公司中参与开发低代码平台项目,在学习过程中发现很多成熟的低代码平台业务包袱和设计较重不利于自己对于核心逻辑的理解,由此希望搭建一个迷你版的低代码平台
加深自己的理解,也帮助更多人更轻松的去学习低代码平台的搭建。代码开源,有需要的朋友自取😁!本章我们将实现效果如下:
1、能从左侧物料面板将组件拖拽至编辑区域渲染
2、编辑区域组件能在编辑区域内自由拖拽
实现左侧物料面板拖拽
实现效果
属性方法介绍
1、draggable
draggable
属性是HTML5中的一个属性,用于标识元素是否可以被拖动。
2、dragStart
dragstart
事件是在拖动操作开始时触发的事件。具体来说,它是在用户开始拖动一个可拖动元素时触发的。
3、dragEnter
dragenter
事件是在拖动元素进入一个可放置区域时触发的事件。它是拖放操作中的一个事件,用于标识拖动元素进入目标区域。
4、dragOver
dragover
事件是在拖动元素在一个可放置区域上移动时持续触发的事件。它可以用来监听拖动元素在目标区域上的移动,并根据需要执行相应的操作。
5、dragLeave
dragLeave
是一个拖放事件,它在拖动元素离开一个可放置区域时触发。当拖动元素从一个可放置区域拖出时,浏览器会触发dragLeave
事件。
6、drop
drop
事件是一个拖放事件,在拖动元素释放时触发。它用于处理拖放操作的最终放置行为。
实现过程
1、将每个组件 drabggble 属性设置 true 标识元素可拖拽,并监听 dragStart 事件在拖拽开始时记录被拖拽组件信息,代码如下所示:
javascript
import './index.css';
import registerConfig, { IComponent } from './registerConfig';
import { GlobalContext } from '../../store';
import React from 'react';
let MaterialPanel = (_props: IProps) => {
const { setCurrentMaterial } = React.useContext(GlobalContext);
// 监听 dragStart 事件在拖拽开始时记录被拖拽组件信息
const handleDragStart = (component: IComponent) => {
setCurrentMaterial(component);
};
return (
<div>
{registerConfig.componentList.map(component => (
<div key={component.type} className="editor-left-item">
<span>{component.label}</span>
<div draggable onDragStart={() => handleDragStart(component)}>
{component.preview()}
</div>
</div>
))}
</div>
);
};
interface IProps {}
export { MaterialPanel };
2、组件渲染区域监听元素进入事件
- dragEnter:当拖拽元素进入渲染区域时改变光标手势,表示元素进入渲染区域
- dragLeave:当拖拽元素离开渲染区域时恢复正常光标,表示元素离开渲染区域
- dragOver:取消默认的拖放行为,如阻止打开文件链接
- drop:当放置元素时生成元素配置加入渲染区域渲染
代码实现如下:
javascript
import './index.css';
import React, { useRef } from 'react';
import { GlobalContext } from '../../store';
import { Block } from './Block';
import { IComponent } from '../MaterialPanel/registerConfig';
//私有常量
//可抽离的逻辑处理函数/组件
/**
* 【组件功能】
*
* 【应用模块】
*
*/
let CanvasArea = (_props: IProps) => {
//变量声明、解构
const { currentMaterial, setCurrentMaterial, setCurrentSchema, currentSchema } = React.useContext(GlobalContext);
const ref = useRef(null);
//组件状态
//网络IO
//数据转换
//逻辑处理函数
//当拖拽元素进入渲染区域时改变光标手势,表示元素进入渲染区域
const handleDragEnter = (event: { dataTransfer: { dropEffect: string } }) =>
(event.dataTransfer.dropEffect = 'move');
//当拖拽元素离开渲染区域时恢复正常光标,表示元素离开渲染区域
const handleDragLeave = (event: { dataTransfer: { dropEffect: string } }) =>
(event.dataTransfer.dropEffect = 'none');
//取消默认的拖放行为,如阻止打开文件链接
const handleDragOver = (event: { preventDefault: () => any }) => event.preventDefault();
//当放置元素时生成元素配置加入渲染区域渲染
const handleDrop = (event: any) => {
const { offsetX, offsetY } = event.nativeEvent;
const config = {
type: currentMaterial?.type,
alignCenter: true, // 表示拖拽到画布后,基于鼠标位置居中展示
focus: false,
style: {
width: undefined,
height: undefined,
left: offsetX,
top: offsetY,
zIndex: 1,
},
};
currentSchema.blocks.push(config);
setCurrentSchema({ ...currentSchema });
setCurrentMaterial(null);
};
return (
<div
id="canvas-container"
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
style={{ ...currentSchema.container }}
ref={ref}
>
{currentSchema.blocks.map((block: IComponent, index: number) => (
<Block key={index} block={block} onMouseDown={e => handleMouseDown(e, index)} parentRef={ref} />
))}
</div>
);
};
interface IProps {}
export { CanvasArea };
元素拖放至编辑区域后是如何完成渲染的?
单独看下这个拖放监听事件代码:
javascript
//当放置元素时将元素加入渲染区域渲染
const handleDrop = (event: any) => {
// 1、 获取元素位置
const { offsetX, offsetY } = event.nativeEvent;
// 2、生成组件配置
const config = {
type: currentMaterial?.type,
focus: false,
style: {
left: offsetX,
top: offsetY,
zIndex: 1,
},
};
// 3、将组件配置加入画布配置中
currentSchema.blocks.push(config);
// 4、更新画布配置
setCurrentSchema({ ...currentSchema });
// 5、清空当前选中的物料组件
setCurrentMaterial(null);
};
渲染区域是根据一份全局的配置文件(称为schema) 进行渲染的,我们拖放元素到渲染区域时做的事情就是将元素信息写入schema,然后触发渲染区域重新渲染。此时实现效果如下:
细心的🧑🎓可能会发现,元素存在偏移,因为我们是以鼠标相对容器的位置作为在元素在容器的位置,如果要正确渲染应该获取元素的左上角相对于容器的位置作为元素的偏移位置
如何获取元素左上角相对于容器的位置?
通过鼠标事件回调参数,我们可以获取到鼠标相对于元素左边缘的位置信息,因此我们只需要在开始拖拽元素时记录鼠标相对于元素的左边缘位置信息,还有在鼠标放置元素时元素相对于容器的左边缘的位置信息,这两个值相减,即可得到最终的偏移量,代码实现如下:
javascript
// 拖拽开始
const handleDragStart = (e: any, component: IComponent) => {
// 开始拖拽元素记录鼠标相对于元素的左边缘位置信息
const { offsetX, offsetY } = e.nativeEvent;
component.offsetInfo = {
offsetX,
offsetY,
};
component.element = e?.target ?? null;
setCurrentMaterial(component);
};
// 拖拽放置
const handleDrop = (event: any) => {
// 1、 获取元素位置
// 放置元素时元素相对于容器的左边缘的位置信息
const { offsetX, offsetY } = event.nativeEvent;
// 开始拖拽元素时鼠标相对于元素的左边缘位置信息
const offsetInfo = currentMaterial?.offsetInfo ?? { offsetX: 0, offsetY: 0 };
// 两个值相减即可得修正后元素偏移量
const left = offsetX - offsetInfo.offsetX;
const top = offsetY - offsetInfo.offsetY;
// 2、生成组件配置
const config = {
type: currentMaterial?.type,
focus: false,
style: {
left,
top,
zIndex: 1,
},
};
// 3、将组件配置加入画布配置中
currentSchema.blocks.push(config);
// 4、更新画布配置
setCurrentSchema({ ...currentSchema });
// 5、清空当前选中的物料组件
setCurrentMaterial(null);
};
如何保障元素在正确的位置上渲染且不超出容器位置?
对元素偏移位置做限制,偏移量不可超过容器大小,代码如下所示:
javascript
//当放置元素时将元素加入渲染区域渲染
const handleDrop = (event: any) => {
// 1、 获取元素位置
const { offsetX, offsetY } = event.nativeEvent;
const { clientWidth, clientHeight } = currentMaterial?.element ?? { clientWidth: 0, clientHeight: 0 };
const offsetInfo = currentMaterial?.offsetInfo ?? { offsetX: 0, offsetY: 0 };
// 修正元素偏移量
const left = offsetX - offsetInfo.offsetX;
const top = offsetY - offsetInfo.offsetY;
// 元素可偏移的最大位置
const maxLeft = currentSchema.container.width - clientWidth;
const maxTop = currentSchema.container.height - clientHeight;
// 限制元素位置不超出渲染区域
const curLeft = Math.max(Math.min(left, maxLeft), 0);
const curTop = Math.max(Math.min(top, maxTop), 0);
// 2、生成组件配置
const config = {
type: currentMaterial?.type,
focus: false,
style: {
left: curLeft,
top: curTop,
zIndex: 1,
},
};
// 3、将组件配置加入画布配置中
currentSchema.blocks.push(config);
// 4、更新画布配置
setCurrentSchema({ ...currentSchema });
// 5、清空当前选中的物料组件
setCurrentMaterial(null);
};
编辑区域组件实现自由拖拽
实现效果
属性方法介绍
1、mousedown
mousedown
是一个鼠标事件,它在用户按下鼠标按钮时触发。
注:click是按下并释放才触发,mousedown是按下时就触发
2、mousemove
mousemove
是一个鼠标事件,它在用户移动鼠标时触发。
3、mouseup
mouseup
是一个鼠标事件,它在用户抬起鼠标按钮时触发。
实现过程
1、监听渲染区域元素的鼠标按下事件,在按下时保留元素的起始位置并监听鼠标的移动和抬起事件
javascript
let Block = (_props: IProps) => {
const handleMouseDown = (e: any) => {
onMouseDown(e);
_onMouseDown(e);
};
const _onMouseDown = (e: { clientX: number; clientY: number }) => {
// 记录起始位置
const initialPlace = {
startX: e.clientX,
startY: e.clientY,
};
// 动态绑定移动事件和鼠标抬起事件
document.addEventListener('mousemove', _blockMouseMove);
document.addEventListener('mouseup', blockMouseUp);
};
return (
<div
className={`editor-block ${block.focus ? 'editor-block-focus' : ''}`}
style={blockStyle}
ref={blockRef}
onMouseDown={handleMouseDown}
>
{RenderComponent}
</div>
);
};
export { Block };
mousemove、mouseup事件为什么要动态绑定?
在渲染区域节点较多的情况下可以减少绑定事件内存开销,跟react事件机制的思路一致
2、利用鼠标移动事件在移动的时候实时更新元素位置,代码实现如下:
javascript
// 在移动的时候实时更新元素位置
const blockMouseMove = (initialPlace: { startX: number; startY: number }) => {
return (e: { clientX: number; clientY: number }) => {
const { clientX, clientY } = e;
// 计算移动的距离
const movX = clientX - initialPlace.startX;
const movY = clientY - initialPlace.startY;
if (parentRef.current && blockRef.current) {
const { clientWidth: pW, clientHeight: pH } = parentRef.current;
const { clientWidth: bW, clientHeight: bH } = blockRef.current;
// 限制移动范围在渲染区域内 start
const maxX = pW - bW;
const maxY = pH - bH;
// @ts-ignore
let newLeft = blockStyle.left + movX;
// @ts-ignore
let newTop = blockStyle.top + movY;
newLeft = Math.max(0, Math.min(newLeft, maxX));
newTop = Math.max(0, Math.min(newTop, maxY));
// 限制移动范围在渲染区域内 end
// 更新位置
// @ts-ignore
block.style.top = newTop;
// @ts-ignore
block.style.left = newLeft;
// 更新视图
setCurrentSchema({ ...currentSchema });
}
};
};
3、在抬起的时候移除移动和抬起事件,代码实现如下:
javascript
// 在抬起的时候移除移动和抬起事件
const blockMouseUp = () => {
document.removeEventListener('mousemove', _blockMouseMove);
document.removeEventListener('mouseup', blockMouseUp);
};