Gutenberg 内部组件 BlockActions 全解析:基于 render prop 的块操作逻辑层
2026/9/16 13:01:59 网站建设 项目流程

Gutenberg 内部组件 BlockActions 全解析:基于 render prop 的块操作逻辑层

【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg

BlockActions 是 WordPress 块编辑器(Gutenberg)中负责"对一组块执行操作"的核心逻辑组件——它封装了复制、删除、插入、成组/解组、粘贴样式等动作及其可用性判断,但不渲染任何 UI,而是通过 render prop 将能力交给消费方。本指南以该组件的 README 为骨架,结合 index.js 源码与真实消费方 block-settings-dropdown.jsx,帮助你彻底掌握它的 Props、可用性标志的计算逻辑、各操作处理器的底层调用链,以及在自定义编辑器 UI 中复用的方法。

组件定位:只提供"能力",不提供"外观"

BlockActions是 block editor 内部的数据与动作封装层,其设计意图非常明确:

为"对一组块执行操作"提供处理函数(复制、删除、插入、成组、解组、粘贴样式),同时给出描述这些操作是否可用的标志。它自身不渲染任何 UI,由消费方通过 render prop 自行构建控件。

这意味着它是一个典型的逻辑层(logic layer)组件:把"能不能做"(flags)和"怎么做"(handlers)统一收敛到一处,UI 层只需关心渲染。源码 index.js 顶部的引用可以直观印证这一点——它完全由数据层驱动,没有任何 JSX 渲染逻辑:

import { useDispatch, useSelect } from '@wordpress/data'; import { hasBlockSupport, store as blocksStore } from '@wordpress/blocks'; import usePasteStyles from '../use-paste-styles'; import { store as blockEditorStore } from '../../store'; import { groupBlocks } from '../../utils/group-blocks';

重要限制:内部组件,不可从包外部导入

需要特别注意的是,BlockActions不会从@wordpress/block-editor导出,它只能在该包内部通过相对路径引用。因此:

  • 第三方插件无法通过import { BlockActions } from '@wordpress/block-editor'使用;
  • 若要在包内复用,导入路径形如import BlockActions from '../block-actions'
  • 该组件必须位于BlockEditorProvider组件树下(详见后文"使用前提"),因为其内部依赖 block editor 的 data store 上下文。

基本用法:render prop 驱动的自定义控件

README 给出的最小可用示例,展示了如何基于标志条件渲染按钮、并绑定操作处理器:

import BlockActions from '../block-actions'; <BlockActions clientIds={ selectedBlockIds }> { ( { onDuplicate, canDuplicate, onRemove, canRemove } ) => ( <> { canDuplicate && ( <button onClick={ onDuplicate }>Duplicate</button> ) } { canRemove && <button onClick={ onRemove }>Remove</button> } </> ) } </BlockActions>;

解读这个模式的三个要点:

  1. clientIds决定操作目标:组件对所有传入的块 ID 统一提供可用性判断与操作入口,天然支持"多选块"场景。
  2. render prop 接收一个对象:该对象同时包含布尔型可用性标志和函数型操作处理器,消费方按需取用、按需渲染。
  3. UI 完全归消费方:示例中使用原生<button>,你也可以使用@wordpress/componentsMenuItemButton等组件构建更复杂的菜单、工具栏。

Props 详解

clientIds

  • 类型:String[]
  • 作用:指定要操作的块的 client ID 数组。它是所有可用性判断与操作的输入基础——源码中getBlocksByClientId( clientIds )canRemoveBlocks( clientIds )duplicateBlocks( clientIds, ... )等均以它为参数。

children

  • 类型:Function
  • 作用:render prop,调用时传入单个对象参数。对象包含以下成员:

可用性标志(Availability flags):

属性类型含义
canRemoveBoolean这些块是否可以被删除
canDuplicateBoolean每个块是否都支持多实例,且能否插入到当前父级中
canInsertBlockBoolean是否可以在这些块旁边插入一个新块
canCopyStylesBoolean每个块是否都支持颜色或排版,从而有样式可供复制

操作处理器(Action handlers):

属性类型行为
onDuplicateFunction复制这些块,返回duplicateBlocksdispatch 的结果
onRemoveFunction删除这些块,返回removeBlocksdispatch 的结果
onInsertBeforeFunction在第一个块之前插入一个默认块
onInsertAfterFunction在最后一个块之后插入一个默认块
onGroupFunction用包含这些块的单个成组块替换这些块
onUngroupFunction用该块的内部块替换该块
onCopyFunction闪烁单个选中块以提示其已被复制;写入剪贴板由消费方负责
onPasteStylesFunction将复制的样式应用到这些块,返回Promise

