WordPress 区块弃用教程:让旧内容平稳迁移到新版本

原作者: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)。开发时你可能习惯点恢复按钮,但不能要求每个编辑者在全站逐个修复。这时应给区块保留旧版解析和保存逻辑,再把数据迁到当前版本。

编辑器先校验当前 save;若不匹配则查找能验证旧标记的 deprecated 版本,再将其迁移结果交给当前版本保存。
未完纪原创技术示意图;用于说明流程,不是运行截图。

弃用版本解决的是哪一部分问题

区块的 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 对文章的审阅与改进。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容