Gutenberg 的脚注(Footnotes)块:从格式标记到服务端渲染的完整实现解析
【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg
导读
本文基于 Gutenberg 仓库中 packages/block-library/src/footnotes/README.md 的官方块 API 文档,结合该目录下的block.json、index.php、edit.jsx、format.jsx、style.scss及端到端测试用例,深入解析core/footnotes动态块的设计与实现。读完本文,你将掌握该块的元数据声明(attributes/supports/context)、正文内脚注引用的插入机制(core/footnote富文本格式)、脚注内容的服务端渲染链路,以及它与文章修订(revisions)系统的集成方式,并能在自己的主题或插件中复用这套模式。
一、块概览:一个自动化的“参考文献”动态块
core/footnotes是 Gutenberg 内置核心块之一,官方文档的描述是:
Display footnotes added to the page.
即“展示页面中添加的脚注”。它被归入 text 分类(category: "text"),keyword 为references(参考文献),并使用了 API 版本 3(apiVersion: 3)。
该块最显著的特点是Dynamic(动态块):它不在文章内容里保存任何 HTML 标记,而是在服务端渲染时由 PHP 回调实时生成最终的<ol>脚注列表。其块名称为core/footnotes。
在文章内容中,该块仅以一个块注释(block comment)的形式存储:
<!-- wp:footnotes /-->正因如此,它是整个脚注体系中的“展示层”,真正负责“采集脚注内容”的是正文内的富文本格式(rich text format),两者配合构成一套完整的“引用-收集-渲染”流程。
二、块元数据(block.json)深度解读
块的声明全部位于 packages/block-library/src/footnotes/block.json,下面逐一拆解其中的关键字段。
1. Attributes:零自定义属性
文档明确说明:
This block has no custom attributes.
该块没有自定义属性(attributes属性未声明)。因为脚注的原始内容并不存放在块属性里,而是存放在文章的 post meta(meta 键footnotes)中,服务端渲染时从 meta 读取。这与动态块“内容不入库”的设计相辅相成。
2. Supports:受支持的样式与行为
supports字段决定了编辑器与前台对块的支持程度,该块声明了以下能力(含__experimental*实验性项):
| 能力项 | 值 | 说明 |
|---|---|---|
anchor | true | 允许为块设置 HTML 锚点 |
color.background | true | 支持背景色 |
color.link | true | 支持链接颜色(默认开启) |
color.text | true | 支持文字颜色(默认开启) |
html | false | 禁止以 HTML 方式编辑 |
multiple | false | 每篇文章只允许出现一次 |
reusable | false | 不允许被保存为复用块 |
inserter | false | 不出现在块插入器列表中 |
spacing.margin/spacing.padding | true | 支持外边距与内边距 |
typography.fontSize/typography.lineHeight | true | 支持字号与行高(字号默认开启) |
interactivity.clientNavigation | true | 支持客户端导航(视图交互) |
此外,block.json中还额外声明了__experimentalBorder(radius/color/width/style,默认不开启)、__experimentalFontFamily、__experimentalTextDecoration、__experimentalFontStyle、__experimentalFontWeight、__experimentalLetterSpacing、__experimentalTextTransform、__experimentalWritingMode等实验性排版与边框支持,而自动生成的 README 只收录了稳定项。
值得特别注意的是multiple: false与inserter: false的组合:
inserter: false意味着用户不能手动从块插入器添加它,它只会在用户为某段文字添加脚注时由代码自动插入到文章底部;multiple: false保证整篇文章只存在一个脚注容器,避免出现重复列表。
3. Context:跨块上下文传递
该块声明了usesContext: ["postId", "postType"],即它需要读取当前文章的 ID 与类型。这依赖块上下文(block context)机制,由编辑器注入。只有postId与postType都存在时,服务端渲染才能定位到正确的 meta 数据——这一点直接决定了index.php中的渲染逻辑。
三、前端实现:脚注格式与编辑体验
1. 插入脚注引用:core/footnote富文本格式
用户之所以能在段落内插入脚注,靠的并不是一个普通块,而是一个名为core/footnote的富文本格式(format)。它定义在 packages/block-library/src/footnotes/format.jsx:
export const formatName = 'core/footnote'; export const format = { title: __( 'Footnote' ), tagName: 'sup', className: 'fn', attributes: { 'data-fn': 'data-fn', }, interactive: true, contentEditable: false, [ usesContextKey ]: [ 'postType', 'postId' ], // edit: ... };这段声明非常关键:
tagName: 'sup'——脚注引用在正文中渲染为上标;className: 'fn'——引用元素携带fn类名;attributes: { 'data-fn': 'data-fn' }——用data-fn属性承载脚注的唯一 ID,该 ID 同时是脚注内容的锚点目标;interactive: true与contentEditable: false——引用对象不参与内容编辑,避免用户误改;- 通过
usesContextKey声明它同样需要postId/postType,用于判断当前环境是否支持脚注。
在format.jsx的onClick逻辑中,插入新脚注时 ID 的生成方式值得借鉴:
// The ID doubles as the anchor target of the footnote link. // A CSS identifier cannot start with a digit, so a bare UUID // (which often does) breaks `querySelector( '#' + id )` and // `#id` style rules on the front end. Prefix it with a letter. id = `fn-${ createId() }`;即用 UUID 生成唯一 ID,并强制加上fn-前缀,保证 ID 以字母开头,从而保证 CSS 选择器#id与querySelector在前台始终可用。随后通过insertObject把如下 HTML 对象插入到光标位置:
<a href="#fn-xxx" id="fn-xxx-link">*</a>其中:
href="#fn-xxx"指向脚注列表项(列表项id就是fn-xxx);- 引用自身的
id="fn-xxx-link"是前台“跳回正文”返回链接的目标锚点。
2. 自动插入脚注容器块
format.jsx中还实现了一个非常实用的行为:当正文中已有脚注引用、但文章里还没有core/footnotes块时,点击“Footnote”工具栏按钮会自动在文章底部创建并插入一个脚注块:
// When there is no footnotes block in the post, create one and // insert it at the bottom. if ( ! fnBlock ) { // ...向上找到 post-content 根块 fnBlock = createBlock( 'core/footnotes' ); insertBlock( fnBlock, undefined, rootClientId ); } selectionChange( fnBlock.clientId, id, 0, 0 );从实现看,插入后还会通过selectionChange把光标直接定位到新脚注的编辑区,让用户可以立即输入脚注内容。这在页面编辑器(site editor)中同样生效——代码会先沿块树向上寻找core/post-content祖先块,再在正确的层级插入。
3. 编辑视图:直接编辑脚注内容
脚注块在编辑器中的渲染由 packages/block-library/src/footnotes/edit.jsx 负责:
- 通过
useEntityProp('postType', postType, 'meta', postId)读写当前文章的footnotesmeta; - 读取 meta 时对数据做了健壮性处理:meta 本质是字符串,可能被其他代码写坏,因此用
JSON.parse包裹try/catch,解析失败或形状不对一律按“无脚注”处理:let parsed; try { parsed = JSON.parse( meta?.footnotes || '[]' ); } catch { // Left undefined, which the check below treats as no footnotes... } const footnotes = Array.isArray( parsed ) ? parsed : []; - 当 meta 不可用(例如当前文章类型不支持 meta)时显示占位提示 “Footnotes are not supported here. Add this block to post or page content.”;
- 当脚注列表为空时显示提示 “Footnotes found in blocks within this document will be displayed here.”;
- 有脚注时渲染为
<ol>列表,每项用RichText可编辑,并通过updateMeta把最新的footnotesJSON 写回 meta:updateMeta( { ...meta, footnotes: JSON.stringify( footnotes.map( ( footnote ) => { return footnote.id === id ? { content: nextFootnote, id } : footnote; } ) ), } );
由此可以总结出编辑器内的数据流:
正文输入脚注 →core/footnote格式在文本中留下data-fn引用 →edit.jsx把脚注内容写入footnotespost meta(JSON 数组)→ 前台由服务端渲染读出。
块注册入口在 packages/block-library/src/footnotes/index.js:init()中同时执行registerFormatType( formatName, format )与initBlock(...),即富文本格式与块是成对注册的,二者缺一不可。
四、服务端渲染:index.php 全链路解析
动态块的渲染回调定义在 packages/block-library/src/footnotes/index.php 的render_block_core_footnotes()中,注册方式为register_block_type_from_metadata( __DIR__ . '/footnotes', array( 'render_callback' => 'render_block_core_footnotes' ) ),即直接读取同目录footnotes(block.json)注册。
渲染回调的执行流程可以概括为四步守卫 + 一次循环输出:
function render_block_core_footnotes( $attributes, $content, $block ) { // 1. postId 为空 → 不渲染 if ( empty( $block->context['postId'] ) ) { return ''; } // 2. 文章受密码保护 → 不渲染 if ( post_password_required( $block->context['postId'] ) ) { return ''; } // 3. 读取 post meta "footnotes",为空 → 不渲染 $footnotes = get_post_meta( $block->context['postId'], 'footnotes', true ); if ( ! $footnotes ) { return ''; } // 4. JSON 解码失败或不是数组/空数组 → 不渲染 $footnotes = json_decode( $footnotes, true ); if ( ! is_array( $footnotes ) || count( $footnotes ) === 0 ) { return ''; } // 循环输出 <li> 列表项,附返回链接 $wrapper_attributes = get_block_wrapper_attributes(); $footnote_index = 1; $block_content = ''; foreach ( $footnotes as $footnote ) { $aria_label = sprintf( __( 'Jump to footnote reference %1$d' ), $footnote_index ); $block_content .= sprintf( '<li id="%1$s">%2$s <a href="#%1$s-link" aria-label="%3$s">↩︎</a></li>', esc_attr( $footnote['id'] ), wp_kses_post( $footnote['content'] ), esc_attr( $aria_label ) ); ++$footnote_index; } return sprintf( '<ol %1$s>%2$s</ol>', $wrapper_attributes, $block_content ); }几个值得展开的细节:
- 上下文依赖:渲染依赖
$block->context['postId'],这正是block.json中usesContext的服务端对应物; - 安全转义:
id用esc_attr()、内容用wp_kses_post()、aria-label用esc_attr(),避免注入风险; - 双向跳转:列表项
id与正文中href的锚点一致(fn-xxx),而返回链接href="#%1$s-link"恰好对应正文引用元素上的id="fn-xxx-link",形成“正文 ↔ 脚注”的往返导航; - 可访问性:每个返回链接带
aria-label="Jump to footnote reference N",N 为从 1 递增的序号,方便屏幕阅读器用户; - 包装属性:
get_block_wrapper_attributes()会把block.json支持的anchor、颜色、间距、排版等样式统一输出到<ol>上。
1. footnotes post meta 的注册
为了让脚注内容能够存取,index.php还以优先级 20 在init钩子上注册了footnotesmeta 字段(register_block_core_footnotes_post_meta()):
register_post_meta( $post_type, 'footnotes', array( 'show_in_rest' => true, // 暴露给 REST API(编辑器读取) 'single' => true, 'type' => 'string', 'revisions_enabled' => true, // 支持修订 ) );注册范围是“在 REST 中可见、且同时支持编辑器、自定义字段与修订”的所有文章类型:
if ( post_type_supports( $post_type, 'editor' ) && post_type_supports( $post_type, 'custom-fields' ) && post_type_supports( $post_type, 'revisions' ) ) {show_in_rest: true使前端useEntityProp能够读取/写入;single: true表示单值 meta;type: 'string'表示存放的是 JSON 字符串;revisions_enabled: true让脚注内容随修订一起被追踪。
2. 与修订(Revisions)系统的集成
index.php末尾还挂接了两个修订相关过滤器,把脚注纳入 WordPress 修订对比界面:
add_filter( '_wp_post_revision_fields', 'wp_add_footnotes_to_revision' ); add_filter( '_wp_post_revision_field_footnotes', 'wp_get_footnotes_from_revision', 10, 3 );wp_add_footnotes_to_revision()把footnotes字段加入修订字段列表(显示名 “Footnotes”);wp_get_footnotes_from_revision()从修订对象中读取该 meta:function wp_get_footnotes_from_revision( $revision_field, $field, $revision ) { return get_metadata( 'post', $revision->ID, $field, true ); }
这意味着每一次保存草稿,脚注内容都会作为一个可对比、可恢复的修订维度被记录下来。这一行为有端到端测试直接验证(见下文第五节)。
五、样式与兼容性:style.scss 的取舍
脚注块的样式位于 packages/block-library/src/footnotes/style.scss,文件头部注释点明其定位:
// These styles are for backwards compatibility with the old footnotes anchors. // Can be removed in the future.即这些样式主要用于向后兼容旧的脚注锚点。核心规则如下:
.editor-styles-wrapper, .entry-content { counter-reset: footnotes; } a[data-fn].fn { vertical-align: super; font-size: smaller; counter-increment: footnotes; display: inline-flex; text-decoration: none; text-indent: -9999999px; } a[data-fn].fn::after { content: "[" counter(footnotes) "]"; text-indent: 0; float: left; }可以看到:
- 通过 CSS 计数器
counter-reset/counter-increment自动为脚注引用编号,输出格式为[1]、[2]…; - 利用
text-indent: -9999999px将原始的“*”号移出可视区域,只显示计数结果; block.json中声明的"style": "wp-block-footnotes"使块在前台自动加载该样式表。
从测试快照 test/integration/fixtures/blocks/core__footnotes.html 也可以看到该块在内容中的标准序列化形式(仅块注释,无 HTML 内容),印证其动态块的存储方式。
六、测试验证:行为如何被锁定
仓库中针对脚注功能的端到端测试很好地充当了“行为规格说明书”。
1. 插入流程测试
test/e2e/specs/editor/various/footnotes.spec.js 覆盖了完整的“插入脚注”用户路径:
- 新建文章,输入两段文字;
- 选中文本后通过工具栏 “More → Footnote” 插入脚注并输入内容;
- 断言当前激活元素(脚注编辑区)的 ID 匹配
/^fn-[0-9a-f-]{36}$/,即fn-前缀 + UUID; - 断言生成的块序列为:段落 1 → 段落 2(含
<sup><ol class="wp-block-footnotes ..."> <li id="fn-xxx">脚注内容 <a href="#fn-xxx-link" aria-label="Jump to footnote reference 1">↩︎</a></li> ... </ol>主题开发者可以直接针对
.wp-block-footnotes编写自定义 CSS(例如改变列表缩进、返回链接样式、优化移动端排版),无需改动任何 PHP 代码。常见问题排查
现象 可能原因 前台不显示脚注列表 文章类型不支持 editor/custom-fields/revisions之一,导致footnotesmeta 未注册文章受密码保护时无脚注 post_password_required()守卫主动跳过渲染(设计如此)编辑器内脚注不可用 当前环境不是文章/页面内容(如某些模板部件),meta 不可用, edit.jsx会显示占位提示页面编辑器(site editor)中脚注位置异常 块插入逻辑依赖 core/post-content祖先块定位,需确认模板结构包含 post-content对开发者可复用的模式
从
core/footnotes的实现中可以提炼出一套通用模式,适合任何“正文内标注 + 文末聚合”的功能需求:- 富文本格式收集引用:用
registerFormatType注册一个tagName为sup/mark等的格式,用属性承载唯一 ID,interactive: true防止内容被误编辑; - 动态块渲染聚合:声明
apiVersion: 3、multiple: false、inserter: false的块,配合render_callback在服务端输出聚合列表; - post meta 承载数据:用
register_post_meta注册type: 'string'、show_in_rest: true、revisions_enabled: true的 JSON 字符串字段,即可同时获得 REST 可读写与修订追踪能力; - 上下文传递:
usesContext: ['postId', 'postType']让格式与块都拿到正确的数据归属。
结语
core/footnotes虽然只占据块目录中的一小块空间,却是 Gutenberg“动态块 + 富文本格式 + post meta + 修订系统”协同工作的经典范例:格式负责在正文中埋下data-fn引用,动态块负责在文末聚合渲染,meta 负责持久化,修订过滤器负责历史追踪。理解它的元数据声明、渲染守卫与数据流,不仅能帮助你在使用中排查问题,更能为设计类似的“引用-聚合”型功能提供一份高质量的实现蓝图。相关资源速查:
- 块元数据:packages/block-library/src/footnotes/block.json
- 服务端渲染与 meta/修订集成:packages/block-library/src/footnotes/index.php
- 编辑器编辑视图:packages/block-library/src/footnotes/edit.jsx
- 脚注富文本格式与自动建块:packages/block-library/src/footnotes/format.jsx
- 样式与兼容性:packages/block-library/src/footnotes/style.scss
- 端到端测试:test/e2e/specs/editor/various/footnotes.spec.js、test/e2e/specs/editor/various/footnotes-revisions.spec.js
【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.
项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg
- 富文本格式收集引用:用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考