- 低代码
- 前端
【免费下载链接】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 存在的几处过时内容,使用文档示例前请先对照修正:
- 标题写的是
@brickd/code-generator,而 package.json 中声明的包名是@brickd/hooks; - 安装命令列出的是
@brickd/canvas、@brickd/components、@brickd/render三个上层包,而不是 hooks 本身; - 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 |
| 属性 | useComponentProps | props 中模板变量到运行时状态的映射 |
| 显隐 | useHiddenComponent | 依据condition表达式决定是否隐藏节点 |
| 方法 | useHandleMethods | 编译并执行组件自定义方法(init / willUpdate 生命周期) |
| 管道 | useCommon | 渲染节点的聚合 Hook:状态 + 数据 + 属性 + 显隐 |
| 渲染 | useForceRender | 基于useReducer的强制刷新 |
| 观察者 | useBrickObserver | 读取全局BrickObserver实例 |
| 手势 | useDragMove | 自由布局下的元素拖拽移动 |
| 手势 | useResize | 自由布局下的八方向缩放 |
六个 Context 分别位于 components/ 目录:
| Context | 传递内容 |
|---|---|
StaticContext | pageConfig(节点树)、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, ): Tselector是要订阅的字段名数组;若包含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的关键流程:
- 每次渲染时用
store.getPageState(isRoot)取当前状态,若引用变化则按selector重新裁剪出selectedState(handleState函数); useLayoutEffect中订阅store.subscribe(checkForUpdates),状态更新时先用自带实现的shallowEqual(同文件 中定义)比较选中切片,一致则放弃更新;不一致且controlUpdate未否决时调用forceRender;- 渲染函数通过 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 }; }整条链路值得逐步拆开:
- 依赖字段裁剪。
getStateFields(packages/utils/src/handleStateFields.ts)扫描节点 props 中的模板变量、api配置与condition表达式,算出该节点真正读写了哪些状态字段;调用方还会把子节点字段(getChildrenFields)通过childSelector传入。节点只订阅自己需要的切片,是画布大量节点同屏时避免无关重渲染的关键。 - 状态聚合。
useGetState见下一节;注意普通节点传入空propsState且字段选择器为裁剪结果,而isStateDomain节点传入完整state并订阅$all——状态域节点是全量状态宿主。 - 数据拉取。
useService(pageState, api)在状态就绪后自动执行接口(见第七节)。 - 属性映射。useComponentProps 调用
@brickd/utils的dataMapping,把 props 中的变量占位(项目支持"模板字符变量"的能力)替换为运行时状态值,rest(容器组件透传的额外 props)有最高合并优先级:{ ...props, ...rest }。 - 显隐判定。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 的处理方式:
- 首次渲染(
renderTime.current === 0)时,对每个funContent执行new Function('return ' + funContent)()编译成真实函数,并按lifeCycle列表(LIFE_CYCLE枚举定义于@brickd/utils)登记进funMap.current[life][funName];无名函数以fun0、fun1兜底命名,随后立即执行全部init方法,入参为(state, funMap, nodeRef); - 之后的每次渲染执行
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的规模不大,但每个文件都对应框架的一个运行时机制。建议的阅读顺序:
- index.ts:全量 API 地图;
- BrickStore.ts → useRedux.ts:组件级状态的存储与受控同步;
- useBrickSelector.ts:订阅、浅比较与 forceRender 的取舍;
- useCommon.ts → useGetState.ts:节点渲染管道与 Proxy 状态分流;
- StateDomainWrapper.tsx:把上述能力组装成状态域;
- 对照 packages/core/src/reducers 与 store.ts 理解全局层,再回到 packages/canvas/src/hooks/useSelector.ts 看全局订阅如何被编辑器复用。
理解这条链路后,brick-design"实时渲染、状态域管理、模板字符变量、表达式、自定义方法"等特性在源码中都有明确的落点:渲染管道负责"变量与表达式何时取值",BrickStore与 Context 体系负责"状态存在哪里、谁能写",而 canvas/components 上层包只负责把用户交互翻译成 action 与 store 更新。
- 低代码
- 前端
【免费下载链接】brick-design
低代码框架,支持流式布局与自由布局拖拽编排,可视化拖拽、随意嵌套组合、实时渲染、实时辅助线展示、自由布局支持辅助对齐、支持自动吸附、实时组件间距展示、实时拖拽排序、状态域管理,可视化属性配置、可视化样式配置、多设备适配展示,支持逻辑渲染、模板字符变量、表达式、自定义方法、自定义状态
相关推荐
vim-indent-guides 终极指南:如何在 Vim 中可视化代码缩进层级
vim indent guides 终极指南:如何在 Vim 中可视化代码缩进层级 想要在 Vim 编辑器中清晰地看到代码的缩进层级吗?vim indent g
腾讯AngelSlim技术解析:Hy-MT2如何实现1.25位极致量化仅440MB存储
腾讯AngelSlim技术解析:Hy MT2如何实现1.25位极致量化仅440MB存储 腾讯Hy MT2是专为复杂现实场景设计的“快速思考”多语言翻译模型系列,
人工智能大模型NLP模型量化本地部署Ant Design状态管理方案:Context API与Hooks在组件中的应用
Ant Design状态管理方案:Context API与Hooks在组件中的应用 你是否还在为React应用中的状态共享而烦恼?当组件层级嵌套过深,props
前端UI组件设计系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考