☰
brick-design 中 @brickd/hooks 深度解析:状态域、useCommon 渲染管道与组件级状态管理
2026/10/13 1:56:24 网站建设 项目流程
  • 低代码
  • 前端

【免费下载链接】brick-design

低代码框架,支持流式布局与自由布局拖拽编排,可视化拖拽、随意嵌套组合、实时渲染、实时辅助线展示、自由布局支持辅助对齐、支持自动吸附、实时组件间距展示、实时拖拽排序、状态域管理,可视化属性配置、可视化样式配置、多设备适配展示,支持逻辑渲染、模板字符变量、表达式、自定义方法、自定义状态

项目地址:https://gitcode.com/gh_mirrors/br/brick-design
点击查看免费下载

@brickd/hooks是 brick-design 低代码框架中最底层的 React Hook 工具包,@brickd/render(节点渲染)、@brickd/canvas(画布编辑器)与@brickd/components(属性/样式面板)三个上层包都构建在它之上。它提供组件级状态存储(BrickStore+useRedux)、状态订阅选择器(useBrickSelector/useSelector)、数据服务(useService)、自定义生命周期方法(useHandleMethods)以及自由布局手势(useDragMove/useResize)。读完本文,你可以掌握 brick-design 双层状态模型的实现原理、useCommon渲染管道中"属性 → 数据 → 显隐"的完整链路,以及状态域(State Domain)如何在组件子树内实现状态隔离。

一、README 勘误与包的定位

先给出 packages/hooks/README.md 原文的安装方式(命令可执行,包名已按下文勘误修正):

yarn add @brickd/hooks # 或 npm install @brickd/hooks

需要指出原 README 存在的几处过时内容,使用文档示例前请先对照修正:

  1. 标题写的是@brickd/code-generator,而 package.json 中声明的包名是@brickd/hooks;
  2. 安装命令列出的是@brickd/canvas、@brickd/components、@brickd/render三个上层包,而不是 hooks 本身;
  3. Usage 示例从@brickd/react导入BrickDesign、BrickTree等组件。当前仓库的文档站 docs/react/index.md 与 docs/react/getting-started.md 也引用了@brickd/react,而仓库包目录中不存在该包;结合 packages/canvas/package.json 的包名与导出看,可以推断文档中对应的包在当前版本里是@brickd/canvas。BrickTree组件位于@brickd/components(参见 docs/react/components/BrickTree.md)。

修正后的集成骨架与 README Usage 一致,即"Provider 注入全局状态 → 编辑器 + 组件树 + 实时预览":

import { createElement } from 'react'; import { BrickDesign, BrickProvider, useSelector, createActions } from '@brickd/canvas'; import { BrickTree } from '@brickd/components'; import BrickRender from '@brickd/render'; const plugins = [(vDom, componentSchema) => vDom]; const customReducer = (state, action) => { const { type, payload } = action; switch (type) { case 'customReducer': return { ...state }; default: return state; } }; const App = () => { const { pageConfig } = useSelector(['pageConfig']); return ( <BrickProvider customReducer={customReducer} config={{ /* componentsMap + componentSchemasMap */ }}> <div onClick={() => createActions({ type: 'customReducer', payload: {} })}>触发 action</div> <BrickDesign /> <BrickRender pageConfig={pageConfig} createElement={createElement} plugins={plugins} /> <BrickTree /> </BrickProvider> ); };

其中createActions与useSelector正是 hooks 库生态的一部分:前者由@brickd/core从全局 store 中派发(createActions 实现 直接getStore().dispatch(action)),后者的 canvas 版本在 useSelector.ts 中只是对 hooks 库useBrickSelector的一层包装。仓库内的真实示例工程 examples/react-example/src/App.tsx 则使用了更上层的BrickEdifice组件一次性完成装配。

@brickd/hooks当前版本为1.0.0-alpha.17(package.json),依赖@brickd/utils、lodash与react(^16.8.0)。整个仓库通过 lerna(lerna.json,useWorkspaces: true,npmClient 为 yarn)统一管理版本,构建产物同时提供 CJS(./dist/cjs/index.js)与 ESM(./dist/esm/index.js)两种入口,且声明sideEffects: false以支持 tree-shaking。

二、导出清单:index.ts 暴露了什么

hooks 库的完整 API 由 packages/hooks/src/index.ts 一行一个模块地重新导出,共 13 个 Hook、6 个 Context 与 2 个工具类:

