# react-lowcode-editor
**Repository Path**: huangshaomo/react-lowcode-editor
## Basic Information
- **Project Name**: react-lowcode-editor
- **Description**: react 低代码编辑器
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 0
- **Created**: 2024-10-12
- **Last Updated**: 2025-03-11
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
低代码的核心原理是围绕 JSON 进行操作,通过 JSON 来描述页面,并将 JSON 与组件建立映射,来渲染页面
### 核心实现
1. 通过 JSON 描述页面
```tsx
export const useComponentStore = defineStore((set, get) => ({
components: [
{
id: "1",
name: "Page",
desc: "页面",
props: {},
children: [],
},
],
}));
```
2. 将 JSON 与我们编写的组件建立映射
```tsx
import Button from "../materials/Button"
import Container from "../materials/Container"
import Page from "../materials/Page"
export const useComponentConfigStore = defineStore((set, get) => ({
componentConfigs: {
// 这里是组件名与组件的映射关系
Button: {
name: "Button",
desc: "按钮",
defaultProps: {
type: "primary",
text: "button"
},
component: Button, // 这里是需要映射的组件
setter: [...],
stylesSetter: [...]
},
}
}))
```
3. 声明一个渲染函数,用户遍历 components,取出对应的映射组件和默认配置来渲染页面
```tsx
// 深度遍历 components, 根据组件名渲染对应的组件
function renderComponents(components: Component[]): React.ReactNode {
return components.map((component) => {
const config = componentConfigs[component.name];
if (!config?.component) return null;
return React.createElement(
config.component,
{
key: component.id,
id: component.id,
name: component.name,
styles: component.styles,
...config.defaultProps,
...component.props,
},
renderComponents(component.children || [])
);
});
}
```
### zustand + immer 实现全局不可变数据仓库
```shell
npm install zustand immer
```
### allotment 实现可调整的布局
```shell
npm install react-alotment
```
实现步骤
1. 在 main.tsx 引入组件
```jsx
import { HTML5Backend } from "react-dnd-html5-backend";
import { DndProvider } from "react-dnd";
;
```

2. 在要拖拽的组件上添加 useDrag, 在放置的组件上添加 useDrop
### 拖拽实现
```shell
npm install react-dbd react-dnd-html5-backend
```
### 4. 画布区 hover 组件时,出现时高亮效果

实现思路:
1. 给每个渲染组件都加上唯一的 id 标识
2. 给画布区添加 mouseover 事件
3. 根据移动时鼠标坐标位置,找到最近带有 id 的元素
4. 获取这个元素的位置信息,就是高亮框的位置信息
### 5. 画布区 Click 组件时,出现操作栏

实现思路:
1. 给画布区添加 click 事件
2. 根据点击时鼠标坐标位置,找到最近带有 id 的元素
3. 由于点击 id 除了用于展示操作栏,还需要显示对应组件属性配置面板,因此把 id 存储到全局中
难点:
1. 由于遮罩层的层级是固定的,高于组件,因此一旦遮罩层出现,就无法点击组件,因此需要动态调整遮罩层的层级
> 解决方法则是使用 pointerEvents: 'none', 让遮罩层不拦截鼠标事件,但给操作栏添加 pointerEvents: 'auto',让操作栏可以拦截鼠标事件
### 6. 画布区 Click 组件时, 出现组件属性配置面板

实现思路:
1. 基础属性来自 components, 用于展示组件的基础信息(ID, name, desc)
2. 配置属性来自 setter, 用于渲染可交互的表单组件,并与 defaultProps 里的属性建立绑定关系
```tsx
componentConfigs: {
Button: {
name: "Button",
desc: "按钮",
defaultProps: {
type: "primary",
text: "button"
},
component: Button,
// 用于渲染表单组件
setter: [
{
name: 'type', // 值对应 defaultProps 中的 type 键
label: '按钮类型',
type: 'select',
options: [
{label: '主按钮', value: 'primary'},
{label: '次按钮', value: 'default'},
],
},
{
name: 'text', // 值对应 defaultProps 中的 text 键
label: '文本',
type: 'input',
}
]
},
}
```
3. 建立组件配置面板与 antd 组件的映射关系
```tsx
function renderFormElement(setter: ComponentSetter) {
const { type, options } = setter;
switch (type) {
case "input":
return ;
case "number":
return ;
case "select":
return ;
}
}
```
3. 通过 useEffect 给表单初始化数据(初始化和改变组件时)
```tsx
// curComponent 变化的时候,把 props 设置到表单用于回显数据:
useEffect(() => {
console.log("执行了");
const data = form.getFieldsValue();
form.setFieldsValue({ ...data, ...curComponent.props });
}, [curComponent]);
```
4. 通过监听表单的 onValueChange 事件,把数据更新到 curComponent 中
```tsx
function handleValueChange(newProps: ComponentConfig) {
if (curComponentId) {
updateComponentProps(curComponentId, newProps);
}
}
return (
);
```
5. curComponent 变化时,又会触发组件的更新,形成双向联动
#### 样式配置面包
1. 给 component 新增一个 styles 属性,用于存储组件的 CSS 样式,并新增一个 updateComponentStyles 方法,用于更新样式
2. 后面的步骤和属性配置面板一样
##### 添加代码编辑器
添加这个只是为了方便书写 css 代码
实现思路:
1. 安装 monaco-eidtor/react
```shell
npm install --save @monaco-editor/react
npm install --save monoco-editor
```
2. 初始化或点击组件时,取出组件的 styles 数据填充进表单和代码编辑器中
3. 记录代码编辑器编辑事件,将得到的 CSS 字符串转为 CSS 对象,存入到组件的 styles 属性中
TIPS:由于表单组件和代码编辑器共用 styles 属性,新增数据都采用追加模式会导致已添加的数据无法再被移除,代码编辑器是需要能删除数据的,所以得采用覆盖更新,同时覆盖前需先合并表单的数据,避免表单的数据丢失
```js
updateComponentStyles(
curComponentId,
{ ...form.getFieldsValue(), ...cssObj },
true
);
```
### 大纲、源码、预览实现
#### 大纲实现

