Gutenberg 区块编辑器数据层实战:core/block-editor 的 Selectors 与 Actions 到底怎么用
【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg
你的插件需要知道用户在 WordPress 块编辑器里当前选中了哪个区块、在它后面插一个自定义区块、或者拦住某个受保护区块不让删——这些诉求最终都落在core/block-editor这个 store 上:selectors 负责读状态,actions 负责让编辑器动起来。这篇文章不按 API 罗列,而是按"你要完成的任务"来组织 Gutenberg 数据层的实际用法。
一、心智模型:编辑器状态是怎么流转的
理解这个 store 之前,先搞清楚三件事:它叫什么、怎么注册、状态长什么样。
命名空间常量定义在 constants.js 里,就一行:STORE_NAME = 'core/block-editor'。store 本身的组装在 index.js:先拼出storeConfig = { reducer, selectors, actions },再用createReduxStore创建,并显式声明persist: [ 'preferences' ]。
persist这一行容易被忽略,但它有实际后果:preferences片段会被持久化到客户端存储,其中就包括insertUsage(各区块的使用记录)——这正是插入器 frecency 排序的数据来源,所以"最近常用区块排前面"这类行为是跨会话生效的。
同一文件里还有一段边界声明:unlock( registeredStore ).registerPrivateActions( privateActions )与registerPrivateSelectors( ... )把 private-actions.js 和 private-selectors.js 挂进了 store。这定义了 API 面:普通select()/dispatch()能拿到的才是稳定接口,private 部分不面向第三方插件承诺。写插件时只用公开面,就不用担心升级被坑。
组件内外怎么访问
组件外直接走@wordpress/data:
import { select, dispatch } from '@wordpress/data'; import { store as blockEditorStore } from '@wordpress/block-editor'; const { getSelectedBlockClientId } = select( blockEditorStore ); const { insertBlock } = dispatch( blockEditorStore );组件内换成useSelect/useDispatch,语义一致。所有 selector 的签名第一个参数都是state,由框架自动注入,你调用时不用传。
状态分块
reducer.js 用combineReducers把状态切成了几十个片段,核心的是这几块:
blocks:区块树本体,内部含byClientId(按 clientId 的区块 Map)、parents(子→父映射)、tree(各嵌套层级的有序块列表)。绝大多数 selector 都是在读这三张表。selection:选区起止,含 clientId、属性键、文本偏移。settings/preferences:编辑器设置与用户偏好。- 交互标志:
isDragging、draggedBlocks、isMultiSelecting、isTyping、insertionPoint、template、highlightedBlock、lastBlockInserted、blockVisibility等。
另外 reducer 外层还包了两个高阶函数(withDerivedBlockEditingModes、withAutomaticChangeReset),分别负责派生编辑模式和自动变更状态复位——读源码时看到derivedBlockEditingModes之类的派生状态,出处就在这里。
二、任务一:读状态、做判断("这块能不能动")
selectors 全部实现在 selectors.js,下面按读取目的分组。
怎么读区块对象和属性
当你需要回答"这个块是什么、长什么样"时用这组:
getBlock( clientId ):返回该 clientId 的解析区块对象(含name、clientId、attributes、innerBlocks)。注意它返回的是解析副本,不是区块类型注册配置——注册配置(edit、save、attributes默认值等)要从core/blocks模块拿。getBlockName( clientId )、getBlockAttributes( clientId ):只取名称或属性,查不到返回null。getBlocks( rootClientId? ):按文档顺序返回某块列表(默认根列表)的区块数组,排除受控内层控制器的子块(坑点见第七节)。getBlockCount( rootClientId? )、getGlobalBlockCount( blockName? ):计数,后者按名称过滤且包含嵌套。
怎么导航区块层级和顺序
批量操作、"只作用于 Group 里的块"这类需求都靠这组:
getBlockOrder( rootClientId? )返回有序 clientId 数组,getBlockIndex( clientId )返回索引(不存在为-1)。getBlockRootClientId( clientId ):父级 clientId,顶层块是空字符串'',块不存在返回null——''和null语义不同,判空时留意。getBlockParents( clientId, ascending )、getBlockParentsByBlockName( clientId, [ 'core/group' ] ):向上遍历父链,后者可按名称过滤,比如只认 Cover 里的块。getClientIdsOfDescendants( rootIds ):拿给定块的全部后代;getAdjacentBlockClientId( startClientId, modifier )的modifier取1/-1表示下一个/上一个。
怎么读当前选区与多选
getSelectedBlockClientId()拿到单选 clientId 后,配合上面的读取器就够了;getMultiSelectedBlockClientIds()返回多选集合(无多选是空数组)。- 谓词类:
hasSelectedBlock、isBlockSelected(强调单选)、isBlockMultiSelected、isBlockWithinSelection(在选区内但不是末尾)。 - 选区起止还有更细的
getBlockSelectionStart/End(clientId 级)和getSelectionStart/End(含属性键与文本偏移),做行内工具栏定位时才需要。
怎么判断区块能不能被插入
can*一族是写操作前的标准前置检查:
canInsertBlockType( blockName, rootClientId? ):某类型能否插入到某块列表。它不是简单查表——内部用createRegistrySelector组合了跨 store 的插入规则与记忆化,属于复合权限判断。canRemoveBlock( clientId )/canRemoveBlocks( clientIds )、canEditBlock、canMoveBlock(s):删除、编辑、移动同理。getTemplateLock( rootClientId? ):模板锁定义,未设置时是false。模板锁是"能不能动"的最终裁判,can*的结果都受它影响。getAllowedBlocks( rootClientId? ):某根下允许插入的类型列表(不传 rootClientId 不返回内容);getBlockListSettings读该块的列表级设置。
三、任务二:写操作("让编辑器动起来")
actions 全部在 actions.js,统一dispatch( blockEditorStore ).actionName( ... )调用。
插入与删除:最常用的两个
insertBlock( block, index?, rootClientId?, updateSelection?, initialPosition?, meta? )插入单个块,insertBlocks是批量版。源码注释把两个关键行为写得很直白:只有被允许的块才会被插入,块不被允许或目标列表存在 templateLock 时,动作会静默失败——没有报错、没有返回 false,你的 UI 得自己先判断。参数里initialPosition设为null可阻止新块抢焦点,updateSelection默认true。另有insertDefaultBlock、insertAfterBlock、insertBeforeBlock处理"追加一个默认块"这类场景。
删除侧:removeBlock( clientId, selectPrevious )、removeBlocks( clientIds, selectPrevious = true )。注意第二个参数默认就是true:删除后自动选中前一个块,若没有前一个则选中直接父块——做批量清理时如果不想动选区,显式传false。
替换、合并与复制
replaceBlock( clientId, block )/replaceBlocks( clientIds, blocks, indexToSelect, initialPosition, meta? ):替换时可指定选中哪个新块。replaceInnerBlocks( rootClientId, blocks, updateSelection = false ):整体换掉某块的子块,注意选区默认不更新,和insertBlocks的默认值相反,容易踩反。mergeBlocks( first, second )合并相邻块;duplicateBlocks( clientIds, updateSelection )复制。
移动与批量更新
moveBlockToPosition( clientId, fromRootClientId?, toRootClientId?, index )和moveBlocksToPosition:两个 from/to root 参数让跨块列表移动成为可能,不传时默认落在根列表。updateBlock( clientId, updates )更新单块;updateBlockAttributes( clientIds, attributes, options )批量改属性——给一组同类型块统一加align用前者,而各块属性不同时传options.uniqueByBlock: true,此时attributes按 clientId 键控,每块一份。resetBlocks( blocks )是整个状态与"编辑器里其他一切"之间的总闸,一般只有编辑器宿主会用。- 选区写操作:
selectBlock( clientId, initialPosition = 0 )(-1反向选中、null不聚焦)、multiSelect( start, end )、clearSelectedBlock、startMultiSelect/stopMultiSelect、selectionChange。
四、任务三:进阶交互(插入点、拖拽、编辑模式)
插入点与拖拽状态
拖放交互的"提示线"由一对 action 控制:showInsertionPoint( rootClientId?, index?, __unstableOptions? )显示(options 里可带__unstableWithInserter决定是否露出插入按钮),hideInsertionPoint隐藏;读侧用getBlockInsertionPoint。
拖拽同样是"信号 + 状态"结构:startDraggingBlocks( clientIds )/stopDraggingBlocks打信号,UI 层用getDraggedBlockClientIds、isDraggingBlocks、isBlockBeingDragged(区分"它自己正被拖"与"它是被拖父块的子块",后者配合isAncestorBeingDragged)来做淡化高亮这类效果。
编辑模式三态与模板锁联动
getBlockEditingMode( clientId = '' )返回三态之一:
| 模式 | 行为 | 典型来源 |
|---|---|---|
'disabled' | 完全禁止编辑,连选中都不行 | 模板锁覆盖、只读场景 |
'contentOnly' | 隐藏工具栏辅助控件、块移动器、设置面板,只留内容编辑 | 区块自设(useBlockEditingMode)、content 角色块遇到内容锁 |
'default' | 正常编辑 | 默认值 |
三条规则值得记住:模式沿内层块向下继承(子块自设则覆盖);区块类型可用useBlockEditingMode钩子声明模式;模板锁为'contentOnly'时会强制覆盖——块的 content role 属性决定落到'contentOnly'还是'disabled'。写入侧是setBlockEditingMode( clientId = '', mode )与unsetBlockEditingMode(''表示根容器)。
其余状态类 API 一句话带过:toggleBlockHighlight/flashBlock( clientId, timeout = 150 )控制高亮,setBlockVisibility配isBlockVisible控制画布可见性,startTyping/stopTyping配isTyping让插件在用户输入时闭嘴,wasBlockJustInserted( clientId, source? )判断某块是否刚被插入,synchronizeTemplate/setTemplateValidity处理模板同步与校验。
五、控制插入器里"出现什么"
插入器面板显示哪些条目,由getInserterItems( rootClientId? )决定:它同时返回静态条目(已注册的区块类型)与动态条目(如可复用区块),每项都带渲染按钮所需的全部字段(name、title、icon、category、keywords、initialAttributes、isDisabled等)。排序规则源码注释写得明白:按utility与frecency降序,而 frecency 是一个结合使用频率(frequency)与时效(recency)的启发式值——数据源就是第一节说的持久化insertUsage。判断"这里到底有没有可插的"用hasInserterItems。
想把外部媒体源接进插入器的"媒体"选项卡,用registerInserterMediaCategory。约定三个接口:请求侧InserterMediaRequest(per_page、page、search);条目侧InserterMediaItem(title、url必填,previewUrl、sourceId、alt、caption可选);fetch若返回{ mediaItems, totalItems, totalPages }形式的InserterMediaResponse,媒体选项卡会为其渲染分页控件。分类配置里name必须全局唯一,mediaType限'image' | 'audio' | 'video',可选getReportUrl、emptyMessage。注意源码里注册动作会把分类强制标记为外部资源,避免"有没有内容"的探测请求打向你的 API。
一个接自有库存图 API 的最小示例(返回分页形式):
wp.data.dispatch( 'core/block-editor' ).registerInserterMediaCategory( { name: 'my-stock-images', labels: { name: '我的库存图', search_items: '搜索库存图', }, mediaType: 'image', async fetch( { per_page, page, search } ) { const url = new URL( '/wp-json/my-plugin/v1/stock-images' ); url.searchParams.set( 'per_page', per_page ); if ( page ) { url.searchParams.set( 'page', page ); } if ( search ) { url.searchParams.set( 'search', search ); } const res = await window.fetch( url ); const json = await res.json(); return { mediaItems: json.images.map( ( img ) => ( { title: img.title, url: img.src, previewUrl: img.thumb, sourceId: img.externalId, } ) ), totalItems: json.total, totalPages: json.pages, }; }, } );六、完整小 Demo:选中块后高亮它,并在其后追加同款块
把读、判、写串起来的最小实用组件:工具栏上一个"插入同款"按钮。
import { useSelect, useDispatch } from '@wordpress/data'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { Button } from '@wordpress/components'; export default function InsertCloneButton() { // 读侧:一次拿全判断所需状态 const { clientId, name, attributes, index, canInsert } = useSelect( ( select ) => { const { getSelectedBlockClientId, getBlockName, getBlockAttributes, getBlockIndex, canInsertBlockType, } = select( blockEditorStore ); const clientId = getSelectedBlockClientId(); const name = clientId ? getBlockName( clientId ) : null; return { clientId, name, attributes: clientId ? getBlockAttributes( clientId ) : null, index: clientId ? getBlockIndex( clientId ) : -1, canInsert: !! name && canInsertBlockType( name, '' ), }; } ); const { insertBlock, toggleBlockHighlight } = useDispatch( blockEditorStore ); const onInsertClone = () => { if ( ! clientId || ! canInsert ) { return; // 前置判断不过就什么都不做 } // 先高亮原块,让用户看清新块落在哪 toggleBlockHighlight( clientId, true ); insertBlock( { name, attributes: { ...attributes } }, index + 1, '' ); }; if ( ! clientId ) { return null; } return <Button onClick={ onInsertClone }>插入同款区块</Button>; }这个 Demo 覆盖了本数据层的标准姿势:useSelect里集中读、只回传需要的字段;点击前用canInsertBlockType挡住注定失败的操作(insertBlock不会告诉你为什么没插进去);insertBlock的index + 1精确落到原块之后,''表示根列表。组件外做同类事(比如快捷键回调)就是select()+dispatch()的等价写法,例如删掉当前多选:
const ids = select( blockEditorStore ).getMultiSelectedBlockClientIds(); if ( ids.length ) { dispatch( blockEditorStore ).removeBlocks( ids, false ); }七、容易踩的坑
getBlock().innerBlocks可能是空的,但块明明有子块。同步模式(synced pattern)和模板部件是"内层区块控制器":子块归属于另一个实体,被刻意排出了所在文档的树。想要它的子块,用getBlocks( 它的 clientId );判断某块是否为控制器,用areInnerBlocksControlled。源码里这类块的子块挂在带controlled||前缀的独立树键下,正是这个设计的落点。insertBlock静默失败。块不在允许列表、或目标列表有templateLock时,不报错也不抛异常。稳妥姿势永远是canInsertBlockType+getTemplateLock先过一遍再动手。isMultiSelecting和hasMultiSelection不是一回事。前者只在多选进行中(鼠标拖选期间)为true,落定即false;后者表示多选已建立。想"拖选时隐藏工具栏"用前者,想"存在多选时启用批量按钮"用后者。- 记忆化对
useSelect的意义。高频 selector 基于createSelector记忆化(getInserterItems、canInsertBlockType等还叠加createRegistrySelector做跨 store 组合)。依赖没变时直接复用缓存结果——文档上千块的场景下,useSelect里只回传你真正需要的原始值(如 clientId、布尔),而不是整块对象,能显著减少无效重渲染。 - 已弃用 API 别用。
getHoveredBlockClientId、isCaretWithinFormattedText、hoverBlock、enterFormattedText/exitFormattedText、setBlockMovingClientId、receiveBlocks等在官方文档中均标记 Deprecated,替代方案以官方文档为准。
core/block-editor是块编辑器状态管理的枢纽:selectors 管"看清现状、判断可行性",actions 管"安全地改变现状",两者配合加上第七节这几个坑,日常插件需求基本都覆盖到了。想要完整 API 清单,请查阅仓库内的 Data Module Reference 与该 store 的专页 contenteditable="false">【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考