- 前端
- UI组件
【免费下载链接】golden-layout
A multi window layout manager for webapps
导读
本文聚焦 Golden Layout(多窗口布局管理器)最核心的机制——组件绑定(Binding Components)。Golden Layout 并不直接渲染业务组件,而是通过"绑定"将应用组件纳入布局系统,并接管其位置(position)、尺寸(size)与可见性(visibility)。文档 docs/binding-components/index.md 系统阐述了四种绑定方式:Embedding via Registration(经典注册式)、Embedding via Events(事件式嵌入)、Virtual via Registration(注册式虚拟化)、Virtual via Events(纯事件虚拟组件)。读完本文,你将掌握每种方式的适用场景、完整事件签名与可运行代码示例,并理解底层 virtual-layout.ts、golden-layout.ts、component-container.ts 的实现原理,能够在自己的应用中正确选型并落地。
一、绑定概述:Golden Layout 如何"控制"你的组件
Golden Layout 将自身定位为一个布局引擎:它负责决定每个组件该出现在哪里、占多大面积、是否可见,而组件本身的创建与销毁则交由应用决定。绑定(Binding)就是二者之间的契约。
根据绑定方式的不同,组件根元素在 DOM 层级中的归属有两种截然不同的策略:
- 嵌入模式(Embedding):组件根 HTML 元素被插入到 Golden Layout 自身的 DOM 子树中。布局变化时,组件根元素的祖先节点可能被重新挂载(reparented)。
- 虚拟模式(Virtual):组件根元素不进入Golden Layout 的 DOM 子树,而是由应用自行管理。布局变化时组件元素不会被 reparent,Golden Layout 仅通过事件"通知"应用应如何摆放组件。
文档定义了四种绑定方式,下文逐一展开。
二、Embedding via Registration:经典注册式绑定
这是 Golden Layout 最早也最常用的绑定方式。应用把组件的构造函数或工厂函数注册到布局中,当布局需要某个组件的新实例时,Golden Layout 负责实例化:
- 实例化时,Golden Layout 传入一个
ComponentContainer对象,其中包含一个 HTML 元素(container.element); - 构造函数/工厂函数创建组件对象,并把组件顶层 HTML 元素挂为
container.element的子节点; - 此后组件即成为 Golden Layout DOM 层级的一部分,布局重排时祖先节点会相应地被 reparent。
可用的注册函数
文档列出了五个注册入口,全部定义在 golden-layout.ts:
| 注册函数 | 说明 |
|---|---|
GoldenLayout.registerComponent() | 通用入口,根据传入函数是否拥有prototype属性自动分派到构造函数或工厂函数(见 golden-layout.ts),已标记 deprecated |
GoldenLayout.registerComponentConstructor() | 注册构造函数,new方式实例化 |
GoldenLayout.registerComponentFactoryFunction() | 注册工厂函数,以函数调用方式实例化 |
GoldenLayout.registerComponentFunction() | 注册"根据 config 返回构造函数"的回调,已标记 deprecated,建议改用下一条 |
GoldenLayout.registerGetComponentConstructorCallback() | 注册回调,在组件类型未注册时按需提供构造函数 |
从源码看,注册的组件类型存放在_componentTypesMap中,注册信息包含constructor、factoryFunction与virtual三个字段(golden-layout.ts)。构造函数与工厂函数的签名分别为:
type ComponentConstructor = new(container: ComponentContainer, state: JsonValue | undefined, virtual: boolean) => ComponentContainer.Component; type ComponentFactoryFunction = (container: ComponentContainer, state: JsonValue | undefined, virtual: boolean) => ComponentContainer.Component | undefined;注意:同一类型名重复注册会抛出BindError(Component is already registered),源码见 golden-layout.ts。
组件构造函数的典型形态
以仓库 apitest 示例中的 text-component.ts 为参照,一个经典的嵌入组件构造如下:接收container、state、virtual三个参数,并将自身元素挂入container.element:
class TextComponent { constructor(container: ComponentContainer, state: JsonValue | undefined, virtual: boolean) { // 非 virtual 模式下,rootElement 即 container.element const rootElement = container.element; // ...创建业务 DOM、绑定事件 } }在 component-base.ts 中可以看到两种模式的根元素策略:virtual === false时_rootElement = this._container.element(嵌入到容器内);virtual === true时则新建一个position: absolute; overflow: hidden的 div(虚拟模式独立挂载)。
三、Embedding via Events:事件式嵌入
当应用希望对组件分配拥有更强控制权时,可以不注册而改用事件。只要给VirtualLayout.bindComponentEvent事件挂上处理器,每当需要新组件时该事件即被触发。处理器需要:
- 创建或获取组件;
- 确保组件顶层 HTML 元素成为
container.element的子节点; - 以
BindableComponent接口返回组件,且virtual: false。
组件从布局中移除时,需要把组件顶层元素从container.element中移除,其他"拆除"(tear-down)动作同样在此阶段完成。文档给出两个时机,二者都会在组件不再被需要时触发(若已挂处理器):
VirtualLayout.unbindComponentEvent事件;- 组件容器的
beforeComponentRelease事件(从 component-container.ts 可知,releaseComponent()会先 emitbeforeComponentRelease,再调用layoutManager.unbindComponent())。
事件绑定在 apitest 中的实现
apitest/app.ts 给出了完整范例:handleBindComponentEvent中通过_useVirtualEventBinding开关决定返回virtual: true还是virtual: false。嵌入模式下的解绑注释明确写道:"If embedded, then component handles unbinding of component elements from content.element"——即嵌入模式下组件自己负责元素清理。
四、Virtual via Events:纯事件虚拟组件(Virtual Components)
这是版本 2 引入、控制力最强的方式。虚拟模式下 Golden Layout完全不知道组件内部结构,组件 HTML 元素也不在其 DOM 层级中。布局引擎只负责计算,通过事件把"需要的位置、尺寸、可见性、z-index"告知应用,由应用自行应用这些值。文档用虚拟表格(virtual grids)作类比:网格不包含展示内容,而是通过事件请求内容。
虚拟组件的四大优势
- 布局变化时组件及其祖先不会被 reparent,从而避免破坏 iframe、WebSocket 等对 DOM 挂载位置敏感的资源;
- 无需再从组件中"抽取"顶层 HTML 元素(在注册式嵌入中这一步往往很别扭);
- 对 Angular、Vue 等自带组件层级的框架友好:无需为了插入 Golden Layout 而打破框架的父子关系规范,组件元素的"传送"(teleporting)不再是必须的;
- 调试更轻松:应用组件直接挂在 Golden Layout 根元素下,应用相关的 DOM 层级明显更浅。
需要处理的全部事件(含源码级实现对照)
1.VirtualLayout.bindComponentEvent: (container, itemConfig) => ComponentContainer.BindableComponent
每当 Golden Layout 需要绑定新组件时触发。处理器接收container与组件条目解析后的配置itemConfig,典型流程:
- 用
itemConfig创建或获取组件; - 取得组件顶层 HTML 元素;
- 确保该元素
position: absolute; - 把元素挂到 Golden Layout 根 HTML 元素下;
- 以
container为键把组件存入 map; - 给 container 挂上
virtualRectingRequiredEvent与virtualVisibilityChangeRequiredEvent(以及可选的virtualZIndexChangeRequiredEvent)处理器; - 返回
{ component, virtual: true }形式的BindableComponent。
文档示例(可在 apitest/app.ts 找到几乎一致的真实实现):
private handleBindComponentEvent(container: ComponentContainer, itemConfig: ResolvedComponentItemConfig) { const componentTypeName = ResolvedComponentItemConfig.resolveComponentTypeName(itemConfig); if (componentTypeName === undefined) { throw new Error('handleBindComponentEvent: Undefined componentTypeName'); } const component = this.createVirtualComponent(container, componentTypeName, itemConfig.componentState); const componentRootElement = component.rootHtmlElement; this._layoutElement.appendChild(componentRootElement); this._boundComponentMap.set(container, component); container.virtualRectingRequiredEvent = (container, width, height) => this.handleContainerVirtualRectingRequiredEvent(container, width, height); container.virtualVisibilityChangeRequiredEvent = (container, visible) => this.handleContainerVisibilityChangeRequiredEvent(container, visible); return { component, virtual: true, }; }resolveComponentTypeName的实现位于 resolved-config.ts:仅当itemConfig.componentType为字符串时才返回类型名,否则返回undefined。
2.VirtualLayout.unbindComponentEvent: (container) => void
组件从布局中移除时触发。处理器以container为键在 map 中找到组件,将其从 Golden Layout 根元素下移除,并从 map 中删除:
private handleUnbindComponentEvent(container: ComponentContainer) { const component = this._boundComponentMap.get(container); if (component === undefined) { throw new Error('handleUnbindComponentEvent: Component not found'); } const componentRootElement = component.rootHtmlElement; if (componentRootElement === undefined) { throw new Error('handleUnbindComponentEvent: Component does not have a root HTML element'); } this._layoutElement.removeChild(componentRootElement); this._boundComponentMap.delete(container); }从 virtual-layout.ts 可以看到底层分发逻辑:若unbindComponentEvent存在则调用之;否则在非 virtual 模式下回退到已废弃的releaseComponentEvent。
3.LayoutManager.beforeVirtualRectingEvent: () => void
该事件不必处理,但可用于优化定位性能。一次布局变化可能伴随多个组件需要重排,此事件在一次布局变化引发的"一批"定位开始前触发一次。典型用法:调用 Golden Layout 根元素的getBoundingClientRect()并缓存,供后续每个组件的定位计算复用:
private handleBeforeVirtualRectingEvent(count: number) { this._goldenLayoutBoundingClientRect = this._layoutElement.getBoundingClientRect(); }底层支撑可见 layout-manager.ts:endVirtualSizedContainerAdding()会批量收集需要重排的虚拟容器,先fireBeforeVirtualRectingEvent(count),再逐个notifyVirtualRectingRequired(),最后fireAfterVirtualRectingEvent()。此外还有一个对偶的afterVirtualRectingEvent钩子(layout-manager.ts)。
4.ComponentContainer.virtualRectingRequiredEvent: (container, width, height) => void
组件的位置与/或尺寸需要改变时触发。处理器以container为键查组件,然后:
- 获取 Golden Layout 根元素的位置(
getBoundingClientRect();也可复用beforeVirtualRectingEvent中缓存的结果); - 获取 container 自身位置(
getBoundingClientRect()); - 计算 container 相对 Golden Layout 根元素的位置偏移;
- 更新组件顶层元素的
left、top、width、height。
private handleContainerVirtualRectingRequiredEvent(container: ComponentContainer, width: number, height: number) { const component = this._boundComponentMap.get(container); if (component === undefined) { throw new Error('handleContainerVirtualRectingRequiredEvent: Component not found'); } const rootElement = component.rootHtmlElement; if (rootElement === undefined) { throw new Error('handleContainerVirtualRectingRequiredEvent: Component does not have a root HTML element'); } const containerBoundingClientRect = container.element.getBoundingClientRect(); const left = containerBoundingClientRect.left - this._goldenLayoutBoundingClientRect.left; rootElement.style.left = this.numberToPixels(left); const top = containerBoundingClientRect.top - this._goldenLayoutBoundingClientRect.top; rootElement.style.top = this.numberToPixels(top); rootElement.style.width = this.numberToPixels(width); rootElement.style.height = this.numberToPixels(height); }numberToPixels在 utils/utils.ts 中实现,即value + 'px'。事件触发时机可由 component-container.ts 的setSizeToNodeSize()/notifyVirtualRectingRequired()佐证。
5.ComponentContainer.virtualVisibilityChangeRequiredEvent: (container, visible) => void
组件可见性变化时触发,通过display属性切换:
private handleContainerVisibilityChangeRequiredEvent(container: ComponentContainer, visible: boolean) { const component = this._boundComponentMap.get(container); // ...查表与判空 if (visible) { componentRootElement.style.display = ''; } else { componentRootElement.style.display = 'none'; } }Golden Layout 内置的注册式虚拟绑定也使用同样的工具函数setElementDisplayVisibility(utils/utils.ts)。
6.ComponentContainer.virtualZIndexChangeRequiredEvent: (container, logicalZIndex, defaultZIndex) => void
组件的 z-index 需要变化时触发。处理器应将组件 z-index 设为参数defaultZIndex:
private handleContainerVirtualZIndexChangeRequiredEvent(container: ComponentContainer, logicalZIndex: LogicalZIndex, defaultZIndex: string) { // ...查表与判空 componentRootElement.style.zIndex = defaultZIndex; }logicalZIndex的取值范围在 utils/types.ts 中定义:'base' | 'drag' | 'stackMaximised';defaultZIndex由LogicalZIndexToDefaultMap映射为具体 CSS 值(对应StyleConstants中的默认 z-index 常量)。在 component-container.ts 中,setLogicalZIndex()会在此值变化时调用notifyVirtualZIndexChangeRequired()触发该事件——例如组件进入拖拽(LogicalZIndex.drag)或 stack 最大化(LogicalZIndex.stackMaximised)状态时。
关于 apitest 的实践提示
文档明确指出apitest 应用演示了虚拟组件的完整实现,见 apitest/app.ts 与 apitest/component-base.ts。在 apitest 中,App构造函数通过new GoldenLayout(layoutElement, bindHandler, unbindHandler)注入事件处理器,并设置beforeVirtualRectingEvent缓存布局根元素位置;ComponentBase实现了GoldenLayout.VirtuableComponent接口,提供rootHtmlElementgetter,并在虚拟模式下将根元素设为absolute+overflow: hidden。
虚拟模式的定位思维
使用虚拟组件时,请把 Golden Layout 当作一个计算位置的引擎而非实际摆放组件的容器。这种绑定方式初始化成本更高,但换来了更大的灵活性:
- 任意 HTML 元素都可作为组件的父容器(不限于 Golden Layout 容器);
- 不同组件可以拥有不同的父容器,从而继承不同的 CSS 或采用不同的事件传播处理方式。
五、Virtual via Registration:注册式虚拟化(混合模式)
上面的六个事件给了应用极大灵活性,但集成成本也高。如果只想低成本获得虚拟组件的收益,可以采用"注册式虚拟化":组件仍然像经典方式一样注册,但注册时打上virtual: true标记,Golden Layout 内部便会按虚拟组件方式处理它,并在内部自行管理那些事件——应用无需编写任何事件处理器。
迁移四步法(对既有应用几乎零侵入)
文档给出了从经典注册升级到虚拟注册的四个步骤:
- 注册函数新增
virtual参数:默认false(经典嵌入绑定),设为true表示该类型组件在内部以虚拟组件方式实现。该参数在 golden-layout.ts 与 golden-layout.ts 的registerComponentConstructor/registerComponentFactoryFunction签名中均可确认。 - 组件提供
rootHtmlElementgetter:TypeScript 组件应实现GoldenLayout.VirtuableComponent接口。接口定义见 golden-layout.ts:仅要求rootHtmlElement: HTMLElement。 - 根元素的
overflowCSS 属性须设为hidden。 - 确保 Golden Layout 容器 HTML 元素处于定位状态(即其
position属性非static)。
注册式虚拟绑定的底层行为(源码佐证)
在 golden-layout.ts 的bindComponent()中可以看到注册式虚拟化的完整内部流程:
- 从注册表取到
instantiator后,以其virtual标记判断走虚拟路径; - 虚拟路径下,实例化组件后将其强转为
VirtuableComponent,取rootHtmlElement; - 调用
ensureElementPositionAbsolute(rootElement)(见 utils/utils.ts)强制设置position: absolute; - 把根元素
appendChild到 Golden Layout 容器下,登记到_virtuableComponentMap; - 内部自动挂上
virtualRectingRequiredEvent、virtualVisibilityChangeRequiredEvent、virtualZIndexChangeRequiredEvent三个处理器,处理器实现位于 golden-layout.ts,与文档中事件式虚拟组件的手写处理器逻辑完全一致(相对getBoundingClientRect()偏移定位、display切换、zIndex赋值)。
解绑路径同样在内部处理:unbindComponent()(golden-layout.ts)会从容器移除根元素并清理_virtuableComponentMap。
需要注意的三处行为差异
文档明确提醒,迁移后会有少量行为变化:
- Golden Layout 会确保组件根元素为
absolute定位; - Golden Layout 会直接修改根元素的高度与宽度。嵌入绑定修改的是容器元素而非组件根元素的尺寸——如果你的应用自己也设置了组件根元素的高宽,需要调整设计。简便做法:给当前根元素套一个新父元素,让新父元素成为组件的根元素,业务逻辑继续使用原元素,Golden Layout 操作新根元素;
- Golden Layout 会修改组件根元素的 z-index。
一个限制
virtual via registration绑定不支持GoldenLayout.registerGetComponentConstructorCallback()注册函数。
六、多绑定方式共存:绑定顺序与优先级
应用可以针对不同组件类型混用多种绑定方式。每当需要绑定组件时,Golden Layout 按下述顺序尝试(见 golden-layout.ts 与 virtual-layout.ts 的分发逻辑):
- 先查注册表:若该类型已注册,则按注册信息绑定(含注册式虚拟化);
- 再查
bindComponentEvent处理器:若存在,以事件方式绑定为虚拟组件; - 再查
getComponentEvent处理器:若存在,以事件方式将组件静态嵌入 Golden Layout DOM(该方法已废弃); - 以上皆无则抛出异常:
BindError(ComponentTypeNotRegisteredAndBindComponentEventHandlerNotAssigned)。
当同时使用 'Virtual via Events' 与 'Embedding via Events' 时,unbindComponentEvent处理器可以通过ComponentContainer.virtual字段(component-container.ts)判断某个组件究竟采用了哪种绑定方式,从而决定解绑动作(apitest 中 app.ts 正是这样做的:container.virtual为 true 时才从布局根元素移除,嵌入模式交由组件自行清理)。
七、VirtualLayout 类:无注册功能的轻量布局
Golden Layout 的继承层级为:LayoutManager→VirtualLayout→GoldenLayout(源码分别在 layout-manager.ts、virtual-layout.ts、golden-layout.ts)。
VirtualLayout实现了 Golden Layout 除注册函数外的全部功能。如果应用只打算用bindComponentEvent做纯虚拟组件,可以直接创建VirtualLayout实例,无需引入GoldenLayout。这一设计意味着纯事件驱动的应用可以完全绕开注册表与_componentTypesMap。此外,VirtualLayout构造函数还支持把bindComponentEvent/unbindComponentEvent处理器作为第二、第三参数直接传入(virtual-layout.ts),此时构造流程是确定性的(determinate),即使是为 popout 子窗口创建实例也会立即调用init(),处理器须随时就绪。
getComponentEvent与releaseComponentEvent两个旧事件在VirtualLayout上保留但已标记 deprecated,官方建议改用bindComponentEvent/unbindComponentEvent配合虚拟组件(virtual-layout.ts)。
八、选型场景速查表
| 场景 | 推荐绑定方式 | 理由 |
|---|---|---|
| 快速上手 | Embedding via Registration | 经典用法,注册即用,文档与示例最丰富 |
| 既有应用兼容 | Embedding via Registration | 现有注册代码零改动,自动沿用经典绑定 |
消除getComponentEvent弃用警告 | Embedding via Events | 以bindComponentEvent+virtual: false快速替换 |
| 低成本获得虚拟化收益 | Virtual via Registration | 注册时virtual: true,仅需实现rootHtmlElement并做少量 CSS/定位调整 |
| 最大设计自由度 | Virtual via Events | 完全掌控组件创建、销毁与定位,支持自定义父容器与多父容器架构 |
结语
四种绑定方式的本质差异在于"组件根元素归属权"与"定位职责"的分配:Embedding 把组件收编进布局 DOM 子树,由 Golden Layout 直接摆布;Virtual 则把组件留在应用侧,Golden Layout 退化为纯计算引擎,通过bindComponentEvent、unbindComponentEvent、virtualRectingRequiredEvent、virtualVisibilityChangeRequiredEvent、virtualZIndexChangeRequiredEvent、beforeVirtualRectingEvent六个事件与ComponentContainer.BindableComponent契约完成协作。对 iframe/WebSocket 敏感、Angular/Vue 等框架应用而言,虚拟模式能显著降低布局变化带来的 DOM 重挂载风险,是版本 2 起最值得采用的绑定策略;而注册式虚拟化则为存量应用提供了一条近乎无痛的升级路径。实践时可直接对照仓库 apitest 的可运行示例,并深入阅读 virtual-layout.ts、golden-layout.ts 与 component-container.ts 三个核心文件验证行为细节。
- 前端
- UI组件
【免费下载链接】golden-layout
A multi window layout manager for webapps
相关推荐
Golden Layout组件绑定完全指南:从基础绑定到动态更新的全流程
Golden Layout组件绑定完全指南:从基础绑定到动态更新的全流程 Golden Layout是一个强大的多窗口布局管理器,能够帮助开发者轻松创建复杂的W
前端UI组件naive-ui Date Picker 组件完全指南:11 种类型、格式化绑定与面板定制
naive ui Date Picker 组件完全指南:11 种类型、格式化绑定与面板定制 本文以 naive ui 官方 Date Picker 演示文档(
前端UI组件TileLang GPU内核开发:20行Python写出FP16 GEMM
TileLang GPU内核开发:20行Python写出FP16 GEMM 约80行Python就能让MLA解码在H100上追平手写FlashMLA,FP16的
编译器编程语言高性能计算人工智能深度学习
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考