分类导出职责
状态useRedux为组件/状态域创建独立的BrickStore并同步 props 变化
状态useBrickdState组件局部状态的 ReactuseState形式封装
状态useGetState用 Proxy 合并全局/局部/函数参数/props 为可写状态对象
状态useGetProps按"局部 state → 节点 props → 父级 props"合并属性
订阅useBrickSelector选择器式状态订阅(useSelector的底层实现)
数据useService按api配置自动拉取数据并回写 state / pageState
属性useComponentPropsprops 中模板变量到运行时状态的映射
显隐useHiddenComponent依据condition表达式决定是否隐藏节点
方法useHandleMethods编译并执行组件自定义方法(init / willUpdate 生命周期)
管道useCommon渲染节点的聚合 Hook:状态 + 数据 + 属性 + 显隐
渲染useForceRender基于useReducer的强制刷新
观察者useBrickObserver读取全局BrickObserver实例
手势useDragMove自由布局下的元素拖拽移动
手势useResize自由布局下的八方向缩放

六个 Context 分别位于 components/ 目录:

Context传递内容
StaticContextpageConfig(节点树)、componentsMap(真实 React 组件映射)、props、options、pageName
FunParamContext函数型属性被调用时的实参(如回调类 props)
BrickStoreContext当前子树的BrickStore(状态域局部 store)
PropsContext父级向子树传递的属性集合
MapNodeContext循环渲染项的item与index
BrickObserverContext全局单例BrickObserver

另有 BrickStore(组件级状态容器)与 BrickOberver(命令式观察者,注意源码中该文件名即拼写为BrickOberver)两个非 React 类。

三、双层状态模型:BrickStore 与全局 Redux store

brick-design 中的"状态"分两层,@brickd/hooks主要服务于第二层。

全局页面状态存放在 Redux store 中。createLegoStore 用redux的createStore创建 store(并自动挂载__REDUX_DEVTOOLS_EXTENSION__),支持外部传入的customReducer经过 combineReducers 串联在内置 reducer 之后。每页状态的结构由 handlePageBrickdState.ts 中的legoState定义:pageConfig(所有组件节点)、selectedInfo(选中信息)、undo/redo(撤销重做栈)、platformInfo(多设备适配,默认platformName: 'PC')。页面状态还按"层"组织:getPageState 读取根状态中的layerName,返回对应层的子状态;因此同一份数据可以维护多个画布层。

组件级状态由BrickStore承载。BrickStore.ts 的BrickStore<T>类维护state、全局监听器数组listeners与按键监听器映射listenerMap:

