引言
你是否曾梦想过像搭积木一样开发应用?低代码平台拖拽组件,让前端开发者也能体验“所见即所得”的快乐!(◕‿◕)
实现原理
核心思路:物料区组件通过拖拽,动态植入画布区的 JSON 组件树,实现可视化页面搭建。
- 组件仓库(componentsStore)负责管理整个页面的组件树,支持增删改查。
- 组件配置仓库(componentConfigStore)记录每个组件的元数据与渲染逻辑。
- 递归渲染:renderComponents 函数利用 React.createElement 递归生成真实 DOM。
- 拖拽交互:基于 react-dnd,物料区组件可拖拽到画布区,自动生成对应 JSON 节点。
- useDrop 封装为自定义 hook,提升复用性与可维护性。
一、项目准备与核心依赖
平台采用 React 19 + TypeScript + Vite 构建,核心依赖包括:
- Ant Design 5:UI 组件库,提升界面一致性与开发效率。
- TailwindCSS:原子化 CSS,极大简化样式管理。
- Zustand:轻量级状态管理,驱动组件树和配置。
- Allotment:实现编辑器区域的可拖拽分栏布局。
- react-dnd:实现物料区到画布区的拖拽交互。
- Monaco Editor:源码编辑与展示。
目录结构清晰,src/editor 下包含核心组件、hooks、物料、仓库等。
关于该项目的完整代码,你可以在这个仓库中查看:内附项目体验地址
「仓库链接」github.com/LZY-Ricardo…
二、组件封装与仓库设计
1. 组件封装:以 Container 为例
import { useMaterialDrop } from '../../hooks/useMaterialDrop'
export default function Container({ id, children, styles }) {
const { canDrop, dropRef, contextHolder } = useMaterialDrop(['Button', 'Container'], id)
return (
<>
{contextHolder}
<div data-component-id={id} ref={dropRef as any} className={`min-h-[100px] p-[20px] ${canDrop ? 'border-[2px] border-[blue]' : 'border-[1px] border-[#000]'}`} style={styles}>
{children}
</div>
</>
)
}
设计分析:
- 组件通过
useMaterialDrop封装拖拽能力,支持接收其他组件拖入。 canDrop动态改变边框样式,提升交互体验。- 结构简洁,便于扩展和复用。
2. 组件配置仓库(componentConfigStore)
export const useComponentConfigStore = create<State & Action>(
(set) => ({
componentConfig: {
Container: { name: 'Container', ... },
Button: { name: 'Button', ... },
// ...
}
})
)
设计分析:
- 统一管理所有可用组件的元数据(如默认属性、描述、开发/生产组件等)。
- 支持动态注册新组件,便于平台扩展。
3. 组件树仓库(componentsStore)
export const useComponentsStore = create<State & Action>(
(set, get) => ({
components: [ { id: 1, name: 'Page', ... } ],
addComponent: (component, parentId) => { ... },
deleteComponent: (componentId) => { ... },
updateComponentProps: (componentId, props) => { ... },
// ...
})
)
设计分析:
- 用于维护画布上的组件树(JSON),所有操作(增删改)都围绕这个仓库展开。
- 通过
addComponent、deleteComponent等方法,保证组件树的响应式和一致性。
三、拖拽组件的实现与原理
1. 拖拽能力的抽象与复用
export function useMaterialDrop(accept: string[], id: number) {
const { addComponent } = useComponentsStore()
const { componentConfig } = useComponentConfigStore()
const [messageApi, contextHolder] = message.useMessage();
const [{ canDrop }, dropRef] = useDrop(() => ({
accept,
drop: (item, monitor) => {
if (monitor.didDrop()) return
messageApi.success(item.type)
const props = componentConfig?.[item.type]?.defaultProps
const desc = componentConfig?.[item.type]?.desc
addComponent({ id: Date.now(), name: item.type, props, desc, styles: {} }, id)
},
collect: (monitor) => ({ canDrop: monitor.canDrop() })
}))
return { canDrop, dropRef, contextHolder }
}
设计分析:
- 将
useDrop逻辑抽离为useMaterialDrop,实现拖拽逻辑的复用和解耦。 - 拖拽落下时自动将组件对象插入到 JSON 树,实现“所见即所得”。
- 通过
canDrop实现拖拽反馈,提升交互体验。
2. 递归渲染组件树
function renderComponents(components) {
return components.map((component) => {
const config = componentConfig?.[component.name]
if (!config.prod) return null
return React.createElement(
config.prod,
{ key: component.id, id: component.id, ... },
renderComponents(component.children || [])
)
})
}
设计分析:
- 利用递归和
React.createElement,将 JSON 组件树动态渲染为真实页面结构。 - 支持组件嵌套和属性传递,保证画布区的灵活性和可扩展性。
效果图:
四、设计思路与效果总结
- 为什么这样做?
- 组件封装与仓库分离,保证了平台的高扩展性和可维护性。
- 拖拽逻辑抽象为 hook,极大提升了代码复用率和可读性。
- 递归渲染与响应式仓库结合,实现了“所见即所得”的低代码体验。
- 这样做的效果?
- 用户可自由拖拽、组合组件,实时预览页面结构。
- 属性编辑、组件树操作、源码同步一体化,极大提升开发效率和平台易用性。
通过上述设计与实现,低代码平台不仅实现了灵活的组件拖拽与动态渲染,还为后续功能扩展(如自定义物料、复杂交互等)打下坚实基础。
项目所有代码均已整理至以下仓库,方便大家查看与使用:内附项目体验地址
→ 代码仓库地址