__experimentalUpdateSelection

  • 类型:Boolean
  • 默认值:true
  • 作用:复制或删除后是否更新块选区。该值原样透传给duplicateBlocksremoveBlocks动作。从命名可知这是一个实验性 API,外部消费时应将其视为不稳定接口,避免依赖其长期行为。

可用性标志的源码级计算逻辑

四个标志全部由 index.js 中的useSelect计算得出,理解它们的判定条件,才能在自定义 UI 中正确预测按钮的可用状态。

canRemove:能否删除

canRemove: canRemoveBlocks( clientIds ),

它直接委托给 block editor store 的canRemoveBlocks选择器。查看 selectors.js 的实现:

export function canRemoveBlocks( state, clientIds ) { return clientIds.every( ( clientId ) => canRemoveBlock( state, clientId ) ); }

所有目标块都必须允许删除(canRemoveBlock内部会综合块锁lock.remove、父级模板锁templateLock等约束),只要有一个块不可删除,canRemove即为false

canDuplicate:能否复制

canDuplicate: blocks.every( ( block ) => { return ( !! block && hasBlockSupport( block.name, 'multiple', true ) && canInsertBlockType( block.name, rootClientId ) ); } ),

三个条件同时满足:

  1. 块真实存在(!! block);
  2. 块类型声明支持多实例——通过 hasBlockSupport 检查multiple支持位,默认值为true(即默认认为可以复制);
  3. 该块类型能插入到当前父级rootClientId下(canInsertBlockType)。

canInsertBlock:能否在旁插入新块

const canInsertDefaultBlock = canInsertBlockType( getDefaultBlockName(), rootClientId ); const directInsertBlock = rootClientId ? getDirectInsertBlock( rootClientId ) : null; // ... canInsertBlock: blocks.every( ( block ) => { return ( ( canInsertDefaultBlock || !! directInsertBlock ) && canInsertBlockType( block.name, rootClientId ) ); } ),

判定要点:当前父级能插入默认块(或通过getDirectInsertBlock返回了直接插入块,见 selectors.js),并且目标块自身也能插入到该父级。这保证了"插入一个默认块"在语义上是可行的。

canCopyStyles:能否复制样式

canCopyStyles: blocks.every( ( block ) => { return ( !! block && ( hasBlockSupport( block.name, 'color' ) || hasBlockSupport( block.name, 'typography' ) ) ); } ),

只要块声明支持colortypography中任意一类即可复制样式——因为样式复制依赖这两个 support 位所对应的属性体系。

操作处理器的底层调用链

render prop 对象中的八个处理器定义在 index.js,全部通过useDispatch( blockEditorStore )获取动作后薄封装。

onDuplicate/onRemove

onDuplicate() { return duplicateBlocks( clientIds, updateSelection ); }, onRemove() { return removeBlocks( clientIds, updateSelection ); },

它们把__experimentalUpdateSelection作为第二个参数透传给 store 动作。以duplicateBlocks为例,actions.js 中的完整逻辑包括:

  1. 空数组或块不存在时提前返回;
  2. 任一目标块不支持multiple时提前返回(与canDuplicate的判定一致);
  3. 通过cloneSanitizedBlock深克隆块;
  4. insertBlocks到最后一个选中块的索引之后;
  5. 若复制出多个块且updateSelection为真,则multiSelect全选新块;
  6. 返回新克隆块的 clientId 数组——这正是 README 所说"返回duplicateBlocksdispatch 的结果"。

onInsertBefore/onInsertAfter

onInsertBefore() { insertBeforeBlock( clientIds[ 0 ] ); }, onInsertAfter() { insertAfterBlock( clientIds[ clientIds.length - 1 ] ); },

分别以首块和末块为锚点调用insertBeforeBlock/insertAfterBlock动作。从 actions.js 可见,insertBeforeBlock会优先使用父级blockListSettings中的defaultBlock(直接插入块)构造新块,否则回退到insertDefaultBlock插入默认块。

onGroup/onUngroup

onGroup() { if ( ! clientIds.length ) return; const groupingBlockName = getGroupingBlockName(); const newBlocks = groupBlocks( getBlocksByClientId( clientIds ), groupingBlockName ); if ( ! newBlocks ) return; replaceBlocks( clientIds, newBlocks ); }, onUngroup() { if ( ! clientIds.length ) return; const innerBlocks = getBlocks( clientIds[ 0 ] ); if ( ! innerBlocks.length ) return; replaceBlocks( clientIds, innerBlocks ); },
  • 成组:取分组块名(默认core/group),调用 group-blocks.js 的groupBlocks工具。该工具在分组块的from变换中查找类型为blockisMultiBlockblocks含通配符*的变换,优先使用其__experimentalConvert进行结构性包裹(而非块的自身变换——块的自身变换会改变块的身份,例如引用块的成组变换会将其拆解为内部块),否则回退到switchToBlockType
  • 解组:取出目标块的内部块后直接replaceBlocks,将父块替换为其子块。

