☰
GrapesJS 组件体系完全指南:Component Manager 工作原理与自定义组件类型实战
2026/9/27 0:14:34 网站建设 项目流程

GrapesJS 组件体系完全指南:Component Manager 工作原理与自定义组件类型实战

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

组件(Component)是 GrapesJS 模板结构中一切元素的根基——从一张图片、一段文本,到由众多子组件组成的 section、页面,乃至整个画布根节点 wrapper,无一例外都是组件。本指南以 docs/modules/Components.md 为骨架,结合 packages/core/src/dom_components 的源码实现,完整讲解组件从 HTML 字符串解析、类型识别、Model/View 分工,到自定义组件类型、生命周期钩子、组件级 CSS 与 JSX 语法优化的全流程。读完你将掌握如何为编辑器注册可识别、可编辑、可导出、行为完全可控的自定义组件类型。

适用版本:本文涉及内容要求 GrapesJS v0.15.8 及以上;其中isParsedNode相关内容要求 v0.23.1 及以上,"组件与 CSS"一节要求 v0.17.27 及以上(均以当前仓库packages/core源码为准)。

组件是如何工作的:从 HTML 字符串到画布

组件是模板中的基础元素,它既可以像图片、文本框一样简单原子,也可以是由其他组件复合而成的复杂结构(如 section、页面)。组件的设计初衷,是允许开发者把不同的行为绑定到不同的元素上——例如,双击图片打开 Asset Manager(资源管理器),就是绑定在该类元素上的自定义行为。

下面我们沿着"向编辑器添加一段 HTML 字符串"这条路径,逐步拆解组件从无到有的全过程。以下代码片段都可以在编辑器初始化之后,直接在浏览器控制台中运行验证。

向画布添加组件

最直接的添加方式是调用addComponents传入 HTML 字符串:

// 直接将组件追加到画布 editor.addComponents(`<div> <img src="https://path/image" /> <span title="foo">Hello world!!!</span> </div>`); // 或追加到某个已存在的组件中 // 例如,追加到当前选中的组件: editor.getSelected().append(`<div>...`); // 实际上,editor.addComponents 是下面的别名: editor.getWrapper().append(`<div>...`);

从源码看,addComponents最终委托给 wrapper 子组件集合的add方法——在 packages/core/src/dom_components/index.ts 中,getWrapper()返回画布根组件(相当于 HTML 页面中的<body>),addComponent等价于this.getComponents().add(...)。

如果需要在特定位置插入组件,可以使用at选项指定索引。例如,要把组件添加到同集合所有子组件的最顶部:

component.append('<div>...', { at: 0 });

或者插入到中间位置:

const { length } = component.components(); component.append('<div>...', { at: parseInt(length / 2, 10) });

Component Definition(组件定义)

第一步,HTML 字符串会被解析并转换为所谓的Component Definition(组件定义)。上述输入对应的结果大致如下(真实定义会更大一些,这里为简明做了裁剪):

{ tagName: 'div', components: [ { type: 'image', attributes: { src: 'https://path/image' }, }, { tagName: 'span', type: 'text', attributes: { title: 'foo' }, components: [{ type: 'textnode', content: 'Hello world!!!' }] } ] }

可以看到,这个结果与我们常说的Virtual DOM十分相似——它是 DOM 元素的轻量级表示。借助这种抽象,编辑器能够持续跟踪元素状态,并做出高性能友好的变更/更新。

tagName、attributes、components的含义一目了然,关键在于type属性:它指明了该Component Definition的组件类型(Component Type)(内置类型列表见下文)。如果省略type,将使用默认值type: 'default'。

那么,编辑器是如何从一段简单的 HTML 字符串推断出这些类型的?这一步骤被称为Component Recognition(组件识别),在下一节详细说明。

组件识别与组件类型栈(Component Type Stack)

当把 HTML 字符串作为组件传给编辑器时,字符串会被解析编译为带有type属性的 Component Definition。为了判断该给每个解析出的 HTML 元素赋予什么类型,编辑器会遍历所有已定义的组件(这些组件构成的集合即Component Type Stack,组件类型栈),并通过每个类型的isComponent方法(下文详述)检查该类型是否适用于当前元素。

