原作者:Michael Burridge。原文 Block deprecation – a tutorial,发表于 2023 年 3 月 10 日,来源 WordPress Developer Blog。本文于 2026 年 10 月 5 日依据完整原文翻译整理,并对照当前区块编辑器手册补充技术边界。原页没有单独列出文章开放许可,原作者和社区贡献者署名保留,不把 WordPress 软件的 GPL 许可自动解释为文章许可。
假设你已经发布了一个静态区块插件,几百个网站正在使用。现在你想改善区块输出,或者客户要求修改已经散布在几十篇文章里的自定义区块。真正的问题不是改不了代码,而是:旧文章里保存的 HTML,仍然是旧版本生成的。
一旦新的 save() 无法重现旧标记,编辑器再次打开文章时就会提示“此区块包含意外或无效的内容”(This block contains unexpected or invalid content)。开发时你可能习惯点恢复按钮,但不能要求每个编辑者在全站逐个修复。这时应给区块保留旧版解析和保存逻辑,再把数据迁到当前版本。

弃用版本解决的是哪一部分问题
区块的 deprecated 数组保存历史版本定义。当现版无法验证旧标记时,编辑器可以用某个旧版定义识别已有内容,随后在文章重新保存时写出当前版本的标记。新插入的区块始终采用当前实现。这样不必更改区块名称,也不必为了这一个问题把静态区块全部改成动态区块。
原文强调,这个问题主要发生在静态区块的编辑器校验阶段。普通静态区块把 save() 的结果写进文章;打开前台时,服务器仍使用 wp_posts 中已经保存的内容,因此仅修改 save 并不会立即重写全站旧文章。
编者校订:“动态区块没有 save、永远不会出现校验错误”是原文的教学简化。典型动态区块可以让 save 返回 null,把输出交给服务端;但动态区块也可能保存回退 HTML 或 InnerBlocks,并非所有含服务端渲染的区块都完全没有可校验的保存内容。同样,前台已保存标记不自动改变,不代表新增 CSS、JavaScript 或服务端逻辑绝不会改变前台表现。背景说明可参阅原文引用的 Joni Halabi 的静态与动态区块比较。
第一次变化:把固定的 hello 改成 hi
原教程使用 @wordpress/create-block 生成一个新的静态区块插件:
npx @wordpress/create-block deprecation-example
在你的本地 WordPress 开发站点启用插件,创建一篇文章并插入新区块,保存好这一版内容。进入 wp-content/plugins/deprecation-example,启动开发构建:
npm start
版本与执行边界:教程写于 2023 年,如今脚手架的模板、默认渲染方式、文件行号和依赖版本都可能不同。应确认生成的是本教程需要的静态区块,并记录实际 WordPress、Node.js、create-block 与 wp-scripts 版本。无版本号的 npx 会获取并执行可变的包,实际项目宜固定经过审查的版本与锁文件。本文没有运行 npx、npm start,没有安装插件,也没有修改任何 WordPress 实例。
所有修改都发生在插件的 src 目录。找到 save.js 中返回的固定字符串,不要只照原文的“第 21 行”定位。先前保存的是:
{ 'Deprecation Example – hello from the saved content!' }
将它改为:
{ 'Deprecation Example – hi from the saved content!' }
保存文件后,在编辑器和前台分别重新载入那篇旧文章。预期前台仍显示数据库里的旧文本;编辑器则会发现新 save 想写出的 hi 与旧内容 hello 不同,显示校验错误。这里描述的是原教程预期结果,并非本文实测截图。
此时先不要点 Attempt Block Recovery,因为我们需要保留旧内容,用它检验兼容处理。创建 src/deprecated.js,把旧版 save 放到 v1 对象中:
const v1 = {
save() {
return (
<p { ...useBlockProps.save() }>
{ 'Deprecation Example – hello from the saved content!' }
</p>
);
}
}
旧 save 使用了 useBlockProps.save(),所以文件也要导入它:
import { useBlockProps } from '@wordpress/block-editor';
导出包含 v1 的数组:
export default [ v1 ];
最后在 index.js 导入数组,并把它作为 registerBlockType 的配置:
import deprecated from './deprecated';
registerBlockType( metadata.name, {
edit: Edit,
save,
deprecated
} );
刷新编辑器后,旧版 save 应能重现保存的 hello 标记,让编辑器正常载入它。更新文章时,当前 save 才把 hi 写入数据库;如果编辑器没有任何变化可保存,可以先作一个可撤销的文章编辑,再保存,以观察当前版本输出。
这里的关键是“验证旧内容,再用现版保存”。编辑器不是把 v1 的旧 save 永久留在前台运行,也不是在安装插件时立即批量转换数据库。
第二次变化:把固定文本改成可编辑属性
为 block.json 加入名为 text 的属性。它是字符串,从段落的 HTML 读取,默认值为 Deprecation Test:
"attributes": {
"text": {
"type": "string",
"source": "html",
"selector": "p",
"default": "Deprecation Test"
}
}
在 edit.js 中导入 RichText:
import { useBlockProps, RichText } from '@wordpress/block-editor';
然后把 Edit 改成下面的形式。函数参数需要解构 attributes 和 setAttributes,onChange 中用后者更新 text。
export default function Edit( { attributes, setAttributes } ) {
const onChangeContent = ( val ) => {
setAttributes( { text: val } )
}
return (
<RichText { ...useBlockProps() }
tagName="p"
onChange={ onChangeContent }
value={ attributes.text }
placeholder="Enter text here..."
/>
);
}
现在编辑者可以输入文本。更新当前保存实现前,先把刚才固定输出 hi 的版本保存为 v2:
const v2 = {
save() {
return (
<p { ...useBlockProps.save() }>
{ 'Deprecation Example – hi from the saved content!' }
</p>
);
}
}
在 save.js 导入 RichText:
import { useBlockProps, RichText } from '@wordpress/block-editor';
当前 save 则使用 RichText.Content 输出 text,而不再输出固定字符串:
export default function save( { attributes } ) {
return (
<RichText.Content { ...useBlockProps.save() }
tagName="p"
value={ attributes.text }
/>
);
}
把 v2 放到弃用数组最前面:
export default [ v2, v1 ];
历史定义按从新到旧排列,编辑器优先尝试最可能出现的最近版本,减少不必要的解析。不过,是否真的进入弃用流程仍取决于当前版本能否直接验证已有标记;不能仅凭数组中出现 v2 就断言每个旧区块都会执行它。
第三次变化:把 text 属性迁为 content
假设想把 text 留给别的含义,并用更贴切的 content 表示内容。先在 deprecated.js 中为即将淘汰的 RichText/text 版本建立 v3 快照:
const v3 = {
save( { attributes } ) {
return (
<RichText.Content { ...useBlockProps.save() }
tagName="p"
value={ attributes.text }
/>
);
}
}
因为这次的旧实现也用到 RichText,deprecated.js 的导入要改成:
import { useBlockProps, RichText } from '@wordpress/block-editor';
接着把当前 block.json 的属性名字改成 content:
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": "p"
}
}
这里是 JSON 配置的一个属性片段,必须合并到实际 block.json 的对象里,而不是单独当作完整 JSON 文件。原文新片段也不再列出旧默认值,实际项目应明确这是否是有意行为变化。
当前 Edit 中所有 text 都要改成 content,尤其不要漏掉 onChange 里的键:
export default function Edit( { attributes, setAttributes } ) {
const onChangeContent = ( val ) => {
setAttributes( { content: val } )
}
return (
<RichText { ...useBlockProps() }
tagName="p"
onChange={ onChangeContent }
value={ attributes.content }
placeholder="Enter text here..."
/>
);
}
当前 save 同样改为读取 content:
export default function save( { attributes } ) {
return (
<RichText.Content { ...useBlockProps.save() }
tagName="p"
value={ attributes.content }
/>
);
}
要让 v3 正确解释旧内容,在该旧版对象里加入自己的属性定义。历史属性不会自动继承当前 block.json:
attributes: {
text: {
type: 'string',
source: 'html',
selector: 'p',
},
},
再加入 migrate。它接收旧版本解析出的属性,把 text 的值放进当前 content:
migrate( { text } ) {
return {
content: text,
};
},
本例只迁移属性。迁移函数也可以处理内部区块,并按 API 约定返回 [attributes, innerBlocks]。
源文的完整 v3 如下:
const v3 = {
attributes: {
text: {
type: 'string',
source: 'html',
selector: 'p',
},
},
migrate( { text } ) {
return {
content: text,
};
},
save( { attributes } ) {
return (
<RichText.Content { ...useBlockProps.save() }
tagName="p"
value={ attributes.text }
/>
);
}
}
最后更新弃用数组:
export default [ v3, v2, v1 ];
可以在自己的开发站点选中区块,用下面的只读选择器检查当前属性:
wp.data.select( 'core/block-editor' ).getSelectedBlock().attributes
检查限制:未选中区块时 getSelectedBlock() 可能返回 null,因此这段控制台表达式不是通用无条件执行的检查程序。更稳妥的检查先保存返回值并判断非空。本文只静态查看了表达式,没有连接编辑器执行。
编者校订:本教程旧 text 和新 content 都从同一个 <p> 的 HTML 提取,当前版本有可能直接从现存 HTML 得到 content 并验证成功,此时无需进入 v3.migrate。看到 content 已有值,并不能证明迁移函数确实运行。如果目标是迁移保存在区块注释中的属性、调整结构或迁移 InnerBlocks,应以真实旧 fixture 验证触发路径;对于仍有效但必须迁移的情况,可研究 isEligible。
使用 Deprecation API 时,哪些定义必须留下
registerBlockType 的 deprecated 属性是一个数组,每个历史对象可包含 attributes、supports、save、migrate 和 isEligible。原教程重点讲前三代 save 和属性迁移;完整 API 含义见 区块编辑器手册:Deprecation。
迁移不是版本链。编辑器会拿原来的保存内容寻找一个可验证的旧定义;匹配后,把该定义的解析与迁移结果直接交给当前版本。它不会按 v1 → v2 → v3 顺序层层执行所有 migrate。某个旧 save 验证失败时,该对象的 migrate 也会被跳过。以后改变当前结构时,可能需要同时更新多个历史版本的迁移函数。
历史快照要稳定。attributes、supports、save 不自动从当前版本继承;不要为了少写几行而让历史 save 引用一个会随着新版一起改变的辅助函数,否则“旧实现”也会悄悄改变。原文的 v3 属性片段省略了先前 text 的 default,真实项目应从已经发布版本复制完整定义,连同影响序列化的 supports 一起核对。不能凭当前代码猜测历史 HTML。
保留回归样本。为每一代存一份原始区块序列化内容,包括区块注释和内部标记,再检查旧版识别、迁移后的属性、当前 save 输出以及重存后再次打开的结果。新建区块验证通过不能替代历史样本验证。富文本中可能有链接、格式及空值,迁移时也不能无意把这些内容丢失。
可以参考 Gutenberg 仓库中 Cover 区块的 deprecated.js 与 Button 区块的 deprecated.js。原作者建议将每个版本单独命名为 const,再放入数组,这样通常比在数组里直接堆很多匿名对象更易维护。这些 trunk 链接会变化,真实发布项目应保留自己的固定版本证据。
原文练习:把段落改成 div
当前 RichText 输出 <p>。练习是把它改为 <div>,并保留段落版的弃用定义。不要只修改当前 save 的 tagName:当前编辑组件、属性 selector 与旧快照都需要保持各自正确含义。旧历史仍解析 p,现版解析 div;迁移输出则必须符合当前属性结构。
此外,要考虑从每一个历史版本直接升级到最终 div 版本,而不只是从最新 p 版本升级。需要提示时,可看原作者提供的 练习参考答案 gist;这是原文保留的延伸链接,本稿未把该链接的代码当作已执行或已验证的实现。
发布前的技术边界
这套机制在编辑器打开并重新保存内容时工作,不会自动扫描和重写所有旧文章。若确实需要全站批量更新,应另行设计有备份、可回滚、尊重区块解析语义的迁移流程;直接在生产数据库做字符串替换容易破坏序列化内容,不属于本教程已经完成的工作。
代码没有发现硬编码的真实秘密,也没有直接发起网络请求或执行 shell 字符串的迁移逻辑。但 RichText 内容属于富文本,不能把客户端迁移当作服务端的权限或安全清理机制,更不应改用任意 HTML 拼接来“绕过校验”。静态审阅没有发现某类问题,不等于证明没有漏洞。
本文保留原教程全部 24 个代码片段,未运行脚手架、构建、插件激活、浏览器控制台命令或数据迁移。旧版内容能否无损升级,仍需读者在相应 WordPress/Gutenberg 版本的隔离环境中回归验证。
原作者感谢 @bph、@fabiankaegy、@thatdevgirl 与 @greenshady 对文章的审阅与改进。












暂无评论内容