onCopy:闪烁反馈

onCopy() { if ( clientIds.length === 1 ) { flashBlock( clientIds[ 0 ] ); } },

仅当选中单个块时触发flashBlock做闪烁反馈。注意:真正把内容写入剪贴板不是它的职责——正如 README 强调的,"Writing to the clipboard is the consumer's responsibility",消费方需自行调用剪贴板 API(参见下方真实消费方的CopyMenuItem实现)。

onPasteStyles:粘贴样式

async onPasteStyles() { await pasteStyles( getBlocksByClientId( clientIds ) ); },

异步执行,返回Promise。其底层是usePasteStyles钩子(use-paste-styles/index.js),完整流程为:

  1. 检查window.navigator.clipboard可用性(http:站点除 localhost 外不可用),不可用时弹出错误通知;
  2. readText()读取剪贴板,权限被拒时提示"allow browser clipboard permissions";
  3. 通过hasSerializedBlocks判断剪贴板文本是否为序列化块(纯文本会被解析为core/freeform,据此排除);
  4. 解析出源块后,按STYLE_ATTRIBUTES列表(use-paste-styles/index.js:alignborderColorbackgroundColortextAligntextColorgradientclassNamefontFamilyfontSizelayoutstyle)逐属性过滤——仅当源块与目标块都支持该属性时才应用
  5. 递归处理内层块,并用registry.batch批量提交更新;
  6. 成功时通过noticesStore弹出成功通知(单个块显示块标题,多个块显示数量)。

真实消费场景:块设置菜单(Block Settings Dropdown)

BlockActions最核心的消费方是 block-settings-dropdown.jsx,它把BlockActions的能力映射为编辑器右上角"选项"(Options)下拉菜单中的各项菜单项:

  • Duplicate(复制):由canDuplicate+onDuplicate驱动,菜单项上还叠加了updateSelectionAfterDuplicate——复制动作返回新块 ID 后,用__experimentalSelectBlock聚焦新块;
  • Add before / Add after(在前/后添加):由canInsertBlock控制显隐,绑定onInsertBefore/onInsertAfter,并展示对应的键盘快捷键;
  • Copy / Cut(复制 / 剪切)CopyMenuItem使用useCopyToClipboardserialize( getBlocksByClientId( clientIds ) )写入剪贴板,并在回调中调用onCopy()触发闪烁;剪切模式则额外调用removeBlocks实现"移动"语义;
  • Copy styles / Paste styles(复制/粘贴样式):由canCopyStyles控制整组显隐,粘贴绑定onPasteStyles
  • Delete(删除):由canRemove控制,绑定onRemove,并配合updateSelectionAfterRemove在删除后将焦点/选区转移到前一块、父块或首个块;
  • 值得注意的细节:当canRemovecanDuplicatecanInsertBlock均为false且块处于contentOnly编辑模式时,整个菜单会直接渲染null,避免出现空菜单。

这个例子完整展示了 README 所说的协作模式:BlockActions 提供逻辑,消费方用DropdownMenuMenuItem等组件构建 UI

使用前提:必须位于 BlockEditorProvider 之下

README 的 "Related components" 一节明确指出:block editor 组件用于组合编辑器 UI,因此它们只能出现在BlockEditorProvider组件树内(参见 provider/README.md)。原因从源码即可看出:BlockActions大量使用useSelect/useDispatch访问core/block-editorstore(getBlocksByClientIdcanRemoveBlocksduplicateBlocks等),而该 store 是由BlockEditorProvider挂载并初始化的。脱离 Provider 使用,这些选择器与动作将无数据可依。

结语

BlockActions是理解 Gutenberg 块编辑器"数据层与 UI 层解耦"设计的一个极佳样本:它用不到两百行代码,把多选场景下最常见的八种操作、四种可用性判断以及选区更新策略统一收口,再通过 render prop 把全部控制权交还给 UI。掌握它,你既能读懂编辑器设置菜单、工具栏等大量 UI 背后的逻辑来源,也能在构建自定义块编辑器界面时,以同样的模式封装自己的"能力层"——先想清楚"哪些操作可用、各自依赖哪些 store 状态",再决定"UI 如何呈现"。

【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询