React低代码编辑器开发实践(一)-核心JSON编辑功能实现
低代码编辑器的核心在于通过可视化操作生成和编辑JSON数据结构,本文作为系列第一篇,将聚焦于如何实现基于React技术栈的JSON编辑核心功能,包括项目搭建、数据结构设计、组件渲染及拖拽交互。
一、项目核心原理与技术栈
核心原理
低代码编辑器的本质是对组件树形JSON结构的可视化操作,所有交互(拖拽、属性编辑、删除等)最终都映射为对JSON的增删改查-
{
"components": [
{
"id": 1,
"name": "Page",
"props": {},
"children": [
{
"id": 2,
"name": "Container",
"props": {},
"children": []
}
]
}
]
}技术栈选择
- 前端框架-React 18(组件化开发核心)
- 构建工具-Vite(快速开发体验)
- 样式方案-Tailwind CSS(原子化样式,快速布局)
- 状态管理-Zustand(轻量高效,适合管理组件树状态)
- 拖拽库-React DnD(处理组件拖拽逻辑)
- 布局组件-Allotment(实现可拖拽调整的分栏布局)
- UI组件-Ant Design(基础UI组件支持)
二、项目初始化与基础布局
1. 项目搭建
# 创建项目
npx create-vite lowcode-editor
cd lowcode-editor
# 安装依赖
npm install
npm install zustand allotment react-dnd react-dnd-html5-backend antd
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p2. 基础配置
Tailwind配置(tailwind.config.js)-
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}入口文件配置(main.tsx)-
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
// 引入拖拽后端支持
import { HTML5Backend } from 'react-dnd-html5-backend'
import { DndProvider } from 'react-dnd'
ReactDOM.createRoot(document.getElementById('root')!).render(
<DndProvider backend={HTML5Backend}>
<App />
</DndProvider>
)3. 编辑器布局实现
使用Allotment实现三栏布局(物料区、画布区、属性区)-
// src/editor/index.tsx
import { Allotment } from "allotment";
import 'allotment/dist/style.css';
import { Header } from "./components/Header";
import { EditArea } from "./components/EditArea";
import { Setting } from "./components/Setting";
import { Material } from "./components/Material";
export default function LowcodeEditor() {
return (
<div className='h-[100vh] flex flex-col'>
{/* 头部区域 */}
<div className='h-[60px] flex items-center border-b border-gray-300 px-4'>
<Header />
</div>
{/* 主内容区(三栏布局) */}
<Allotment className="flex-1">
{/* 左侧物料区 */}
<Allotment.Pane preferredSize={240} maxSize={300} minSize={200}>
<Material />
</Allotment.Pane>
{/* 中间画布区 */}
<Allotment.Pane>
<EditArea />
</Allotment.Pane>
{/* 右侧属性区 */}
<Allotment.Pane preferredSize={300} maxSize={500} minSize={280}>
<Setting />
</Allotment.Pane>
</Allotment>
</div>
);
}基础组件实现(以Header为例)-
// src/editor/components/Header.tsx
export function Header() {
return <div className="font-bold text-lg">低代码编辑器</div>;
}三、核心状态管理设计
1. 组件数据结构定义
// src/editor/stores/components.tsx
import { create } from 'zustand';
// 组件类型定义
export interface Component {
id: number; // 唯一标识
name: string; // 组件名称(如"Button"、"Container")
props: Record<string, any>; // 组件属性
children?: Component[]; // 子组件
parentId?: number; // 父组件ID
}
// 状态与操作定义
interface State {
components: Component[]; // 组件树
}
interface Action {
addComponent: (component: Component, parentId?: number) => void; // 添加组件
deleteComponent: (componentId: number) => void; // 删除组件
updateComponentProps: (componentId: number, props: any) => void; // 更新属性
}2. 状态管理实现
使用Zustand实现组件树的增删改查-
// 续上-stores/components.tsx
export const useComponentsStore = create<State & Action>((set, get) => ({
// 初始组件树(默认包含Page根组件)
components: [
{
id: 1,
name: 'Page',
props: {},
}
],
// 添加组件
addComponent: (component, parentId) => set((state) => {
if (parentId) {
// 查找父组件并添加到children
const parent = getComponentById(parentId, state.components);
if (parent) {
parent.children ? parent.children.push(component) : parent.children = [component];
component.parentId = parentId;
}
return { components: [...state.components] };
}
// 无父组件时直接添加到根
return { components: [...state.components, component] };
}),
// 删除组件
deleteComponent: (componentId) => {
const component = getComponentById(componentId, get().components);
if (component?.parentId) {
const parent = getComponentById(component.parentId, get().components);
if (parent?.children) {
parent.children = parent.children.filter(item => item.id !== componentId);
set({ components: [...get().components] });
}
}
},
// 更新组件属性
updateComponentProps: (componentId, props) => set((state) => {
const component = getComponentById(componentId, state.components);
if (component) {
component.props = { ...component.props, ...props };
}
return { components: [...state.components] };
}),
}));
// 工具函数-递归查找组件
export function getComponentById(id: number, components: Component[]): Component | null {
for (const component of components) {
if (component.id === id) return component;
if (component.children?.length) {
const result = getComponentById(id, component.children);
if (result) return result;
}
}
return null;
}四、组件渲染与物料管理
1. 组件配置映射
为了将JSON中的组件名称映射到实际React组件,创建组件配置Store-
// src/editor/stores/component-config.tsx
import { create } from 'zustand';
import Page from '../materials/Page';
import Container from '../materials/Container';
import Button from '../materials/Button';
// 组件配置类型
export interface ComponentConfig {
name: string;
defaultProps: Record<string, any>; // 默认属性
component: React.ComponentType; // 组件实例
}
export const useComponentConfigStore = create((set) => ({
// 已注册的组件配置
componentConfig: {
Page: {
name: 'Page',
defaultProps: {},
component: Page
},
Container: {
name: 'Container',
defaultProps: {},
component: Container
},
Button: {
name: 'Button',
defaultProps: { type: 'primary', text: '按钮' },
component: Button
}
},
// 注册新组件
registerComponent: (name: string, config: ComponentConfig) => set((state) => ({
componentConfig: { ...state.componentConfig, [name]: config }
}))
}));2. 基础组件实现
以Container和Button为例-
// src/editor/materials/Container/index.tsx
import { CommonComponentProps } from '../../interface';
import { useMaterailDrop } from '../../hooks/useMaterialDrop';
const Container = ({ id, children }: CommonComponentProps) => {
// 使用自定义拖拽钩子,支持接收子组件
const { canDrop, drop } = useMaterailDrop(['Button', 'Container'], id);
return (
<div
ref={drop}
className={`min-h-[100px] p-5 ${canDrop ? 'border-2 border-blue-500' : 'border border-gray-300'}`}
>
{children}
</div>
);
};
export default Container;// src/editor/materials/Button/index.tsx
import { Button as AntdButton } from 'antd';
import { CommonComponentProps } from '../../interface';
const Button = ({ text, type }: CommonComponentProps) => {
return <AntdButton type={type}>{text}</AntdButton>;
};
export default Button;3. 画布区渲染组件树
通过递归渲染将JSON组件树转换为React组件-
// src/editor/components/EditArea.tsx
import { useComponentsStore } from '../../stores/components';
import { useComponentConfigStore } from '../../stores/component-config';
import { Component } from '../../stores/components';
export function EditArea() {
const { components } = useComponentsStore();
const { componentConfig } = useComponentConfigStore();
// 递归渲染组件树
const renderComponents = (comps: Component[]): React.ReactNode => {
return comps.map((component) => {
const Config = componentConfig[component.name];
if (!Config?.component) return null;
return (
<Config.component
key={component.id}
id={component.id}
name={component.name}
{...Config.defaultProps}
{...component.props}
>
{renderComponents(component.children || [])}
</Config.component>
);
});
};
return (
<div className="h-full p-4 overflow-auto bg-gray-50">
{renderComponents(components)}
</div>
);
}五、拖拽功能实现
1. 物料区拖拽源
为物料区组件添加拖拽能力-
// src/editor/components/MaterialItem/index.tsx
import { useDrag } from 'react-dnd';
export interface MaterialItemProps {
name: string;
}
export function MaterialItem({ name }: MaterialItemProps) {
// 拖拽配置
const [, drag] = useDrag({
type: name, // 拖拽类型(与组件名一致)
item: { type: name } // 传递的数据
});
return (
<div
ref={drag}
className="border border-dashed border-gray-400 py-2 px-3 m-2 inline-block cursor-move hover:bg-gray-100"
>
{name}
</div>
);
}物料区列表-
// src/editor/components/Material/index.tsx
import { useComponentConfigStore } from '../../stores/component-config';
import { MaterialItem } from '../MaterialItem';
export function Material() {
const { componentConfig } = useComponentConfigStore();
const components = Object.values(componentConfig);
return (
<div className="p-4">
<h3 className="font-semibold mb-3">组件物料</h3>
<div>
{components.map(item => (
<MaterialItem key={item.name} name={item.name} />
))}
</div>
</div>
);
}2. 画布区拖拽目标
封装通用拖拽接收钩子,支持组件接收子组件-
// src/editor/hooks/useMaterialDrop.ts
import { useDrop } from 'react-dnd';
import { useComponentsStore } from '../stores/components';
import { useComponentConfigStore } from '../stores/component-config';
export function useMaterailDrop(accept: string[], id: number) {
const { addComponent } = useComponentsStore();
const { componentConfig } = useComponentConfigStore();
return useDrop(() => ({
accept, // 可接收的组件类型
drop: (item: { type: string }, monitor) => {
// 避免重复处理
if (monitor.didDrop()) return;
// 添加新组件
const config = componentConfig[item.type];
addComponent({
id: Date.now(), // 用时间戳作为临时ID
name: item.type,
props: { ...config.defaultProps }
}, id); // 父组件ID
},
collect: (monitor) => ({
canDrop: monitor.canDrop() // 是否可放置
})
}));
}六、功能验证与总结
功能验证
通过属性区展示JSON结构,验证拖拽操作对数据的影响-
// src/editor/components/Setting/index.tsx
import { useComponentsStore } from '../../stores/components';
export function Setting() {
const { components } = useComponentsStore();
return (
<div className="p-4 overflow-auto h-full">
<h3 className="font-semibold mb-3">组件数据</h3>
<pre className="text-sm bg-gray-100 p-2 rounded">
{JSON.stringify(components, null, 2)}
</pre>
</div>
);
}此时,拖拽物料区组件到画布区的Page或Container中,会实时在属性区看到JSON结构的变化,验证了核心编辑功能的正确性。
总结
本文实现了低代码编辑器的核心JSON编辑功能,关键成果包括-
- 数据驱动架构-基于组件树形JSON结构,所有操作最终映射为对JSON的修改
- 状态管理-使用Zustand实现组件树的增删改查,配合递归算法处理树形结构
- 组件渲染-通过组件配置映射,将JSON动态渲染为React组件
- 拖拽交互-基于React DnD实现组件拖拽添加,支持嵌套结构
后续将在此基础上实现组件选中、属性编辑、删除等功能,进一步完善编辑器能力。