- 低代码
- 前端
【免费下载链接】brick-design
低代码框架,支持流式布局与自由布局拖拽编排,可视化拖拽、随意嵌套组合、实时渲染、实时辅助线展示、自由布局支持辅助对齐、支持自动吸附、实时组件间距展示、实时拖拽排序、状态域管理,可视化属性配置、可视化样式配置、多设备适配展示,支持逻辑渲染、模板字符变量、表达式、自定义方法、自定义状态
BrickTree 是 brick-design 低代码编辑器中的组件树面板,它以树形结构实时呈现当前页面的pageConfig层级,并支持选中联动、复制、删除以及跨组件拖拽改序等操作。本文以官方文档 BrickTree.md 的 API 说明为主体,结合packages/components与packages/core中的真实源码,完整拆解它的渲染结构、与 BrickDesign 画布的同步机制,以及“增、删、改 + 拖拽排序”背后的 Action/Reducer 调用链。读完本文,你将掌握如何在BrickProvider体系内正确接入 BrickTree,并理解每一个树上操作如何最终改写pageConfig状态。
一、BrickTree 的定位:画布的“结构镜像”
官方文档对 BrickTree 的介绍可以概括为三点:
- 它是 brick design 编辑器的组件树,同时也是页面结构的可视化展示;
- 当直接编辑画布不方便时,可以通过修改组件树结构来实现想要的效果——即树面板是画布编辑之外的另一条操控入口;
- BrickTree 与 BrickDesign 实时同步,并且同样支持增、删、改,支持跨组件拖拽更改顺序。
在完整的编辑器外壳中,BrickTree 并不单独出现。从源码 Layers 面板 可以看到,它被放置在名为 "Layers" 的可拖拽调整宽度的侧栏(DragResizeBar)中,与页面列表Pages上下组合:
<DragResizeBar title={'Layers'} minWidth={212} icon={layersIcon} key={'Layers'}> <Pages onDelete={onDelete} /> <BrickTree /> <Tips icon={warnIcon} tip={'删除不可逆'} tipTitle={'注意'} ... /> </DragResizeBar>其中Pages负责页面(Layer)的增删改名,BrickTree负责当前页面内组件树的展示与编辑;Tips组件则承载“删除不可逆”的确认弹窗。也就是说,BrickTree 的编辑对象是当前页面的组件结构,而页面级的切换由同级的Pages完成。
二、安装与使用方式
按 docs/react/getting-started.md 的说明,首先安装 React 集成包:
yarn add @brickd/react或者
npm install @brickd/react然后在BrickProvider内部与BrickDesign并列挂载BrickTree:
import { createElement } from 'react'; import { BrickDesign, BrickTree, BrickProvider, useSelector, createActions, } from '@brickd/react'; const plugins = [(vDom, componentConfig) => vDom]; const customReducer = (state, action) => { const { type, payload } = action; switch (type) { case 'customReducer': return { ...state }; default: return state; } }; const App = () => { const { componentConfigs } = useSelector(['componentConfigs']); return ( <BrickProvider initState={{}} customReducer={customReducer} config={{ /* 你的配置 */ }}> <div onClick={() => createActions({ type: 'customReducer', payload: { /* ... */ } })}> 触发 action </div> <BrickDesign /> <BrickTree /> </BrickProvider> ); };这与 packages/components/README.md 中的用法示例一致:BrickTree必须处于BrickProvider的上下文中,与BrickDesign(画板/页面编辑组件)共享同一份状态。如果你使用完整编辑器外壳,示例工程 examples/react-example/src/App.tsx 直接使用了封装好的BrickEdifice,其内部(BrickEdifice/index.tsx)同样以BrickProvider+BrickDesignCanvas组合呈现各面板,Layers 面板(含 BrickTree)由Menus按需激活。
属性说明:className
文档明确 BrickTree 目前只有一个属性className,它作用在组件树的最外层 div 上。从当前源码结构看(BrickTree/index.tsx),组件树的根容器渲染为:
<div onMouseLeave={onMouseLeave} onWheel={(event) => event.stopPropagation()} className={styles['sort-container']} > <SortTree disabled childNodes={[ROOT]} specialProps={{ key: ROOT, domTreeKeys: [], parentKey: '' }} componentName={''} /> </div>这里有两个实现细节值得注意:
onWheel中调用event.stopPropagation(),阻止页面滚动事件冒泡,避免树面板内部滚动时干扰外层画布交互;onMouseLeave中调用setOperateState({ operateHoverKey: null, hoverNode: null }),当鼠标离开整个树面板时,清除画布上残留的 hover 高亮。
样式层面,.sort-container使用 flex 布局并允许自身滚动(index.less)。如果需要对树面板做布局调整(例如固定高度、宽度),按文档说明将className作用在最外层 div 上即可。
三、树的数据来源与递归渲染结构
3.1 数据源:pageConfig[ROOT]
BrickTree 入口 通过useSelector订阅STATE_PROPS域中的pageConfig,其比较函数只在根节点从无到有(或从有到无)时才触发刷新:
const { pageConfig } = useSelector( ['pageConfig'], (prevState, nextState) => { const { pageConfig } = nextState; return !!(!prevState.pageConfig[ROOT] && pageConfig[ROOT]); } ); if (!pageConfig[ROOT]) return null;这解释了 BrickTree 的渲染前提:页面必须存在根节点ROOT,树面板才会渲染;一旦整页被清空(见后文deleteComponent对 ROOT 的处理),树面板即返回null。树从childNodes={[ROOT]}开始递归展开,specialProps携带key、domTreeKeys、parentKey等定位信息,供深层节点识别自己的位置。
3.2 递归渲染:SortTree → SortItem
树的主体由 SortTree.tsx 与 SortItem.tsx 两个组件递归构成:
SortTree把一组childNodes渲染为可排序容器(ReactSortable+SortItem列表);SortItem表示一个节点,其内部通过renderSortTree决定如何展开子级(SortItem.tsx L33-L70):- 若
childNodes是数组(普通容器),继续渲染一个SortTree; - 若
childNodes不是数组且存在nodePropsConfig,则按nodePropsConfig的每个属性名渲染“属性子树”——也就是说,组件上配置了nodeProps的字段(如 Form 的 items、List 的数据源等)也会作为可嵌套子树展示,propChildNodes取get(childNodes, propName, [])。
- 若
每个SortItem通过useSelector订阅selectedInfo与pageConfig(SortItem.tsx L103-L112),从pageConfig[key]读出节点的componentName与childNodes。此外,每个节点 DOM 上写入了三类数据属性(SortItem.tsx L159-L165):
<div className={styles['sort-item']} id={key} >onMouseOver={() => { !isSelected && setOperateState({ hoverNode: getSelectedNode(key), operateHoverKey: key, }); }}鼠标悬停树上节点时,通过getSelectedNode(key)拿到画布中对应的真实 DOM 节点,写入operateHoverKey与hoverNode,画布侧组件据此显示 hover 轮廓;onMouseLeave时再清空。Header还通过useOperate()的setSubscribe(changeStatus)订阅操作状态变化,从而响应画布侧发起的 hover(即画布 → 树方向的反向高亮)。
(2)树 → 画布(点击选中 + 滚动定位)
onClick={(event) => { event.stopPropagation(); if (isSelected) { clearSelectedStatus(); setOperateState({ selectedNode: null, operateSelectedKey: null }); } else { const selectedNode = getSelectedNode(key); selectComponent({ ...specialProps, propName }); nodeScrollIntoView(selectedNode); setOperateState({ selectedNode, operateSelectedKey: key }); } }}点击节点会派发selectComponentaction(定义见 produceActions/selectedComponent.ts),更新全局selectedInfo;nodeScrollIntoView负责把画布中对应节点滚动进可视区域;再次点击已选中节点则取消选中。由于 BrickDesign 画布同样订阅selectedInfo来渲染选中边框,树与画布的选中态因此始终保持一致。
(3)画布占位预览:placeholderBridgeStore桥
这是“实时同步”中最有特色的一环。当你在树上拖拽节点时,画布侧会实时渲染一个占位框(GuidePlaceholder),预览组件将被放入的位置。其实现链路是:
- dragSort.ts L14-L23 定义了全局桥对象:
export const placeholderBridgeStore = { prevRects: null, renderPlaceholder: null, changePosition(rects: PlaceholderPositionType) { if (!isEqual(this.prevRects, rects)) { this.prevRects = rects; this.renderPlaceholder && this.renderPlaceholder(rects); } }, };- 画布侧 GuidePlaceholder 组件 把
placeholderBridgeStore.renderPlaceholder注册为占位框渲染函数; - 树上拖拽过程中,
SortTree的onChange调用 onDragBrickTree(下文详述),实时计算node1/node2/node3三段占位矩形并changePosition推给画布; - 拖拽结束或无变化时,
changePosition({})清空占位框(参见 Container.ts 中对placeholderBridgeStore的清空调用)。
因此,即使落点在画布深处,你也能在拖到一半时就直观看到“松手后会插在哪里”。
五、增、删、改:树上操作的 Action/Reducer 实现
文档所说的“支持增、删、改,支持跨组件拖拽更改顺序”,在 Header.tsx 中落为三个按钮行为,对应的 action 与 reducer 均在@brickd/core包内。
5.1 复制(copyComponent)
树节点 hover 时显示复制图标(非属性节点、非 ROOT 节点才显示),点击后执行copyComponent({ key, parentKey, parentPropName })。其 reducer 实现于 handleComponentSchema.ts L104-L136:
- 未选中节点且未传
key时,warn('Please select the node you want to copy')并返回原 state; - 禁止复制根节点:
selectedKey或key为ROOT时给出Prohibit copying root node警告; - 合法复制时,先
undo.push({ pageConfig })、清空redo,再通过getNewKey(pageConfig)生成新键,用 immer 的produce + update把newKey追加到父节点childNodes,并用copyConfig深拷贝原节点配置——这就是文档所述“增”的来源,复制出的节点会即时出现在树与画布中(配合 3.3 节的自动展开)。
5.2 删除(deleteComponent)
同样在 handleComponentSchema.ts L184-L224:
- 未选中且无
key时警告Please select the components you want to delete; - 若删除的是
ROOT,直接返回空pageConfig(即删除整个页面,树面板随之整体不渲染); - 否则从父节点
childNodes中移除引用(deleteChildNodesKey),若该节点自身还有childNodes,则递归deleteChildNodes清掉所有子节点,最后delete oldState[key]并重置selectedInfo: null。
删除会先压入undo,支持撤销;而页面级的删除(Layers 面板的deleteLayers)则在 Layers/index.tsx 中通过Tips弹窗做“删除不可逆”的二次确认。
5.3 排序与跨组件移动(onLayoutSortChange)
这是 BrickTree 的核心能力。SortTree的onSortEnd回调(SortTree.tsx L81-L99)拿到排序后的sortKeys与拖拽事件后:
const layoutSortChange = useCallback(function (sortKeys: string[], evt) { const dragInfo = JSON.parse(evt.clone.dataset.special); // {key, parentPropName, parentKey} onLayoutSortChange({ sortKeys, parentKey: key, parentPropName: propName!, dragInfo }); setOperateState({ dropNode: null, isDropAble: false }); setNewDragKey(null); }, []);对应的 reducer(handleComponentSchema.ts L144-L177)做了两件事:
- 本层重排:把父节点位置的
childNodes直接替换为sortKeys; - 跨组件移动清理:若
dragInfo存在且其parentKey/parentPropName与当前落点不同,说明节点从别的父容器中被拖出,于是再对原父节点执行deleteChildNodesKey(childNodes, key, parentPropName),删除旧引用。
源码注释也明确写道:“dragInfo 有值说明为跨组件排序,需要删除拖拽组件原先父组件中的引用”。两次update在同一个produce内完成,保证pageConfig原子更新,随后画布按新结构重新渲染——这就是文档中“支持跨组件拖拽更改顺序”的完整闭环。
六、拖拽排序的实现细节:ReactSortable 与落点规则
6.1 拖拽引擎封装
树的可排序容器基于 ReactSortable,它是对sortablejs的forwardRef封装:useEffect中为onChoose/onAdd/onUpdate/onChange/onEnd五个事件统一注入包装逻辑——
onChoose记录nextSibling与当前容器;onAdd/onUpdate触发时先把evt.item插回nextSibling之前(回滚 DOM 乐观更新),再取sortable.toArray()得到完整键序并回调onSortEnd(items, evt),由状态层真正改写pageConfig;onChange把实时键序回调给onChange(items, evt),用于拖拽过程中的画布占位预览。
6.2 Sortable 配置与落点校验
SortTree.tsx L117-L137 给出了关键的options:
<ReactSortable className={styles['sort-able']} options={{ group: { name: 'nested', put: (a, b, c) => putItem(c, props) }, animation: 200, disabled, dataIdAttr: 'id', ghostClass: styles['item-background'], swapThreshold: 0.5, }} onSortEnd={layoutSortChange} onChange={onChange} onEnd={onEnd} >group.name: 'nested'+put: putItem:所有层级的 SortTree 同属nested组,因此节点天然可以跨层拖动;而putItem(SortTree.tsx L50-L64)是落点守门人:
const putItem = (c: any, props: SortTreePropsType) => { const { childNodes = [], nodeProps, componentName } = props; const fatherNodesRule = c.dataset.farules; const dragName = c.dataset.name; if (fatherNodesRule && !JSON.parse(fatherNodesRule).include(componentName)) return false; if (nodeProps) { if (nodeProps.isOnlyNode && childNodes.length === 1) return false; if (nodeProps.childNodesRule) { return nodeProps.childNodesRule.includes(dragName); } } return true; };落点合法性由组件 Schema 决定:fatherNodesRule声明“哪些父级可以容纳我”,childNodesRule声明“我可放入哪些子级”,isOnlyNode约束“容器内只能有一个节点”时禁止再插入第二个。这使砖块设计器可以把组件的嵌套约束完全配置化。
dataIdAttr: 'id'与SortItem的id={key}配合,保证toArray()返回的就是pageConfig键序列;animation: 200、ghostClass(灰色幽灵占位)、swapThreshold: 0.5控制拖拽手感;disabled由上层传入,根层SortTree传disabled是因为 ROOT 本身不可被拖走(复制/删除也禁止操作 ROOT,见 5.1、5.2)。
6.3 拖拽过程:画布占位框的实时计算
拖拽过程中onChange会持续触发 onDragBrickTree:
export const onDragBrickTree = (dragKey, sortKeys, parentKey, setOperateState, propName) => { let parentNode = getSelectedNode(parentKey); let childNodes = get(getVNode(parentKey), 'childNodes', []); if (!Array.isArray(childNodes) && propName) { childNodes = get(childNodes, 'propName', []); // 属性子树 } if (isEqual(childNodes, sortKeys)) return placeholderBridgeStore.changePosition(placeholderPosition); // ...计算 node1/node2/node3 三段占位矩形 placeholderBridgeStore.changePosition(placeholderPosition); };它会:解析属性子树(propName)情形、把落点 DOM 写入dropNode并置isDropAble: true(供画布侧渲染投放提示)、根据拖拽键在排序序列中的位置(首/中/尾)分别调用getPlaceholder1Rect/getPlaceholder3Rect计算三段式占位矩形,最后推送给placeholderBridgeStore。若排序结果与原childNodes完全一致(isEqual),直接清空占位——这解释了为什么“原地微动”不会在画布上留下多余占位框。
七、使用注意事项小结
结合文档与源码,使用 BrickTree 时需记住:
- 必须在
BrickProvider内使用,且与BrickDesign共享状态;页面pageConfig中没有ROOT时树面板不渲染(index.tsx L27); - 当前对外属性只有
className,作用于组件树最外层 div,用于布局与样式覆盖; - ROOT 节点是保护对象:不可复制、不可拖拽,删除 ROOT 等于清空整页(会触发
undo可撤销,但页面级deleteLayers不可逆); - 嵌套约束由组件配置驱动:
fatherNodesRule、childNodesRule、isOnlyNode决定拖拽能否落点,配置不当会出现“拖不进去”的现象,排查时优先检查对应组件的NodeProps配置; - 树操作全部走 action(
copyComponent/deleteComponent/onLayoutSortChange/selectComponent,见 actionTypes.ts 与 reducers/index.ts),并自动维护undo/redo,因此撤销/重做按钮(MidBottomButtons)对树操作同样生效; - 性能上做了针对性收敛:
BrickTree、SortTree、SortItem、Header均为memo组件,useSelector的自定义比较函数限制了根级重渲染,滚动/悬停事件也做了stopPropagation隔离,这些是树与画布能“实时同步”而不互相拖累的前提。
至此,BrickTree 从文档描述的“组件树 + 实时同步 + 增删改 + 跨组件拖拽”,到pageConfig数据流、placeholderBridgeStore占位桥、nested组拖拽规则与produce原子更新的完整实现路径均已闭环。若需进一步阅读,建议依次查看 BrickTree 面板源码、handleComponentSchema.ts 与 dragSort.ts,它们分别对应树渲染、状态改写与拖拽预览三个核心环节。
- 低代码
- 前端
【免费下载链接】brick-design
低代码框架,支持流式布局与自由布局拖拽编排,可视化拖拽、随意嵌套组合、实时渲染、实时辅助线展示、自由布局支持辅助对齐、支持自动吸附、实时组件间距展示、实时拖拽排序、状态域管理,可视化属性配置、可视化样式配置、多设备适配展示,支持逻辑渲染、模板字符变量、表达式、自定义方法、自定义状态
相关推荐
Brick Design拖拽编排进阶:跨组件嵌套与排序技巧
Brick Design拖拽编排进阶:跨组件嵌套与排序技巧 Brick Design 是一款强大的 低代码可视化拖拽编排框架 ,专为前端开发者和设计师打造。通过
低代码前端Ant Design Blazor Tree 组件拖拽排序实战:Draggable 节点拖放与数据同步解析
Ant Design Blazor Tree 组件拖拽排序实战:Draggable 节点拖放与数据同步解析 Ant Design Blazor 的 Tree 组
前端UI组件设计系统Brick Design核心组件详解:BrickProvider、BrickDesign、BrickTree
Brick Design核心组件详解:BrickProvider、BrickDesign、BrickTree Brick Design是一款强大的低代码可视化拖
低代码前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考