news 2026/9/17 1:18:05

Gutenberg 的脚注(Footnotes)块:从格式标记到服务端渲染的完整实现解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Gutenberg 的脚注(Footnotes)块:从格式标记到服务端渲染的完整实现解析

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.jsonindex.phpedit.jsxformat.jsxstyle.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*实验性项):

能力项说明
anchortrue允许为块设置 HTML 锚点
color.backgroundtrue支持背景色
color.linktrue支持链接颜色(默认开启)
color.texttrue支持文字颜色(默认开启)
htmlfalse禁止以 HTML 方式编辑
multiplefalse每篇文章只允许出现一次
reusablefalse不允许被保存为复用块
inserterfalse不出现在块插入器列表中
spacing.margin/spacing.paddingtrue支持外边距与内边距
typography.fontSize/typography.lineHeighttrue支持字号与行高(字号默认开启)
interactivity.clientNavigationtrue支持客户端导航(视图交互)

此外,block.json中还额外声明了__experimentalBorder(radius/color/width/style,默认不开启)、__experimentalFontFamily__experimentalTextDecoration__experimentalFontStyle__experimentalFontWeight__experimentalLetterSpacing__experimentalTextTransform__experimentalWritingMode等实验性排版与边框支持,而自动生成的 README 只收录了稳定项。

值得特别注意的是multiple: falseinserter: false的组合:

  • inserter: false意味着用户不能手动从块插入器添加它,它只会在用户为某段文字添加脚注时由代码自动插入到文章底部;
  • multiple: false保证整篇文章只存在一个脚注容器,避免出现重复列表。

3. Context:跨块上下文传递

该块声明了usesContext: ["postId", "postType"],即它需要读取当前文章的 ID 与类型。这依赖块上下文(block context)机制,由编辑器注入。只有postIdpostType都存在时,服务端渲染才能定位到正确的 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: truecontentEditable: false——引用对象不参与内容编辑,避免用户误改;
  • 通过usesContextKey声明它同样需要postId/postType,用于判断当前环境是否支持脚注。

format.jsxonClick逻辑中,插入新脚注时 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 选择器#idquerySelector在前台始终可用。随后通过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.jsonusesContext的服务端对应物;
  • 安全转义idesc_attr()、内容用wp_kses_post()aria-labelesc_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 覆盖了完整的“插入脚注”用户路径:

  1. 新建文章,输入两段文字;
  2. 选中文本后通过工具栏 “More → Footnote” 插入脚注并输入内容;
  3. 断言当前激活元素(脚注编辑区)的 ID 匹配/^fn-[0-9a-f-]{36}$/,即fn-前缀 + UUID
  4. 断言生成的块序列为:段落 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的实现中可以提炼出一套通用模式,适合任何“正文内标注 + 文末聚合”的功能需求:

    1. 富文本格式收集引用:用registerFormatType注册一个tagNamesup/mark等的格式,用属性承载唯一 ID,interactive: true防止内容被误编辑;
    2. 动态块渲染聚合:声明apiVersion: 3multiple: falseinserter: false的块,配合render_callback在服务端输出聚合列表;
    3. post meta 承载数据:用register_post_meta注册type: 'string'show_in_rest: truerevisions_enabled: true的 JSON 字符串字段,即可同时获得 REST 可读写与修订追踪能力;
    4. 上下文传递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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 1:13:46

Hyper-V内部网络外网连通:路由模式替代NAT的实战方案

1. 这不是“配个IP”那么简单&#xff1a;Hyper-V内部网络固定IP外网连通的真实场景与核心矛盾你搜到这个标题&#xff0c;大概率正卡在某个具体环节&#xff1a;虚拟机里装好了CentOS Stream 10&#xff0c;nmcli配了静态IP&#xff0c;ping 192.168.137.1通了&#xff0c;但p…

作者头像 李华
网站建设 2026/9/17 1:12:06

2026 国内 AI 科研平台选型指南:沁言学术功能与适用性解析

引言&#xff1a;随着人工智能技术与科研工作的深度融合&#xff0c;AI 工具已成为众多高校师生及科研人员提升效率的重要辅助手段。然而&#xff0c;面对市场上琳琅满目的产品&#xff0c;如何甄别其实际能力、选择契合自身研究需求的平台&#xff0c;成为许多研究者面临的难题…

作者头像 李华
网站建设 2026/9/17 1:08:17

VineCopulaCPP实战:Matlab中藤Copula建模与尾部依赖分析

简介&#xff1a;这是一份藤Copula建模工具&#xff0c;底层基于C实现&#xff0c;并通过Matlab接口封装&#xff0c;面向需要量化多元随机变量依赖关系的研究者与从业者&#xff0c;适用于金融工程、风险管理与统计建模等场景。压缩包共23个文件&#xff0c;以17个cpp源码与3个…

作者头像 李华
网站建设 2026/9/17 1:07:41

SpringBoot+Vue+MySQL牙科诊所管理系统开发实战

1. 项目概述&#xff1a;牙科诊所管理系统的全栈实现作为一名经历过三次医疗信息化项目重构的老码农&#xff0c;看到这个毕业设计选题不禁会心一笑。这个SpringBootVueMySQL的技术栈组合&#xff0c;正是当前医疗行业中小型诊所管理系统的黄金配置方案。去年我帮本地一家连锁牙…

作者头像 李华