组件类型栈本质是一个组件类型的数组,关键在于类型的排列顺序:

  • 任何新注册的自定义组件类型都会被压入栈顶;
  • 解析器产出的每个元素会从栈顶向栈底遍历这个栈;
  • 一旦某个类型的isComponent返回真值,遍历立即停止;
  • 栈的最后一个元素永远是default(兜底类型)。

这一逻辑在源码中有清晰印证:在 packages/core/src/parser/model/ParserHtml.ts 的detectNode方法中,解析器先检查节点是否带有data-gjs-type属性(有则直接使用该类型,跳过识别),否则遍历compTypes数组调用各类型的识别方法;而 packages/core/src/dom_components/index.ts 中维护着componentTypes数组(栈),addType时通过this.componentTypes.unshift(methods)把新类型放到栈顶(见 index.ts)。

从 v0.23.1 起,组件识别还可以依赖isParsedNode方法,它接收的是归一化后的解析节点而非 DOM 元素。如果一个组件同时定义了isParsedNode和isComponent,则isParsedNode优先——这一优先级同样体现在detectNode的实现中。

性能提示:如果导入的是大段 HTML 字符串(例如由 Blocks 定义的),可以通过直接传入 Component Definition 对象或使用 JSX 语法跳过解析与组件识别这两个较重步骤。JSX 的配置方式见文末"JSX 语法"一节。

Component 实例(Model)

当 Component Definition 就绪、类型确定后,就可以创建Component 实例(即Model)。回到前面的 HTML 字符串例子,append方法的返回值是一个由已添加组件构成的数组:

const component = editor.addComponents(`<div> <img src="https://path/image" /> <span title="foo">Hello world!!!</span> </div>`)[0];

Component 实例提供了读取和修改自身数据的属性和方法。用get读取属性,例如读取type:

const componentType = component.get('type'); // eg. 'image'

用set更新属性,这会立刻改变组件在画布中的行为:

// 让组件不可拖拽 component.set('draggable', false);

还可以使用getAttributes、setAttributes、components等方法:

const innerComponents = component.components(); innerComponents.forEach((comp) => console.log(comp.toHTML())); // 更新组件内部内容 component.components(`<div>Component 1</div><div>Component 2</div>`);

每个组件都可以定义自己的属性和方法,但它们都至少会继承default类型的实现(后面会看到如何创建自定义组件并扩展已有类型)。完整的属性和方法清单可查阅 Component API。

Component 的核心职责是跟踪自身数据并在需要时返回它们。一个最常见的需求是获取组件当前的 HTML:

const componentHTML = component.toHTML();

这会返回包含组件及其所有子节点 HTML 的字符串。组件同样实现了toJSON方法,因此可以这样获取其 JSON 结构:

JSON.stringify(component);

提示:对全部组件的存储与加载,应当依赖 Storage Manager 完成。

总之,Component 实例负责模板的最终数据(如 HTML、JSON)。例如,需要往 HTML 里新增/修改某个属性时,必须更新对应的组件(如component.addAttributes({ title: 'Title added' }))——Component/Model 是你唯一的Source of Truth(数据真相源)。

组件渲染(View)

组件的另一重要部分是如何在画布中被渲染,这由它的View负责。View 与最终导出的 HTML 数据没有任何关系:你可以让组件导出的 HTML 是一个大<div>...</div>,却在画布中渲染成一个简单的图片(想想复杂/动态数据的占位符场景)。

默认情况下,组件 View 会自动与 Model 的数据保持同步(没有 Model 就不存在 View)。当你更新组件属性或追加子组件时,画布中的视图都会随之更新。

但有些场景需要额外的逻辑来更好地处理组件呈现。试想让用户自由搭建<table>:你希望在画布上为表格添加自定义按钮,方便增删列/行。这类需求就可以依赖 View 实现——在 View 中追加额外的 DOM、绑定事件等,且这些附加物与<table>最终导出的 HTML(用户真正期望的结果)完全无关,因为它由 Model 负责。

