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>;解读这个模式的三个要点:
clientIds决定操作目标:组件对所有传入的块 ID 统一提供可用性判断与操作入口,天然支持"多选块"场景。- render prop 接收一个对象:该对象同时包含布尔型可用性标志和函数型操作处理器,消费方按需取用、按需渲染。
- UI 完全归消费方:示例中使用原生
<button>,你也可以使用@wordpress/components的MenuItem、Button等组件构建更复杂的菜单、工具栏。
Props 详解
clientIds
- 类型:
String[] - 作用:指定要操作的块的 client ID 数组。它是所有可用性判断与操作的输入基础——源码中
getBlocksByClientId( clientIds )、canRemoveBlocks( clientIds )、duplicateBlocks( clientIds, ... )等均以它为参数。
children
- 类型:
Function - 作用:render prop,调用时传入单个对象参数。对象包含以下成员:
可用性标志(Availability flags):
| 属性 | 类型 | 含义 |
|---|---|---|
canRemove | Boolean | 这些块是否可以被删除 |
canDuplicate | Boolean | 每个块是否都支持多实例,且能否插入到当前父级中 |
canInsertBlock | Boolean | 是否可以在这些块旁边插入一个新块 |
canCopyStyles | Boolean | 每个块是否都支持颜色或排版,从而有样式可供复制 |
操作处理器(Action handlers):
| 属性 | 类型 | 行为 |
|---|---|---|
onDuplicate | Function | 复制这些块,返回duplicateBlocksdispatch 的结果 |
onRemove | Function | 删除这些块,返回removeBlocksdispatch 的结果 |
onInsertBefore | Function | 在第一个块之前插入一个默认块 |
onInsertAfter | Function | 在最后一个块之后插入一个默认块 |
onGroup | Function | 用包含这些块的单个成组块替换这些块 |
onUngroup | Function | 用该块的内部块替换该块 |
onCopy | Function | 闪烁单个选中块以提示其已被复制;写入剪贴板由消费方负责 |
onPasteStyles | Function | 将复制的样式应用到这些块,返回Promise |
__experimentalUpdateSelection
- 类型:
Boolean - 默认值:
true - 作用:复制或删除后是否更新块选区。该值原样透传给
duplicateBlocks与removeBlocks动作。从命名可知这是一个实验性 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 ) ); } ),三个条件同时满足:
- 块真实存在(
!! block); - 块类型声明支持多实例——通过 hasBlockSupport 检查
multiple支持位,默认值为true(即默认认为可以复制); - 该块类型能插入到当前父级
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' ) ) ); } ),只要块声明支持color或typography中任意一类即可复制样式——因为样式复制依赖这两个 support 位所对应的属性体系。
操作处理器的底层调用链
render prop 对象中的八个处理器定义在 index.js,全部通过useDispatch( blockEditorStore )获取动作后薄封装。
onDuplicate/onRemove
onDuplicate() { return duplicateBlocks( clientIds, updateSelection ); }, onRemove() { return removeBlocks( clientIds, updateSelection ); },它们把__experimentalUpdateSelection作为第二个参数透传给 store 动作。以duplicateBlocks为例,actions.js 中的完整逻辑包括:
- 空数组或块不存在时提前返回;
- 任一目标块不支持
multiple时提前返回(与canDuplicate的判定一致); - 通过
cloneSanitizedBlock深克隆块; insertBlocks到最后一个选中块的索引之后;- 若复制出多个块且
updateSelection为真,则multiSelect全选新块; - 返回新克隆块的 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变换中查找类型为block、isMultiBlock且blocks含通配符*的变换,优先使用其__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),完整流程为:
- 检查
window.navigator.clipboard可用性(http:站点除 localhost 外不可用),不可用时弹出错误通知; readText()读取剪贴板,权限被拒时提示"allow browser clipboard permissions";- 通过
hasSerializedBlocks判断剪贴板文本是否为序列化块(纯文本会被解析为core/freeform,据此排除); - 解析出源块后,按
STYLE_ATTRIBUTES列表(use-paste-styles/index.js:align、borderColor、backgroundColor、textAlign、textColor、gradient、className、fontFamily、fontSize、layout、style)逐属性过滤——仅当源块与目标块都支持该属性时才应用; - 递归处理内层块,并用
registry.batch批量提交更新; - 成功时通过
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使用useCopyToClipboard将serialize( getBlocksByClientId( clientIds ) )写入剪贴板,并在回调中调用onCopy()触发闪烁;剪切模式则额外调用removeBlocks实现"移动"语义; - Copy styles / Paste styles(复制/粘贴样式):由
canCopyStyles控制整组显隐,粘贴绑定onPasteStyles; - Delete(删除):由
canRemove控制,绑定onRemove,并配合updateSelectionAfterRemove在删除后将焦点/选区转移到前一块、父块或首个块; - 值得注意的细节:当
canRemove、canDuplicate、canInsertBlock均为false且块处于contentOnly编辑模式时,整个菜单会直接渲染null,避免出现空菜单。
这个例子完整展示了 README 所说的协作模式:BlockActions 提供逻辑,消费方用DropdownMenu、MenuItem等组件构建 UI。
使用前提:必须位于 BlockEditorProvider 之下
README 的 "Related components" 一节明确指出:block editor 组件用于组合编辑器 UI,因此它们只能出现在BlockEditorProvider组件树内(参见 provider/README.md)。原因从源码即可看出:BlockActions大量使用useSelect/useDispatch访问core/block-editorstore(getBlocksByClientId、canRemoveBlocks、duplicateBlocks等),而该 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),仅供参考