GrapesJS Layer API 完全指南:掌握 Style Manager 属性栈层(Layer)的增删改查与预览
2026/9/12 2:54:01 网站建设 项目流程

GrapesJS Layer API 完全指南:掌握 Style Manager 属性栈层(Layer)的增删改查与预览

【免费下载链接】grapesjsFree and Open source Web Builder Framework. Next generation tool for building templates without coding项目地址: https://gitcode.com/GitHub_Trending/gr/grapesjs

导读

本文围绕 GrapesJS 开源 Web 构建器框架中 Style Manager(样式管理器)的核心数据结构之一——Layer(层)展开。Layerstack(栈)类型属性的基本组成单元,典型的场景如box-shadow(多重阴影)、text-shadow(多重文本阴影)等可以叠加多组取值的 CSS 属性。读完本文,你将掌握 Layer 的完整 API(getIdgetIndexgetValuesgetLabelisSelectedselectremovemovegetStylePreviewhasPreview),并理解其与PropertyStack之间的代理关系以及视图层如何消费这些 API 实现可视化编辑。


一、Layer 是什么:stack 类型属性的基本单元

在 GrapesJS 的 Style Manager 中,绝大多数属性(Property)与 CSS 属性一一对应,取值是单一值。但有一类属性允许一组属性值的叠加,即stack(栈)类型,其底层模型为PropertyStack(见 PropertyStack 源码),而stack中每一组独立的取值就是一个Layer

一个最典型的应用是box-shadow:一条阴影由xyblurspreadcolor等多个子值组成,同时一个元素可以同时存在多条阴影。此时每一条阴影对应一个Layer,多个 Layer 组合起来才是一条完整的box-shadow声明。在 Home.md 中可以看到官方给出的box-shadow配置示例:

{ name : 'Box shadow', property : 'box-shadow', type : 'stack', preview : true, // List of nested properties, available only for 'stack' and 'composite' types properties : [{ name: 'Shadow type', property: 'shadow-type', type: 'select', defaults: '', list: [ { value : '', name : 'Outside' }, { value : 'inset', name : 'Inside' }], },{ name: 'X position', property: 'shadow-x', type: 'integer', units: ['px','%'], defaults : 0, },{ name: 'Y position', property: 'shadow-y', type: 'integer', units: ['px','%'], defaults : 0, },{ name: 'Blur', property: 'shadow-blur', type: 'integer', units: ['px'], defaults : 0, min: 0, }] }

注意:stack/composite 类型的嵌套属性不要求使用真实的 CSS 属性名(如shadow-x),因为它们的值最终会按顺序合并到父属性(如box-shadow: X Y blur spread color)中。

从源码结构看,Layer.ts 是基于 BackboneModel的封装,其内部唯一的状态字段是values(一个以子属性 id 为键、取值为键值的对象)。每个 Layer 都隶属于某个PropertyStack,并通过collection引用获取所属的栈属性(this.prop = cl?.prop),这正是 Layer 的大量方法可以"委托"给 PropertyStack 实现的原因。

获取 Layer 实例的入口

Layer 实例不会孤立存在,通常通过所属的PropertyStack来获取。相关的获取方法定义在 PropertyStack.ts 中:

// 获取属性对应的 Property 实例后: const layers = property.getLayers(); // 所有层 const layer = property.getLayer(0); // 按索引取层,默认第 0 个 property.selectLayer(layer); // 选中某个层 property.selectLayerAt(1); // 按索引选中层 const selected = property.getSelectedLayer(); // 当前选中层(可能为 undefined)

PropertyStack还提供了层的增删改操作:addLayer(props, opts)opts.at指定插入位置,默认追加到末尾)、removeLayer(layer)removeLayerAt(index)moveLayer(layer, index)。这些方法与 Layer 自身的方法(removemove)本质上是同一套能力的两个入口——Layer 方法内部最终也是调用 PropertyStack 的对应实现。


二、Layer 核心 API 详解

以下逐一说明 docs/api/layer.md 中定义的 10 个方法,每个方法均可在 Layer.ts 源码 中找到对应实现。

1.getId():获取层 id

getId(); // 返回值: String

返回层的唯一标识。实现上直接返回 Backbone 模型的cid(client id):

getId() { return this.cid; }

cid由框架在模型创建时自动分配,无需手动维护,可放心用于层实例的识别与缓存。

2.getIndex():获取层索引

getIndex(); // 返回值: Number

返回该层在所属集合中的位置(从 0 开始)。实现逻辑为:

getIndex() { const coll = this.collection; return coll ? coll.indexOf(this) : -1; }

若该层已不属于任何集合(例如已被移除),返回-1。索引在层被新增、删除、移动后会实时变化,因此不宜长期缓存使用。

3.getValues(opts):获取层取值

getValues(opts = {}); // opts.camelCase: Boolean(可选)——是否以 camelCase 形式返回属性名 // 返回值: Object

返回当前层所有子属性的值,键为子属性 id,值为对应的 CSS 值字符串:

getValues(opts = {}) { const values = this.get('values')!; return opts.camelCase ? Object.keys(values).reduce((res, key) => { res[camelCase(key)] = values[key]; return res; }, {}) : values; }