setPageState = (newState: T, isReplace?: boolean, executeKey?: string) => { if (isReplace) { this.state = newState; } else { this.state = { ...this.state, ...newState }; // 默认浅合并 } executeListener(this.listeners); if (executeKey && this.listenerMap[executeKey]) { executeListener(this.listenerMap[executeKey]); // 按键精准通知 } };

subscribe(listener, key?)返回取消函数:不传key挂全局监听,传了key则只在该键更新时被executeKeyListener触发,实现"按字段订阅"的低成本通知。isPageStore = true这个实例属性则是选择器识别"这是页面级 store"的标记(见下一节)。

useRedux 把BrickStore接入 React 生命周期:

export function useRedux(propsState: any) { const brickStore = useRef(new BrickStore(propsState)).current; const prevPropsState = useRef(propsState).current; useMemo(() => { if (!isEqual(prevPropsState, propsState)) brickStore.setPageState(propsState, true); // 深比较,变化才整体替换 }, [propsState, prevPropsState]); return brickStore; }

useRef保证每个组件实例只有唯一一个 store;props 中的初始state通过 lodashisEqual深比较,仅在真正变化时以isReplace = true整体替换——这为"从外部初始化状态域"提供了受控入口。

四、useBrickSelector:选择器式状态订阅的实现

useBrickSelector.ts 是整个 hooks 库技术含量最高的文件,它实现了一个"传字段名数组、做浅比较、按需重渲染"的选择器:

export function useBrickSelector<T, U extends string>( selector: U[], controlUpdate?: ControlUpdate<T>, stateDeep?: string, context: any = BrickStoreContext, isRoot?: boolean, ): T
  • selector是要订阅的字段名数组;若包含ALL_PROPS(在 packages/utils/src/handleStateFields.ts 中定义为'$all')则直接返回整个 store 状态;
  • stateDeep支持点路径深层取值,如['a.b.c']会取出state.a.b.c并以末段名c作为返回对象的键;
  • controlUpdate(prev, next)是自定义更新判定:返回false时即使浅比较不一致也不触发渲染;
  • context决定从哪个 Context 取 store,isRoot决定读根状态还是当前层状态。

内部useSelectorWithStore的关键流程:

  1. 每次渲染时用store.getPageState(isRoot)取当前状态,若引用变化则按selector重新裁剪出selectedState(handleState函数);
  2. useLayoutEffect中订阅store.subscribe(checkForUpdates),状态更新时先用自带实现的shallowEqual(同文件 中定义)比较选中切片,一致则放弃更新;不一致且controlUpdate未否决时调用forceRender;
  3. 渲染函数通过 useForceRender(useReducer((s) => s + 1, 0))拿到强制刷新函数,更新发生在 effect 之后,因此组件"读到的最新值在下一次提交中生效",这是它选择useLayoutEffect+ 手动 force 而非useState缓存的原因——避免了 store 与 React 状态的双向同步复杂度。

返回值有一个重要分支:当store.isPageStore为真时,结果会额外挂上setPageState与getPageState两个方法。也就是说读取结果的形态本身携带了写入通道——组件拿到const { pageConfig, setPageState } = useSelector(['pageConfig'])后即可直接改状态。@brickd/canvas的 useSelector 只是把全局 store 所在的BrickContext作为context参数传入,其余逻辑完全复用上面的实现。

五、useCommon:节点渲染的聚合管道

useCommon 是@brickd/render与@brickd/canvas渲染每个节点时都会经过的"总装配线":

export function useCommon(vNode: VirtualDOMType, rest: any, childSelector: string[] = []) { const { props: prevProps, state, api, condition, isStateDomain } = vNode; const selector = useMemo( () => [...getStateFields({ prevProps, api, condition }), ...childSelector], [prevProps, api, condition, childSelector], ); const pageState = useGetState( isStateDomain ? state : {}, isStateDomain ? [ALL_PROPS] : selector, ); useService(pageState, api); const props = useComponentProps(prevProps, pageState, rest); const hidden = useHiddenComponent(pageState, condition); return { props, hidden, pageState }; }

整条链路值得逐步拆开:

  1. 依赖字段裁剪。getStateFields(packages/utils/src/handleStateFields.ts)扫描节点 props 中的模板变量、api配置与condition表达式,算出该节点真正读写了哪些状态字段;调用方还会把子节点字段(getChildrenFields)通过childSelector传入。节点只订阅自己需要的切片,是画布大量节点同屏时避免无关重渲染的关键。
  2. 状态聚合。useGetState见下一节;注意普通节点传入空propsState且字段选择器为裁剪结果,而isStateDomain节点传入完整state并订阅$all——状态域节点是全量状态宿主。
  3. 数据拉取。useService(pageState, api)在状态就绪后自动执行接口(见第七节)。
  4. 属性映射。useComponentProps 调用@brickd/utils的dataMapping,把 props 中的变量占位(项目支持"模板字符变量"的能力)替换为运行时状态值,rest(容器组件透传的额外 props)有最高合并优先级:{ ...props, ...rest }。
  5. 显隐判定。useHiddenComponent 对condition求值:包含$的表达式走tokenize(变量替换语义),否则走evalExpression(逻辑表达式语义);求值为真时节点返回hidden = true,渲染容器直接if (hidden) return null(render/src/wrappers/Container.tsx)。

useGetState 是这条管道里最巧妙的部分。它把多来源状态合并成一个Proxy 可写对象:

return new Proxy( { ...brickdState, ...state, funParams, props, ...mapItem, index }, { set: function (target, propKey, value, receiver) { if (propKey in brickdState) { brickdState.setPageState({ [propKey]: value }); // 写入 Redux 全局状态 } else { state.setState({ [propKey]: value }); // 写入组件局部状态 } return Reflect.set(target, propKey, value, receiver); }, }, );
  • 读侧按全局选中切片 → 组件 state → funParams → props → 循环项(item/index)的顺序覆盖合并,index是循环渲染下的当前项下标;
  • 写侧根据键的归属自动分流:属于全局状态的键走setPageState派发全局更新,其余键落在组件局部setState;
  • funParams来自FunParamContext:当childNodes使用#propName这类函数型属性时(见 render Container 中propName.includes('#')的分支),回调实参会被塞入该 Context,子组件就能在useGetState的结果中读到它们。

六、状态域:StateDomainWrapper 与子树状态隔离

brick-design 的"状态域管理"能力在渲染层由 StateDomainWrapper.tsx 实现,它是 hooks 库 API 的教科书式组合:

function StateDomainWrapper(props: any) { const { renderKey, ...rest } = props; const { pageConfig } = useContext(StaticContext); const { state, api, childNodes } = pageConfig[renderKey]; const brickdStore = useRedux(state); const childProps = useGetProps(brickdStore.getPageState(), rest); useService(brickdStore.getPageState(), api); return ( <PropsProvider value={childProps}> <BrickStoreProvider value={brickdStore}> {childNodes ? <Container renderKey={renderKey} /> : <NoneContainer renderKey={renderKey} />} </BrickStoreProvider> </PropsProvider> ); }

工作原理:

  • 当节点树中出现isStateDomain: true的节点时,render Container 的renderArrChild会把它包进StateDomainWrapper(画布侧有对称实现 canvas/wrappers/StateDomainWrapper.tsx);
  • useRedux(state)为该节点创建一个独立于全局 Redux store 的局部BrickStore,初始值来自节点配置里的state;
  • BrickStoreProvider通过BrickStoreContext把这个局部 store 注入整棵子树。子树中任何useBrickSelector默认读取的就是BrickStoreContext而非全局BrickContext——于是局部状态读写被天然隔离在状态域内,setPageState更新的是局部 store 并只通知本子树的订阅者;
  • useGetProps 按{ ...state, ...props, ...parentProps }合并出子树属性,父级 props 优先级最高,保证外层配置可以覆盖状态域内部值。

配合第五节的useGetState,一个状态域组件内部"读全局(useSelector)+ 读写局部(pageStateProxy)"两套通道并存,这是框架"自定义状态"能力的运行时基础。

七、useService:api 数据服务与状态回写

useService 实现了组件api配置到真实请求的桥接:

  • 首次挂载时执行一次初始拉取(isFirst = true),之后监听prevApi/api/prevState/state变化,变化时再次拉取;
  • 请求本身委托@brickd/utils的fetchData(packages/utils/src/api.ts),请求函数来自StaticContext.options(由config.fetcher注入,参见 ConfigTypes:fetcher?: FetcherType),因此框架不绑定任何具体 HTTP 库;
  • 响应约定为{ state, pageState }双通道:state写入组件局部(nextData.setState),pageState写入页面级(nextData.setPageState)——与useGetState的 Proxy 写分流形成闭环。

也就是说,api配置的"数据 → 状态 → 重渲染"整条回路由useService一个 Hook 完成,组件声明式地依赖了它,无需手写useEffect。

八、useHandleMethods:自定义方法与生命周期

useHandleMethods 支撑项目描述中的"自定义方法"能力。节点配置里的methods是{ funContent, lifeCycle }[],Hook 的处理方式:

  1. 首次渲染(renderTime.current === 0)时,对每个funContent执行new Function('return ' + funContent)()编译成真实函数,并按lifeCycle列表(LIFE_CYCLE枚举定义于@brickd/utils)登记进funMap.current[life][funName];无名函数以fun0、fun1兜底命名,随后立即执行全部init方法,入参为(state, funMap, nodeRef);
  2. 之后的每次渲染执行willUpdate方法。

方法内部通过funMap可以互相调用,通过nodeRef拿到节点 DOM——这是"逻辑渲染、自定义方法"与画布 DOM 打通的通道。需要注意new Function意味着方法体执行在运行时且不受沙箱保护,生产环境应对方法来源保持信任边界。

九、手势类 Hook:useDragMove 与 useResize

自由布局(非流式布局)的拖拽编排能力由两个纯 DOM 操作 Hook 提供,被 canvas 编辑器的操作面板与@brickd/components的拖拽组件复用:

useDragMove 接受一个getTarget: () => HTMLElement:

  • onMoveStart记录起点clientX/clientY与元素当前top/left(从getComputedStyle读取),并临时pointerEvents = 'none'、transition = 'none',避免拖动中被自身事件拦截或 CSS 过渡拖影;
  • onMove增量计算newLeft = left + offsetX、newTop = top + offsetY,直接写target.style而不经过 React 状态,保证拖拽帧率;
  • onMoveEnd恢复pointerEvents并复位isMove。

useResize 处理八方向缩放(Direction枚举:top/right/bottom/left/topRight/bottomRight/bottomLeft/topLeft):

  • onResizeStart(event, direction)通过 utils 的css()读取当前width/height/minWidth/minHeight/maxWidth/maxHeight(formatUnit把非数字解析为null表示无约束);
  • onResize按方向计算offsetX/offsetY并套用 min/max 约束后再写 style;
  • 约束以"类型非 number 即放行"的方式处理,auto等非法 CSS 值经formatUnit转null后自然被忽略。

这两个 Hook 体现了框架"拖拽过程走 DOM、结束落 state"的一致策略:过程中的高频更新绕开 React,最终值在结束时经resizeChange等 action 写回全局状态(参见 docs/react/api/functions.md 的resizeChange({width?,height?})说明)。

十、BrickObserver:跨组件的命令式通知

BrickOberver.ts 与 React 无关,是一个一次性观察者:addSubject接受单个函数或"键 → 函数"的对象,executeSubject执行全部函数型观察者(执行后清空数组)与subjectsMap,cleanSubject手动清理。BrickObserverContext 用模块级变量维护单例:首个BrickObserverProvider渲染时创建,其后所有 Provider 共享同一实例;useBrickObserver供消费者读取。从源码结构看(canvas 的 ZoomProvider 等引入了它),它主要用于需要"渲染期登记回调、统一时刻批量执行"的场景,如缩放、对齐等编辑器级联动。

十一、构建与集成:如何在 monorepo 中跑起来

  • 版本与依赖:@brickd/hooks@1.0.0-alpha.17,运行依赖@brickd/utils、lodash、react ^16.8.0(package.json);
  • 入口:main为./dist/cjs/index.js,module为./dist/esm/index.js,types指向./lib/index.d.ts;sideEffects: false允许消费方 tree-shaking 未用的 Hook;
  • 构建脚本(package.json):build执行tsc && rollup -c rollup.config.js(类型产物 + 打包产物),dev并行tsc -w与rollup -c rollup.config.js -w,clean调用根目录 scripts/clean_package.sh;rollup 配置见 packages/hooks/rollup.config.js;
  • 消费关系(从源码 import 关系看):@brickd/render用useCommon/useRedux/useGetProps/StaticContext完成节点求值;@brickd/canvas用useBrickSelector包装编辑器全局订阅(useSelector.ts),并用useDragMove/useResize系能力操作画布;@brickd/components面板组件复用同类 Hook 做属性面板交互。完整可运行的装配参考 examples/react-example,其initBrickdState(xiaomi.ts)展示了带pageConfig的初始数据形态。

十二、源码阅读路线小结

@brickd/hooks的规模不大,但每个文件都对应框架的一个运行时机制。建议的阅读顺序:

  1. index.ts:全量 API 地图;
  2. BrickStore.ts → useRedux.ts:组件级状态的存储与受控同步;
  3. useBrickSelector.ts:订阅、浅比较与 forceRender 的取舍;
  4. useCommon.ts → useGetState.ts:节点渲染管道与 Proxy 状态分流;
  5. StateDomainWrapper.tsx:把上述能力组装成状态域;
  6. 对照 packages/core/src/reducers 与 store.ts 理解全局层,再回到 packages/canvas/src/hooks/useSelector.ts 看全局订阅如何被编辑器复用。

理解这条链路后,brick-design"实时渲染、状态域管理、模板字符变量、表达式、自定义方法"等特性在源码中都有明确的落点:渲染管道负责"变量与表达式何时取值",BrickStore与 Context 体系负责"状态存在哪里、谁能写",而 canvas/components 上层包只负责把用户交互翻译成 action 与 store 更新。

  • 低代码
  • 前端

【免费下载链接】brick-design

低代码框架,支持流式布局与自由布局拖拽编排,可视化拖拽、随意嵌套组合、实时渲染、实时辅助线展示、自由布局支持辅助对齐、支持自动吸附、实时组件间距展示、实时拖拽排序、状态域管理,可视化属性配置、可视化样式配置、多设备适配展示,支持逻辑渲染、模板字符变量、表达式、自定义方法、自定义状态

项目地址:https://gitcode.com/gh_mirrors/br/brick-design
点击查看免费下载

相关推荐

上一篇:FLAC入门:10分钟学会无损音频编码与解码
下一篇:AniPortrait开源协议:License选择与商业使用规范

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询