Gutenberg Custom HTML(core/html)块深度解析:从 block.json 到编辑渲染的完整实现
【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg
导读
本文基于 Gutenberg 插件仓库中 Custom HTML 块的官方文档 及其完整源码实现,深入剖析core/html这一核心静态块的元数据定义、属性与支持能力、序列化标记结构,以及它的编辑体验、代码转换与权限控制机制。读完本文,你将掌握 Custom HTML 块的完整工作方式,理解其“静态块 + inner content”的存储模型,并能据此排查二次开发中遇到的序列化、权限与预览相关问题。
一、Custom HTML 块是什么
Custom HTML(core/html)是 Gutenberg 内置的“自定义 HTML”块,官方定位一句话即可概括:添加自定义 HTML 代码,并在编辑时实时预览效果(Add custom HTML code and preview it as you edit)。它属于widgets(小工具)分类,块类型为静态块(Static block)——这意味着它的标记会以原始 HTML 的形式直接保存在文章内容中,而不是像动态块那样由服务端渲染函数在输出时生成。
从 block.json 可以看到它的元数据全貌:
| 元数据字段 | 值 | 说明 |
|---|---|---|
name | core/html | 块的唯一注册名,出现在注释分隔符中 |
title | Custom HTML | 块在编辑器中的显示名称 |
category | widgets | 归入小工具分类 |
description | Add custom HTML code and preview it as you edit | 官方描述 |
apiVersion | 3 | 使用第 3 版块 API |
keywords | ["embed"] | 搜索关键词 |
editorStyle | wp-block-html-editor | 编辑器专用样式句柄 |
textdomain | default | 文本域 |
在 index.js 中,块通过initBlock完成注册,并配置了图标、示例内容与编辑/保存/转换实现。值得注意的是它的示例内容是一个<marquee>标签——这是块作者用来展示“可以放入任意 HTML”的经典演示片段。
二、属性(Attributes):唯一的content与role: local语义
按照块 API 规范,属性通过 block.json 中的attributes字段定义。Custom HTML 块只有一个属性:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
content | string | — | Role:local |
content的role: local非常关键:它表示该属性是编辑器本地状态,不会作为 JSON 序列化进块注释分隔符中。这一点在 test/index.jsdom.test.js 中有专门的测试断言:
it( 'keeps the content attribute out of the block delimiter', () => { const block = createBlock( 'core/html', { content: '<marquee>Hello</marquee>', } ); // `role: 'local'` prevents the attribute from being written into // the comment delimiter as JSON. expect( serialize( block ) ).not.toContain( '{"content"' ); } );历史遗留:content属性的迁移逻辑
从源码注释与 edit.jsx 可以看到一段重要的兼容性设计:早期版本的 Custom HTML 块把标记存放在content属性中,而现在的实现把标记迁移到了块的inner content(内部内容片段)中。为了兼容旧的程序化创建方式(例如createBlock( 'core/html', { content } )),块在加载时一旦发现attributes.content存在,就会:
- 通过
deprecated()输出废弃警告(自 7.1 版本起,建议改用 inner content); - 用
updateBlock把attributes.content中的标记写入innerContent,同时清除content属性。
这正是“不丢数据”的渐进式迁移策略:旧内容自动升级到新存储模型,而不会在版本升级时丢失。
三、Supports 能力矩阵:为何它如此“克制”
Supports 决定了编辑器为该块提供哪些通用能力(类名、锚点、可见性开关等)。Custom HTML 块的 supports 配置相当克制,block.json 中定义如下:
| Supports 项 | 值 | 影响 |
|---|---|---|
customClassName | false | 不允许用户在额外 CSS 类中自定义类名 |
className | false | 不自动注入.wp-block-html类 |
html | false | 不提供“以 HTML 方式编辑”的块级切换 |
interactivity.clientNavigation | true | 支持客户端导航(在交互式前端路由中保留) |
listView | true | 在列表视图中可见 |
customCSS | false | 不提供自定义 CSS 支持 |
visibility | false | 不提供块可见性控制 |
这些“关闭”项是有意为之:Custom HTML 块的定位就是把原始 HTML 原样交还给用户,如果套上自动类名或自定义 CSS 包装,反而会污染用户输入的标记。这种“少即是多”的设计,与core/paragraph等富文本块形成鲜明对比。
四、块标记(Block Markup):静态块的序列化模型
由于是静态块,Custom HTML 的标记会原样写入文章内容,以<!-- wp:html -->与<!-- /wp:html -->注释分隔符包裹。文档给出了典型示例:
<!-- wp:core/html --> <h1>Some HTML code</h1> <div>This is a div</div> <!-- /wp:core/html -->源码层面的真相:save()返回null
值得深入说明的是,save.js 的实现只有一行:
// The block's markup is serialized from its `innerContent` (static HTML // fragments interleaved with inner blocks), not from a save implementation. export default function save() { return null; }也就是说,Custom HTML 块的标记并非由save()函数生成,而是直接取自块的innerContent——即“静态 HTML 片段与内部块(inner blocks)交错排列”的数组。这正是 test/index.jsdom.test.js 中验证的行为:当 innerContent 为['<div>', null, '</div>']且内嵌一个段落块时,序列化结果是:
<!-- wp:html --> <div><!-- wp:paragraph --> Editable <!-- /wp:paragraph --></div> <!-- /wp:html -->这意味着 Custom HTML 块内部可以嵌套其他块——用户在 HTML 代码里书写的<!-- wp:* -->分隔符会被解析为真实的内部块,这些块仍然可编辑,而其余部分保持为静态 HTML 片段。
五、编辑体验:占位符、代码弹窗与实时预览
Custom HTML 的编辑界面由 edit.jsx 驱动,整体流程分三种状态:
- 空内容占位状态:当内容为空时,显示带“Custom HTML”标签和“Edit HTML”主按钮的
Placeholder占位符,点击后弹出编辑弹窗(HTMLEditModal)。 - 正常编辑状态:通过
InnerContent(来自 block-editor 私有 API)渲染已解析的标记;工具栏(BlockControls)提供“Edit code”按钮,右侧检查器(InspectorControls)也提供同名的“Edit code”次按钮。 - 弹窗编辑:点击任一“Edit code”都会打开 modal.jsx 中的
HTMLEditModal。
编辑弹窗的三栏式设计
编辑弹窗是本块最富特色的部分,它在一个大型Modal中提供:
- Tab 页签:HTML / CSS / JavaScript 三个页签(CSS 与 JavaScript 页签仅在用户拥有 unfiltered HTML 权限时显示);
- 分栏布局:左侧是带等宽字体、LTR 方向(无论语言设置如何,HTML 代码始终从左到右书写)的代码编辑区,右侧是实时预览区;窄屏(移动端视口)自动切换为上下布局;
- 全屏切换:桌面端提供全屏按钮(
fullscreen/square图标切换); - 原生撤销:通过
useNativeUndo保留浏览器自身的撤销/重做行为——因为弹窗内字段的本地状态只在点击“Update”时才提交给块,期间的撤销必须交给浏览器; - 底栏按钮:Cancel 与 Update(点击 Update 提交并关闭弹窗)。
预览是如何实现的
预览组件在 preview.jsx 中实现,核心是@wordpress/components的SandBox——一个沙箱化的 iframe。它做了两件事:
- 注入一组
DEFAULT_STYLES,清除编辑器可能继承给预览内容的边距/内边距等样式; - 通过
transformStyles把编辑器全局样式设置(getSettings().styles)转换后注入预览 iframe,使预览尽可能接近真实前台效果。
此外,当块未被选中时,会渲染一个.block-library-html__preview-overlay透明覆盖层。源码注释解释了原因:部分浏览器不会把沙箱 iframe 内的点击事件冒泡出来,这个覆盖层保证用户在预览区域点击时能重新选中该块。
权限与内容剥离:unfiltered HTML 检查
编辑弹窗通过settings.__experimentalCanUserUseUnfilteredHTML判断用户是否拥有unfiltered_html权限(通常仅管理员/编辑者具备)。这一判断直接影响两处行为:
- CSS 与 JavaScript 页签是否显示;
- 提交内容时是否保留 CSS/JS 段。
在 modal.jsx 的handleUpdate中可以看到:没有该权限的用户,其 CSS 和 JS 段会在保存时被剥离,只保留 HTML。同时,若块内原本就含有 CSS/JS 而用户无权限,弹窗顶部会显示一条不可关闭的警告 Notice:这些内容将在保存时被移除。这一设计是为了避免内容经过服务端 kses 过滤后留下残缺标记——与其让用户看到被截断的半成品,不如在编辑器内主动剥离并明示。
六、CSS / JS 的分段存取:parseContent与serializeContent
编辑弹窗把 HTML、CSS、JS 分成三个独立编辑区,这背后依赖 utils.js 中两个对称的工具函数:
parseContent( content ):解析合并内容字符串。它创建一个临时 HTML 文档(document.implementation.createHTMLDocument),从中提取带data-wp-block-html="css"标记的<style>标签和带data-wp-block-html="js"标记的<script>标签,其余内容归为 HTML。注意:未带标记的<style>/<script>标签不会被提取,它们按普通 HTML 处理;多个同类型标记标签只取第一个。serializeContent( { html, css, js } ):反向操作,按“CSS → JS → HTML”的顺序拼接,并为 CSS/JS 段重新加上data-wp-block-html标记;空白段会被忽略。
两者配合实现无损的往返(round-trip)序列化,这在 test/utils.jsdom.test.js 中有完整验证——包括空内容、纯 CSS、纯 JS、三段俱全、畸形 HTML 容错、多标记标签取首个、空白裁剪等十余个用例。这套分段机制的价值在于:编辑器把 CSS/JS 与 HTML 分开管理,权限检查与内容剥离只需针对标记段进行,不会误伤用户写在 HTML 里的<style>/<script>标签。
七、编辑时内容如何回流:onUpdate的再解析机制
当用户在弹窗点击“Update”,edit.jsx 的onUpdate会执行一次完整的“再解析”流程:
- 用
parse()把新内容包进<!-- wp:html -->分隔符后解析,得到parsedBlock; - 静态 HTML 部分成为块的
innerContent片段,<!-- wp:* -->分隔段则成为内部块; - 通过
serialize()逐块比较新旧内部块:若内部块标记未变化(例如只改了周边的静态 HTML),则保留原有内部块及其 clientId 与选中状态,避免编辑静态代码时无辜重置内部块; - 所有更新放在
registry.batch()中批量执行,保证updateBlock与replaceInnerBlocks原子性生效。
这套机制保证了“静态 HTML 与可编辑内部块共存”模型的健壮性:用户在弹窗里编辑的是整体源码,但提交后,其中被识别为块的部分依然保持为真正可交互的嵌套块。
八、块转换:从core/code一键转为 Custom HTML
transforms.js 定义了块的转换规则:支持从core/code(代码块)转换为 Custom HTML。转换逻辑值得细读:
const text = create( { html } ).text; const [ block ] = parse( `<!-- wp:html -->\n${ text }\n<!-- /wp:html -->` ); return block ?? createBlock( 'core/html', {}, [], [ text ] );流程是:先把代码块的 HTML 内容转成纯文本(@wordpress/rich-text的create().text),再包上wp:html分隔符重新解析。这样做的目的是:如果代码块里恰好含有<!-- wp:paragraph -->之类的块分隔符文本,它们会被还原为可编辑的真实内部块,而不是变成惰性注释文本。若解析失败则退化为仅含单个静态文本片段的core/html块。
test/index.jsdom.test.js 用两个用例验证了这一行为:包含分隔符的代码块转换后内部多出一个段落块(innerContent含null占位),而纯静态 HTML 的代码块转换后内部块数量为 0。
九、注册与初始化:init.js到initBlock
块的初始化入口在 init.js:
import { init } from './'; export default init();index.js中的init调用initBlock({ name, metadata, settings })(来自 ../utils/init-block),完成registerBlockType注册。Custom HTML 块的完整源码目录结构如下,方便读者继续深入:
- block.json:元数据定义
- edit.jsx:编辑组件
- modal.jsx:编辑弹窗
- preview.jsx:沙箱预览
- save.js:序列化逻辑(返回 null,标记来自 innerContent)
- transforms.js:块转换
- utils.js:HTML/CSS/JS 分段工具
- editor.scss:编辑器样式
- test/index.jsdom.test.js 与 test/utils.jsdom.test.js:行为与工具函数测试
十、前端渲染与安全边界:理解“原样输出”的双刃剑
最后需要明确 Custom HTML 块的运行边界:
- 静态存储:块标记保存在文章内容中,由 WordPress 前端原样输出,不存在服务端渲染回调;
- 权限防线:是否允许在块内保存
<script>/<style>(以及 CSS/JS 页签是否可用)完全取决于用户的unfiltered_html能力,无权限用户保存时 CSS/JS 段会被主动剥离,以避免 kses 过滤产生残缺标记; - 编辑期沙箱:预览通过
SandBoxiframe 隔离运行,避免编辑器环境与预览内容互相干扰; - 嵌套块能力:静态 HTML 中嵌入的
<!-- wp:* -->分隔符会被解析为内部块,实现“整体不可分割、局部仍可编辑”的混合模型。
因此在实际使用中,Custom HTML 块适合有经验的开发者放置第三方嵌入代码、统计脚本或定制标记;对于普通内容作者,应优先使用官方块并借助权限体系控制谁能写入脚本类内容。
总结
本文从 官方块文档 出发,结合 block.json 与 edit.jsx、modal.jsx、save.js、transforms.js、utils.js 等源码文件,完整还原了 Custom HTML 块的设计与实现:content属性的role: local语义与迁移逻辑、克制的 supports 配置、基于 innerContent 的静态序列化模型、三栏式编辑弹窗与沙箱预览、unfiltered HTML 权限下的 CSS/JS 剥离机制,以及从core/code的智能转换。理解这些内部机制,无论对插件开发、块扩展还是内容架构设计,都能提供扎实的底层依据。
【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考