两个注意点:

  • camelCase选项:GrapesJS 内部大量使用带连字符(kebab-case)的 id(如shadow-xmargin-top),而某些场景(如与 JS 对象互操作、自定义标签拼接)希望使用 camelCase(shadowXmarginTop),此时传入{ camelCase: true }即可,转换由 mixins.ts 中的camelCase工具完成。
  • 默认取值兜底:当子属性在values中缺失时,PropertyStack.getStyleFromLayer会回退到该子属性的默认值(prop.getDefaultValue()),因此getValues()返回的可能只是"已显式设置"的值,读取时需要留意。

4.getLabel():获取层标签

getLabel(); // 返回值: String

返回该层在界面上展示的标签文本。实现是委托给所属 PropertyStack 的getLayerLabel

getLabel(): string { return this.prop?.getLayerLabel(this); }

而 PropertyStack.getLayerLabel 的默认行为是把各子属性值按属性顺序用空格拼接,过滤掉空值;如果配置了layerLabel回调,则完全由自定义逻辑生成:

// PropertyStack 配置项 layerLabel: (layer) => { const values = layer.getValues(); return `A: ${values['prop-a']} B: ${values['prop-b']}`; }

自定义回调接收(layer, { index, values, property }),其中index为层索引、values为层取值、property为所属的 PropertyStack 实例。这一配置的详细说明见 property_stack.md。

5.isSelected():判断层是否被选中

isSelected(); // 返回值: Boolean

判断当前层是否为所属属性栈中"当前选中"的层:

isSelected() { return this.prop?.getSelectedLayer() === this; }

PropertyStack.getSelectedLayer()会先校验选中层索引是否有效(layer.getIndex() >= 0),无效(如已被移除)则返回undefined

6.select():选中当前层

select();

将当前层设为所属属性的选中层:

select() { return this.prop?.selectLayer(this); }

底层对应 PropertyStack.selectLayer,通过set('selectedLayer', layer, { __select: true })实现。选中层在编辑器中具有重要意义——没有选中层时,对内部子属性所做的任何修改都不会生效PropertyStack.__upPropertiesif (!layer) return;直接返回),因此 UI 层总是保证有一个选中层存在。

7.remove():移除当前层

remove();

从所属栈中移除当前层:

remove() { return this.prop?.removeLayer(this); }

底层调用PropertyStack.removeLayer(layer),即this.layers.remove(layer)。层被移除后,PropertyStack会通过change事件触发目标样式更新(__upLayers__upTargetsStyleProps),最终反映到画布上的目标元素。

8.move(index):移动层到新索引

move(index); // index: Number —— 目标索引

将层移动到指定索引位置:

move(index: number) { return this.prop?.moveLayer(this, index); }

底层 PropertyStack.moveLayer 的实现要点:

  • 目标索引必须是有效索引(0 <= index < layers.length);
  • 目标索引与当前索引相同则不做任何操作;
  • 移动实现为"先移除再按索引插入":removeLayer(layer)layers.add(layer, { at: index })

由于 Layer 顺序直接决定box-shadow等多值属性的拼接顺序(栈的层序即 CSS 值顺序),move常用于实现 UI 中"上移/下移"或拖拽排序(LayersView.ts 中的StyleManagerSorter正是通过拖动触发这类移动)。

9.getStylePreview(opts):获取预览样式对象

getStylePreview(opts = {}); // 返回值: Object —— 样式对象

获取用于展示"预览色块/预览效果"的样式对象:

getStylePreview(opts: OptionStyleStack = {}): Record<string, any> { return this.prop?.getStylePreview(this, opts); }

底层 PropertyStack.getStylePreview 的逻辑是:只有当属性配置了preview: true时才返回样式对象,否则返回空对象

getStylePreview(layer, opts = {}) { let result = {}; const preview = this.get('preview'); if (preview) { result = this.getStyleFromLayer(layer, opts); } return result; }

optsPropertyStack.getStyleFromLayer完全一致,支持两个关键选项(详见 property_stack.md):

  • camelCase:返回的样式对象键名使用 camelCase;
  • number:限制数值类型子属性的结果范围,例如{ number: { min: -3, max: 3 } }——这在预览时非常有用,可以把极大的 blur 值"夹紧"到可观察范围而不影响真实 CSS 值。

getStyleFromLayer内部支持toStyle自定义函数;未配置时对非 detached 属性返回形如{ 'box-shadow': '1px 2px 3px #000' }的合并样式,对 detached 属性则逐子属性展开。数值类型子属性在传入number限制时,会通过PropertyNumber.parseValue进行钳制后再拼入样式值。

10.hasPreview():检查是否启用层预览

hasPreview(); // 返回值: Boolean

判断所属属性是否为该层开启了预览:

hasPreview() { return !!this.prop?.get('preview'); }

即直接读取 PropertyStack 的preview配置项(默认false)。注意hasPreview()不依赖具体层实例,同一个栈的所有层共享该开关。


三、视图层如何消费这些 API