组件渲染完成后,随时可以拿到它的 View 和 DOM 元素:

const component = editor.getSelected(); // 获取 View const view = component.getView(); // 获取 DOM 元素 const el = component.getEl();

一般而言,View 无需改动,默认实现已经处理了与 Model 的同步;只有当你需要更强的元素控制力(例如在画布中做自定义 UI)时,才需要创建自定义组件类型并扩展默认 View——这正是下一节的主题。

至此,组件的核心概念已经清晰:

  • Model/Component是模板最终代码(例如 HTML 导出)的source of truth;
  • View/ComponentView是编辑器在画布中向用户预览组件的呈现层。

内置组件类型

下面是当前仓库中内置的组件类型,按其(实际)在Component Type Stack中的位置从栈顶到栈底排列。对应源码均在 packages/core/src/dom_components/model 目录下:

类型 ID用途源码
cell处理<td>、<th>元素ComponentTableCell.ts
row处理<tr>元素ComponentTableRow.ts
table处理<table>元素ComponentTable.ts
thead处理<thead>元素ComponentTableHead.ts
tbody处理<tbody>元素ComponentTableBody.ts
tfoot处理<tfoot>元素ComponentTableFoot.ts
map处理嵌入地图(如 Google Maps)ComponentMap.ts
link处理<a>链接元素ComponentLink.ts
label正确处理<label>元素ComponentLabel.ts
video视频组件(YouTube/Vimeo 等嵌入)ComponentVideo.ts
image图片组件ComponentImage.ts
script处理<script>元素ComponentScript.ts
svg处理 SVG 元素ComponentSvg.ts
comment注释节点(对邮件编辑器可能有用)ComponentComment.ts
textnode类似 DOM 定义中的 textnode,即没有标签的文本元素ComponentTextNode.ts
text可内联编辑的简单文本组件ComponentText.ts
wrapper画布根组件(wrapper)的标识ComponentWrapper.ts
default默认基础组件Component.ts

补充:在 index.ts 的实际栈中还包含svg-in(内联 SVG)、iframe、head以及 data 相关的variable、condition、collection等类型,这里仅列出文档所覆盖的常规内置类型。

定义自定义组件类型

理解了组件的工作原理后,就可以开始探索如何创建自定义组件类型(Component Type)了。

第一条规则:定义新组件类型的代码必须放在插件(plugin)内。如果你希望在组件初始化(例如从数据库加载模板)之前就加载自定义类型,这一点是必须的。插件会在组件获取之前被加载(例如使用 Storage 的场景),因此是定义组件类型的绝佳位置。

