React低代码编辑器开发实践(一)

2025年4月26日
约 6 分钟阅读时间
By 麦兜九天 & Tianyang Wang

目录

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 -p

2. 基础配置

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编辑功能,关键成果包括-

  1. 数据驱动架构-基于组件树形JSON结构,所有操作最终映射为对JSON的修改
  2. 状态管理-使用Zustand实现组件树的增删改查,配合递归算法处理树形结构
  3. 组件渲染-通过组件配置映射,将JSON动态渲染为React组件
  4. 拖拽交互-基于React DnD实现组件拖拽添加,支持嵌套结构

后续将在此基础上实现组件选中、属性编辑、删除等功能,进一步完善编辑器能力。