理解 Layer API 在 UI 中的使用方式,能帮助你更准确地判断各方法的调用时机与返回值语义。核心视图是 LayerView.ts,它把 Layer 模型渲染为样式管理器中的一个可操作条目。

LayerView 在updateLabel()中消费了三个 API:

updateLabel() { const label = model.getLabel(); this.getLabelEl().innerHTML = label; if (model.hasPreview()) { const prvEl = this.getPreviewEl(); const style = model.getStylePreview({ number: { min: -3, max: 3 } }); const styleStr = keys(style) .map((k) => `${k}:${style[k]}`) .join(';'); prvEl.setAttribute('style', styleStr); } }

可以看到:getLabel()驱动标签文本,hasPreview()决定是否渲染预览块,getStylePreview({ number: { min: -3, max: 3 } })生成预览块的内联样式——注意这里官方视图正是以number限制参数调用,印证了上一节提到的"预览钳制"用途。

交互层面,LayerView 的事件映射揭示了各 API 的触发入口:

events() { return { click: 'select', // 点击层条目 -> layer.select() 'click [data-close-layer]': 'removeItem', // 点击关闭按钮 -> layer.remove() 'mousedown [data-move-layer]': 'initSorter', // 按住拖拽手柄 -> 触发排序器 'touchstart [data-move-layer]': 'initSorter', }; }

即:单击层条目调用select(),点击关闭图标调用remove(),拖拽手柄则通过StyleManagerSorter完成排序(排序过程中通过moveLayer修改索引)。而updateVisibility()model.isSelected()决定子属性编辑区是否展示——只有选中层才展开其子属性输入框。这解释了"没有选中层时子属性修改无效"的设计:未选中层根本不渲染子属性输入。

该视图对应的渲染测试见 PropertyStackView 测试,其中验证了层容器(.layers)、层包装器([data-layers-wrapper])、添加按钮(#add)等 DOM 结构的渲染,可作为阅读视图逻辑的参考。


四、实战:用代码操作一个 box-shadow 栈的层

结合前述 API,这里给出一个完整的编程式操作示例,覆盖 Layer 的查询、选中、取值、增删与移动(相关入口方法均定义于 PropertyStack.ts 与 Layer.ts):

// 1. 从样式管理器拿到目标属性(假设已在配置中定义了 box-shadow 栈) const property = editor.Styles.getProperty('sector-id', 'box-shadow'); // 若不存在可动态添加 // const property = editor.Styles.addProperty('sector-id', { type: 'stack', property: 'box-shadow', ... }); // 2. 新增两个层(addLayer 会为缺失的子属性补默认值) const layerA = property.addLayer({ 'shadow-x': '1px', 'shadow-y': '1px' }); const layerB = property.addLayer({ 'shadow-x': '3px', 'shadow-y': '3px' }, { at: 0 }); // 插到最前面 // 3. 遍历与读取 property.getLayers().forEach((layer) => { console.log('id:', layer.getId()); // String console.log('index:', layer.getIndex()); // Number console.log('values:', layer.getValues()); // { 'shadow-x': '...', ... } console.log('camelCase:', layer.getValues({ camelCase: true })); // { shadowX: '...', ... } console.log('label:', layer.getLabel()); }); // 4. 选中与判断 property.selectLayerAt(0); // 或 layer.select() const selected = property.getSelectedLayer(); console.log('is selected:', layerB.isSelected()); // 取决于当前选中 // 5. 预览样式(preview: true 时才有内容) console.log(layerB.getStylePreview({ number: { min: -3, max: 3 } })); console.log('hasPreview:', layerB.hasPreview()); // 6. 移动与移除 layerB.move(1); // 移动到索引 1 layerA.remove(); // 移除 layerA,等价于 property.removeLayer(layerA)

执行后目标元素的box-shadow样式会随层的增删改实时更新——这正是PropertyStack在层集合发生add/remove时通过__upLayersgetStyleFromLayers将各层样式按layerJoin/layerSeparator拼接并同步到目标元素(__upTargetsStyleProps)的机制。


五、小结

Layer是 GrapesJS Style Manager 中stack属性(如box-shadowtext-shadow)的取值单元,其 API 设计遵循"薄模型 + 委托 PropertyStack"的模式:

  • 只读查询getIdgetIndexgetValues({ camelCase })getLabelisSelectedhasPreviewgetStylePreview(opts)
  • 写操作selectremovemove(index)

在界面交互中,LayerView 通过getLabel/hasPreview/getStylePreview渲染条目与预览,通过click/关闭按钮/拖拽手柄分别触发select/remove/排序移动,并通过isSelected控制子属性编辑区的显隐。掌握这套 API 后,你既可以在样式管理器配置层面通过layerLabelpreviewlayerSeparatorlayerJoinemptyValue等 PropertyStack 配置 定制体验,也可以在业务代码中直接编程式地构建、查询与操作多层 CSS 值。

更多关联阅读:PropertyStack API 文档、PropertyComposite 源码、Style Manager 模块文档。

【免费下载链接】grapesjsFree and Open source Web Builder Framework. Next generation tool for building templates without coding项目地址: https://gitcode.com/GitHub_Trending/gr/grapesjs

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

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

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

立即咨询