const myNewComponentTypes = (editor) => { editor.DomComponents.addType(/* API for component type definition */); }; const editor = grapesjs.init({ container: '#gjs', // ... plugins: [myNewComponentTypes], });

假设我们希望让编辑器更好地理解和处理<input>元素。下面就是定义新组件类型的方式:

editor.DomComponents.addType('my-input-type', { // 让编辑器识别何时绑定 `my-input-type` isComponent: (el) => el.tagName === 'INPUT', // Model 定义 model: { // 默认属性 defaults: { tagName: 'input', draggable: 'form, form *', // 只能被拖放到 `form` 元素内部 droppable: false, // 内部不能拖入其他元素 attributes: { // 默认属性 type: 'text', name: 'default-name', placeholder: 'Insert text here', }, traits: ['name', 'placeholder', { type: 'checkbox', name: 'required' }], }, }, });

有了这段代码,编辑器就能识别普通的文本<input>,为其赋予默认属性,并在属性面板中展示合适的 traits 以便更好地处理属性。

提示:关于 traits 的详细机制,建议阅读其 专页文档——强烈建议在读完本文之后再阅读。

isComponent

先看看上面做了什么。首先是isComponent函数,它在"组件识别"一节中已经提及:编辑器在识别阶段用它来理解<input>。该函数只接收el参数(解析后的 HTMLElement 节点),当元素满足你的逻辑条件时返回真值。例如,向编辑器添加下面的 HTML 字符串:

// ...编辑器初始化之后 editor.addComponents(`<input name="my-test" title="hello"/>`);

得到的 Component Definition 将是:

{ type: 'my-input-type', attributes: { name: 'my-test', title: 'hello', }, }

如果需要,还可以通过返回一个对象来自定义识别后的 Component Definition:

editor.DomComponents.addType('my-input-type', { isComponent: el => { if (el.tagName === 'INPUT') { // 必须显式声明返回对象的 type, // 否则会使用 `default` 类型 const result = { type: 'my-input-type' }; if (/* 某些其他条件 */) { result.attributes = { title: 'Hi' }; } return result; } }, // ... });

务必保持isComponent函数尽可能简单!

请注意:该方法会接收到画布中解析出的任意节点(例如加载或添加时),而这些节点并非都拥有相同的接口(属性/方法)。如果你这样写:

// ... // 打印所有节点 isComponent: (el) => { console.log(el); return el.tagName === 'INPUT'; }, // ... editor.addComponents(`<div> I'm a text node <!-- I'm a comment node --> <img alt="Image here"/> <input/> </div>`);

你会看到所有节点都被打印出来。如果在isComponent里不加检查地调用el.getAttribute('...')(这在div上可用,但在text node上不可用),代码就会出错。

还需要理解:只有需要解析时才执行isComponent(例如以 HTML 字符串添加组件,或用fromElement初始化编辑器)。如果类型已经显式指定,isComponent不会执行。看几个例子:

// isComponent 会在 some-element 上执行 editor.addComponents('<some-element>...</some-element>'); // 对 OBJECT 不执行 isComponent // 如果对象没有 `type` 键,将使用 `default` 类型 editor.addComponents({ type: 'some-component', }); // 因为强制指定了类型,isComponent 不会执行 editor.addComponents('<some-element>editor.Components.addType('my-input-type', { isParsedNode: (node) => { if (node.tagName === 'input') { return { type: 'my-input-type', }; } }, // ... });

该方法接收一个归一化后的解析节点(parsed node),返回值与isComponent接受的值类型相同。如果同时提供了isParsedNode和isComponent,isParsedNode始终优先。

在 headless 解析模式下,既有的isComponent定义依然可以工作:当启用了自定义 HTML 解析器时,GrapesJS 会提供一个只读的合成元素(synthetic element),具备常见的 DOM 类似属性,如tagName、childNodes、children、getAttribute、textContent。如果你某条遗留检查需要额外的辅助方法,可以全局扩展该合成元素:

editor.Parser.config.customSyntheticElement = (SyntheticElement) => class MySyntheticElement extends SyntheticElement { get foo() { return this.getAttribute('data-foo') || ''; } };

Model

弄懂了isComponent之后,我们来探索model属性。model很可能是你用得最多的部分——它用于描述组件,首先映入眼帘的是defaults键,它代表默认组件属性,直接映射到前面讲过的 Component Definition。

Model 还决定你最终看到的 HTML 导出结果。你可能已经注意到模型中的tagName(未指定时默认div)和attributes属性。另一个重要属性(我们上面的 input 集成中没用它,因为<input/>不需要)是components,它定义默认的内部子组件:

defaults: { tagName: 'div', attributes: { title: 'Hello' }, // 可以是字符串 components: ` <h1>Header test</h1> <p>Paragraph test</p> `, // 一个组件定义 components: { tagName: 'h1', components: 'Header test', }, // 由字符串/组件定义构成的数组 components: [ { tagName: 'h1', components: 'Header test', }, '<p>Paragraph test</p>', ], // 或者一个函数,接收当前 model 作为参数, // 返回值必须是上述形式之一 components: model => { return `<h1>Header test: ${model.get('type')}</h1>`; }, }
读取与更新 Model

只要持有 model 的引用,就可以随时读写它的属性。以下是最常用 API 的一些参考:

// 使用选中的组件 const modelComponent = editor.getSelected(); // 获取所有 model 属性 const props = modelComponent.props(); // 获取单个属性 const tagName = modelComponent.get('tagName'); // 更新单个属性 modelComponent.set('tagName', '...'); // 更新多个属性 modelComponent.set({ tagName: '...', // ... }); // 一些辅助方法 // 获取全部属性 const attrs = modelComponent.getAttributes(); // 新增属性 modelComponent.addAttributes({ title: 'Test' }); // 替换全部属性 modelComponent.setAttributes({ title: 'Test' }); // 获取所有内部组件的集合 modelComponent.components().forEach((inner) => console.log(inner.props())); // 用 HTML 字符串/组件定义更新内部内容 const addedComponents = modelComponent.components(`<div>...</div>`); // 按查询字符串查找组件 modelComponent.find(`.query-string[example=value]`).forEach((inner) => console.log(inner.props()));

你会发现,任何变更都会同步反映到画布中的组件以及导出的代码上。

提示:完整的方法/属性清单请查阅 Component API。在 Component.ts 中可以看到,props()直接返回模型的全部属性(this.attributes),setAttributes/addAttributes内部则是对attributes键的set操作。

监听属性变化

如果需要在某个属性变化时执行某些操作,可以在init方法中设置监听器:

editor.DomComponents.addType('my-input-type', { // ... model: { defaults: { // ... someprop: 'initial value', }, init() { this.on('change:someprop', this.handlePropChange); // 监听任意属性变化 this.on('change:attributes', this.handleAttrChange); // 监听 title 属性的变化 this.on('change:attributes:title', this.handleTitleChange); }, handlePropChange() { const { someprop } = this.props(); console.log('New value of someprop: ', someprop); }, handleAttrChange() { console.log('Attributes updated: ', this.getAttributes()); }, handleTitleChange() { console.log('Attribute title updated: ', this.getAttributes().title); }, }, });

除init外还有其他生命周期方法,见下文"生命周期钩子"一节。

View

通常,在 GrapesJS 中创建组件时,你期望画布中显示的就是模型中所定义内容的预览。事实上,编辑器默认正是这样做的:当模型中任何内容变化(属性、标签等)时,同步更新画布中的元素,从而获得经典的WYSIWYG(所见即所得)体验。

但"最简单的不总是最正确的",为构建器制作组件时你会逐渐发现,有时需要更多能力:

  • 提升组件的编辑体验。最典型的例子是 TextComponent:它的视图内嵌了 RTE(富文本编辑器),用户双击即可快速编辑文本。为此你可能需要在 View 中添加对 DOM 事件的处理,甚至围绕组件添加自定义 UI 元素(例如按钮)。
  • DOM 呈现与实际期望不同,需要改变某些行为。例如 VideoComponent 通过 iframe 加载 YouTube 视频。iframe 加载完成后,其内部处于另一个上下文,编辑器无法感知:光标移到 iframe 上会与视频交互而非编辑器,导致组件都无法选中。为了规避这个问题,渲染时编辑器禁用了对 iframe 的指针交互,并用另一个元素将其包裹(没有 wrapper 的话,编辑器会选中父级组件)。显然,这些改动与最终代码毫无关系——导出结果始终是一个简单的 iframe。
  • 需要自定义内容,或从服务器获取数据填充。

以上所有场景,都可以在组件类型定义中使用view。<input>组件也许不是这个场景的最佳示例,但下面的例子覆盖了大多数需求:

editor.DomComponents.addType('my-input-type', { // ... model: { // ... }, view: { // 默认情况下,元素标签与 model 一致 tagName: 'div', // 用 `events` 轻松添加组件专属监听器 // 因为是组件专属的(例如无法在这里给 window 挂监听), // 组件被移除时无需手动清理,编辑器会自动管理 events: { click: 'clickOnElement', // 也可以使用事件委托, // 监听内部某个元素冒泡上来的事件 'dblclick .inner-el': 'innerElClick', }, innerElClick(ev) { ev.stopPropagation(); // ... // 需要时,可以从 View 的任意函数中访问 model this.model.components('Update inner components'); }, // 在 init 中创建监听器(与 model 类似),或启动其他初始化逻辑 init({ model }) { // 在 model 属性变化时于 View 中做点什么 this.listenTo(model, 'change:prop', this.handlePropChange); // 如果给外部对象挂了监听,务必在 `removed` 中解绑,避免内存泄漏 this.onDocClick = this.onDocClick.bind(this); document.addEventListener('click', this.onDocClick); }, // 元素从画布移除时触发的回调 removed() { document.removeEventListener('click', this.onDocClick); }, // 元素渲染完成后对内容做点处理。 // DOM 元素以 `el` 传入参数对象, // 也可以在任意函数中通过 `this.el` 访问 onRender({ el }) { const btn = document.createElement('button'); btn.value = '+'; // 这只是示例,请避免给内部元素直接加事件, // 此类场景应使用 `events` btn.addEventListener('click', () => {}); el.appendChild(btn); }, // 异步内容的示例 async onRender({ el, model }) { const asyncContent = await fetchSomething({ someDataFromModel: model.get('someData'), }); // 记住:这些改动只存在于编辑器画布内, // 任何 DOM 变更都不会被存入模板数据, // 如果需要持久化,请更新 model 属性 el.appendChild(asyncContent); }, }, });

从源码看,init、removed、onRender正是 ComponentView.ts 中定义的空钩子方法(init(opts) {}、removed(opts) {}、onRender(opts) {}),View 构造时会调用init,postRender阶段调用onRender并触发component:render相关事件,remove时调用removed——你的自定义实现只需覆写这些钩子即可。

更新组件类型

更新组件类型非常简单:

const domc = editor.DomComponents; domc.addType('some-component', { // 可以更新 isComponent 逻辑,也可以沿用 `some-component` 原有的 // isComponent: (el) => false, // 按需更新 model model: { // `defaults` 属性的处理方式不同: // 它会被与旧的 `defaults` 合并 defaults: { tagName: '...', // 覆盖旧值 someNewProp: 'Hello', // 新增属性 }, init() { // 覆盖 `some-component` 的 `init` 函数 }, }, // 按需更新 view view: {}, });

从 index.ts 的addType实现可以看到:若传入的类型已存在(compType),新定义会直接替换其model与view,并触发component:type:update事件;若不存在,则把新类型unshift到类型栈栈顶并触发component:type:add。此外,addType还支持block选项——传入true或块属性对象时,会自动为这个组件类型注册一个对应的 Block。

扩展组件类型

有时需要基于另一个类型创建新类型,直接使用extend和extendView指明要扩展的组件即可:

comps.addType('my-new-component', { isComponent: el => {/* ... */}, extend: 'other-defined-component', model: { ... }, // 将扩展 'other-defined-component' 的 model view: { ... }, // 将扩展 'other-defined-component' 的 view });
comps.addType('my-new-component', { isComponent: el => {/* ... */}, extend: 'other-defined-component', model: { ... }, // 将扩展 'other-defined-component' 的 model extendView: 'other-defined-component-2', view: { ... }, // 将扩展 'other-defined-component-2' 的 view });

扩展父级函数

当需要复用父类型的函数时,不必这样写:

domc.getType('parent-type').model.prototype.init.apply(this, arguments);

而可以使用extendFn和extendFnView选项:

domc.addType('new-type', { extend: 'parent-type', extendFn: ['init'], // 需要从 `parent-type` 扩展的 model 函数数组 model: { init() { // 做点什么 }, }, });

View 的扩展同理,使用extendFnView。源码中 addType 的getExtendedObj辅助函数 会依次调用父级函数与子级函数,实现"先父后子"的组合调用。

提示:如需获取当前全部组件类型,可以使用getTypes:

editor.DomComponents.getTypes().forEach((compType) => console.log(compType.id));

生命周期钩子(Lifecycle Hooks)

每个组件都会触发不同的生命周期钩子,让你可以在各个阶段注入自定义动作。钩子分为两类:全局(global)与局部(local)。

  • 局部钩子:在创建/扩展组件类型时定义(通常通过model/view中的某个方法),目的是响应该特定组件类型的事件;
  • 全局钩子:对任何组件都会无差别触发,通过editor.on(...)监听,适用于更通用的场景,也可以在其他组件内部监听。

下面是全部钩子的触发流程:

  • 局部钩子:model.init()方法,在组件 model 初始化完成后执行
  • 全局钩子:component:create事件,紧随model.init()之后触发,回调参数为 model。 例如editor.on('component:create', model => console.log('created', model))
  • 局部钩子:view.init()方法,在组件 view 初始化完成后执行
  • 局部钩子:view.onRender()方法,在组件渲染到画布后执行
  • 全局钩子:component:mount事件,紧随view.onRender()之后触发,回调参数为 model
  • 局部钩子:model.updated()方法,当 model 的某个属性被更新时执行
  • 全局钩子:component:update事件,在model.updated()之后触发,回调参数为 model; 也可以通过component:update:{propertyName}监听特定属性的变化
  • 局部钩子:model.removed()方法,在组件被移除时执行
  • 全局钩子:component:remove事件,在model.removed()之后触发,回调参数为 model

下面是一个全部钩子的示例用法:

editor.DomComponents.addType('test-component', { model: { defaults: { testprop: 1, }, init() { console.log('Local hook: model.init'); this.listenTo(this, 'change:testprop', this.handlePropChange); // 这里也可以用 editor.on('...') 监听全局钩子 }, updated(property, value, prevValue) { console.log('Local hook: model.updated', 'property', property, 'value', value, 'prevValue', prevValue); }, removed() { console.log('Local hook: model.removed'); }, handlePropChange() { console.log('The value of testprop', this.get('testprop')); }, }, view: { init() { console.log('Local hook: view.init'); }, onRender() { console.log('Local hook: view.onRender'); }, }, }); // 为自定义组件注册一个 Block editor.BlockManager.add('test-component', { label: 'Test Component', content: '<div>domc.addType('component-css', { model: { defaults: { attributes: { class: 'cmp-css' }, components: ` <span>Component with styles<span> <div class="cmp-css-a">Component A</div> <div class="cmp-css-b">Component B</div> `, styles: ` .cmp-css { color: red } .cmp-css-a { color: green } .cmp-css-b { color: blue } @media (max-width: 992px) { .cmp-css{ color: darkred; } .cmp-css-a { color: darkgreen } .cmp-css-b { color: darkblue } } `, }, }, });

这种方式允许编辑器将这些样式(即 CssRule 实例)分组管理,并在同一组件的所有引用都被移除时一并清理。

重要注意事项

在上面的例子中,我们使用了一个自定义组件和默认的子组件。样式只声明在自定义组件上,这意味着:如果从画布中移除全部.cmp-css-a和.cmp-css-b实例,它们的 CssRule 仍会保存在项目里(这里讨论的不是 CSS 导出——导出能够跳过未使用的规则——而是存储在项目 JSON 中的实例)。

更干净的做法是遵循面向组件的样式(component-oriented styling):只在组件自身的范围内声明样式。以上面的例子来说,应该是这样:

domc.addType('cmp-a', { model: { defaults: { attributes: { class: 'cmp-css-a' }, components: 'Component A', styles: ` .cmp-css-a { color: green } @media (max-width: 992px) { .cmp-css-a { color: darkgreen } } `, }, }, }); domc.addType('cmp-b', { model: { defaults: { attributes: { class: 'cmp-css-b' }, components: 'Component B', styles: ` .cmp-css-b { color: blue } @media (max-width: 992px) { .cmp-css-b { color: darkblue } } `, }, }, }); domc.addType('component-css', { model: { defaults: { attributes: { class: 'cmp-css' }, components: ['<span>Component with styles<span>', { type: 'cmp-a' }, { type: 'cmp-b' }], styles: ` .cmp-css { color: red } @media (max-width: 992px) { .cmp-css{ color: darkred; } } `, }, }, });

提示(组件优先的样式策略):默认情况下,在画布中选中组件并应用样式时,修改会作用到它现有的 class 上,这意味着所有使用这些 class 的组件都会一起变化。如果你希望样式只作用于当前选中的组件,可以启用componentFirst策略:

grapesjs.init({ ... selectorManager: { componentFirst: true, }, })

外部 CSS

如果需要加载组件专属的外部 CSS,需要借助script属性。更多细节请参考 组件与 JS(Components & JS)。

组件与 JS

如果想了解如何创建携带 JavaScript 逻辑的组件(例如计数器、画廊、轮播等),请查阅专页 组件与 JS(Components & JS)。

实战技巧

JSX 语法

如果向编辑器导入大段 HTML 字符串(例如通过 Blocks 定义的),JSX 是性能与代码可读性之间的绝佳平衡点:它保留了 HTML 的书写语法,同时跳过解析与组件识别两个重步骤。

默认情况下,GrapesJS 能直接理解 React JSX preset 生成的对象。如果你在 React 应用中开发,很可能已经在用 JSX 且无需任何额外配置——你的环境已经配置好了在 JavaScript 文件中解析 JSX。

所以,与其写这种(字符串形式,会执行解析与组件识别):

// 传入字符串,解析与组件识别步骤会被执行 editor.addComponents(`<div> <span>// 传入 Component Definition,跳过重步骤但代码可读性较差 editor.addComponents({ tagName: 'div', components: [ {...} ], });

不如使用这种格式:

editor.addComponents( <div> <custom-component>grapesjs.init({ // ... domComponents: { processor: (obj) => { if (obj.$$typeof) { // 例如这是 React Element const compDef = { type: obj.type, components: obj.props.children, ... }; ... return compDef; } } } })

源码佐证:processor正是 packages/core/src/dom_components/config/config.ts 中定义的DomComponentsConfig.processor——它会在任何新组件被加入编辑器前执行,用于把框架特定对象(如 JSX 元素)转换为 GrapesJS 组件定义,官方注释同时提醒"尽量做聪明的检查以避免无谓的执行"。同文件中还提供了draggableComponents(组件本身可拖拽,默认true)、disableTextInnerChilds(编辑文本时禁用内部子组件)、voidElements(HTML 空元素列表)、useFrameDoc(使用 frame 文档创建 DOM 元素,对 Web Components 等场景有用)、keepAttributeIdsCrossPages(跨页面保留相同属性 ID)等配置项,可按需查阅。

  • 如果需要从零支持 JSX(不使用任何支持 JSX 的框架),首先需要实现一个把 JSX 转换为 JS 可读代码的解析器。对于 Babel 用户,只需添加两个插件:@babel/plugin-syntax-jsx和@babel/plugin-transform-react-jsx,然后更新.babelrc文件:
{ "plugins": [ "@babel/plugin-syntax-jsx", "@babel/plugin-transform-react-jsx" ] }

还可以自定义执行转换的 pragma 函数:["@babel/plugin-transform-react-jsx", { "pragma": "customCreateEl" }]。默认使用React.createElement(要让其工作,文件中需要有一个可用的 React 实例)。

小结

回顾整条链路:一段 HTML 字符串进入编辑器后,先被解析为Component Definition,再经Component Type Stack从栈顶到栈底依次用isComponent/isParsedNode完成类型识别,随后实例化为Component(Model)——它是模板最终 HTML/JSON 的source of truth;而View负责画布内的 WYSIWYG 呈现,可与最终导出代码完全不同。基于这套架构,你可以通过DomComponents.addType注册自定义组件类型(配合isComponent/isParsedNode、model、view、extend/extendView/extendFn/extendFnView),借助生命周期钩子在init/onRender/updated/removed各阶段注入逻辑,通过styles属性实现组件级 CSS,并利用 JSX 语法优化大段模板的导入性能。无论是要构建一个可复用的业务组件库,还是为编辑器深度定制交互行为,本文所覆盖的 API 与源码路径都将是你的可靠起点。

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

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

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

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

立即咨询