Article
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-editorcd lowcode-editor
# 安装依赖npm installnpm install zustand allotment react-dnd react-dnd-html5-backend antdnpm install -D tailwindcss postcss autoprefixernpx 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实现三栏布局(物料区、画布区、属性区)-
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为例)-
export function Header() { return <div className="font-bold text-lg">低代码编辑器</div>;}三、核心状态管理设计
1. 组件数据结构定义
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.tsxexport 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-
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为例-
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;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组件-
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. 物料区拖拽源
为物料区组件添加拖拽能力-
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> );}物料区列表-
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. 画布区拖拽目标
封装通用拖拽接收钩子,支持组件接收子组件-
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结构,验证拖拽操作对数据的影响-
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实现组件拖拽添加,支持嵌套结构
后续将在此基础上实现组件选中、属性编辑、删除等功能,进一步完善编辑器能力。