实现思路:
1. 遍历 component JSON,使用 Tree 树型控件把 desc 属性渲染出来
2. 每个节点点击时,高亮对应的区域即可
#### 源码实现
这个实现就更简单了,直接把 component JSON 数据用代码编辑器展示
出来即可
#### 预览实现
预览和编辑最大的不同是只能看不能改
实现思路:
1. 每个组件定义两种状态,编辑的叫(dev), 预览的叫(prod)
2. 定义两个编辑器组件,之前定义编辑器组件的叫 EditArea,再定义一个 Previwe 预览组件
3. 编辑组件负责渲染 dev 组件,预览组件负责渲染 prod 组件
4. 然后全局定义个 Mode,有 Edit 和 Preview 两种状态,默认为 Edit
5. Edit 状态就渲染编辑组件 Preview 就渲染预览组件
### 事件绑定
实现思路:
1. 首先在组件配置(component-config)中新增一个 events 属性,用于配置组件支持的事件
```tsx
Button: {
// ...
events: [
{
name: 'onClick',
label: '点击事件'
},
{
name: 'onDoublelClick',
label: '双击事件'
}
]
},
```
2. 接着将配置的 events 属性渲染成表单
3. 用户与事件表单交互,选择指定事件后插入到组件(component)的 props 属性中,数据结构类似如下 `props:{ onClick: {type: "goToLink", url: "xxx"}}`
4. 当用户进入预览状态时,将 JSON 事件数据转为组件的具体事件处理函数 `{ onclick: () => {}, onDoubleClick: () => {}} `
5. 最后将处理后的 props 传递给预览状态组件(Button、Container...)去绑定上事件
#### 自定义 jS
大部分与上面的步骤一样,只是在转为具体的事件处理函数时,需要将用户输入的 JS 代码转为函数,然后执行函数即可
```tsx
case 'customJs':
// context 是参数名, action.code 是函数体
const func = new Function("context", action.code)
func({
name: component.name,
props: component.props,
ShowMessage(content: string) {
message.success(content)
}
})
break;
```
### 组件联动
组件联动的本质就是在组件中调用属于另一个组件的方法
实现思路:
1. 组件通过 forwarfRef 和 useImperativeHandle 将组件的方法暴露出去
2. 在组件配置(component-config)中新增一个 methods 字段, 把需暴露的方法名在这里定义一遍 `[{name: "open", label: "打开弹窗"},...]`
3. 遍历所有的组件(components)和 methods,渲染成表单 Select
4. 用户选择指定组件方法,形成组件参数, 如 `{type: "componentMedhod", config: {conponentId: "xxx", methodName: "open"}}`
5. 在预览模式时,收集所有组件实例(ref), 根据组件参数,找到对应的组件实例,执行对应的方法即可
### 组件拖拽排序
组件拖拽排序就是调整组件的顺序,可以是
- 先删除旧的再新增
- 直接移动
实现思路:
1. 给所有物料元素添加拖拽功能
2. 为了区分是新增还是移动,需要传递一个 dragType,同时带上当前组件 id `type: Button, items: {type:'Button', dragType: 'move', id: 'xxx'}`
3. 放置时,根据 dragType 区分
4. 当为移动时
### 新增 Table
实现思路:
1. 分别定义 Table 和 TableColumn 组件(Table 组件是负责接收 TableColumn 组件和数据的容器)
2. Table 字段由 TableColumn 拖拽生成
```tsx
const columns = useMemo(() => {
return React.Children.map(children, (item: any) => ({
title: (
{item.props?.title}
),
dataIndex: item.props?.dataIndex,
key: item,
}));
}, [children]);
```
3. 数据由通过 URL 获取,在 `Preview` 模式展示出来
### 新增 Form
1. 分别定义 Form 和 FormItem 组件(Form 组件是负责接收 FormItem 组件和数据的容器)
2. Form 字段及数据由 FormItem 拖拽生成
```tsx
const formItems = useMemo(() => {
return React.Children.map(children, (item: any) => {
return {
label: item.props?.label,
name: item.props?.name,
type: item.props?.type,
id: item.props?.id,
rules: item.props?.rules,
};
});
}, [children]);
```
3. 表单暴露一个提交事件,通过按钮触发表单实例暴露的 submit 事件
4. 表单验证通过后,会自动触发表单的 onFinish 事件执行,