Gutenberg 命令面板(Command Palette)开发指南:@wordpress/commands 包的静态命令、动态加载器与上下文机制
【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg
导读
@wordpress/commands是 Gutenberg(WordPress 块编辑器)项目中用于构建命令面板(Command Palette)的通用包,它允许开发者注册、修改并以统一的命令菜单形式展示各类操作。本文以该包的官方文档为主体,结合仓库内 store、hooks 与 components 的源码实现,系统讲解命令注册的两种方式(静态与动态)、命令对象结构、上下文(context)与分类(category)机制,以及如何使用 WordPress Data API 编程式控制面板开关。阅读完本文,你将掌握在编辑器或站点编辑器中注册自有命令、实现随搜索词实时变化的动态命令、并利用上下文命令提升特定场景操作效率的完整实战方案。
一、命令面板是什么
命令面板(Command Palette)是一个统一的命令搜索与执行入口,在编辑器中按下cmd+k(Windows/Linux 下为Ctrl+K)即可唤起。它把散落在编辑器各处的操作——新建页面、打开偏好设置、切换模板、编辑文档等——聚合到一个可搜索、可键盘操作的面板中,与 VS Code 等现代编辑器的 Command Palette 体验一致。
在仓库的 components/command-menu.jsx 中可以看到,面板的快捷键core/commands被注册为category: 'global'、键位为modifier: 'primary'+character: 'k'的全局快捷键;触发时根据面板当前开关状态执行close()或open(),并额外通过withIgnoreIMEEvents忽略输入法(IME)组合事件,避免中文等输入法场景下的误触发:
registerShortcut( { name: 'core/commands', category: 'global', description: __( 'Open the command palette.' ), keyCombination: { modifier: 'primary', character: 'k' }, } );面板本体是一个基于cmdk库的模态框(Modal),包含搜索输入框、Recent(最近使用)、Suggestions(上下文建议)、Results(搜索结果)等多个分组,将在下文"渲染与交互实现"一节详述。
二、命令的两种注册方式:静态与动态
所有命令注册 API 都接收一个命令对象(command object),其完整字段如下:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 是 | 机器可读的唯一命令名,建议使用plugin/command-name命名空间前缀 |
label | string | 是 | 在面板中展示给人看的可读文本,一般用__()做国际化 |
icon | SVG 图标 / 元素 / 函数 | 是 | 命令左侧的 SVG 图标(可从@wordpress/icons引入) |
callback | Function | 是 | 命令被选中时执行的回调,接收{ close }等参数 |
category | string | 否 | 命令分类,见下文"命令分类";缺省或非法时回退为action |
context | string | 否 | 命令生效的上下文,见下文"上下文命令" |
keywords | string[] | 否 | 用于搜索匹配的附加关键词数组 |
searchLabel | string | 否 | 搜索时使用的独立标签,区别于展示用label |
disabled | boolean | 否 | 为true时不注册该命令 |
补充:
searchLabel与disabled虽未在 README 属性表中列出,但在 store/actions.js 的WPCommandConfig类型定义与 reducer 的字段落库逻辑中均真实存在,属于可直接使用的完整配置项。
2.1 静态命令(Static commands)
静态命令适用于"动作固定、不依赖搜索词"的场景,例如新增页面、打开编辑器偏好设置弹窗等。有两种注册方式:
方式一:React HookuseCommand(推荐在 React 组件内使用)
import { useCommand } from '@wordpress/commands'; import { plus } from '@wordpress/icons'; useCommand( { name: 'myplugin/my-command-name', label: __( 'Add new post' ), icon: plus, category: 'command', callback: ( { close } ) => { document.location.href = 'post-new.php'; close(); }, } );从 hooks/use-command.js 的实现看,useCommand本质上是registerCommand/unregisterCommand两个 data action 的封装:组件挂载时通过useEffect注册命令,卸载时自动注销(return () => unregisterCommand( command.name )),无需手动清理;command.disabled为true时直接跳过注册。回调通过useRef保存最新引用,避免因闭包捕获过期回调。
方式二:Data 派发wp.data.dispatch(...).registerCommand
import { store as commandsStore } from '@wordpress/commands'; wp.data.dispatch( commandsStore ).registerCommand( { name: 'myplugin/open-preferences', label: __( 'Open Editor Preferences' ), icon: settings, category: 'view', callback: ( { close } ) => { openPreferences(); close(); }, } );该方法与 Hook 等价,适合在非 React 上下文(如现有 JS 初始化代码)中使用。与之配对的unregisterCommand( name )用于移除命令,对应 action 实现在 store/actions.js。
批量注册多个静态命令可使用useCommandsHook,传入命令对象数组。useCommands为每个命令注册独立的条目,并在依赖数组变化或卸载时统一注销,适用于一个插件一次性暴露一组相关操作的场景,完整示例见 hooks/use-command.js。
2.2 动态命令(Dynamic commands)与命令加载器
当命令列表依赖于用户在面板输入框中敲入的搜索词,或仅在特定条件下才可用时,静态注册不再适用。此时需要"命令加载器(command loader)":通过useCommandLoaderHook 注册一个自定义 React Hook,该 Hook 接收{ search }参数并返回动态生成的命令数组。
文档给出的典型场景是:用户输入 "contact" 时,面板需要按该输入去过滤页面记录,尝试找到 Contact 页面。完整示例(页面搜索加载器)如下:
import { __ } from '@wordpress/i18n'; import { addQueryArgs } from '@wordpress/url'; import { useCommandLoader } from '@wordpress/commands'; import { page } from '@wordpress/icons'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { useMemo } from '@wordpress/element'; function usePageSearchCommandLoader( { search } ) { // 依据 "search" 词检索页面。 const { records, isLoading } = useSelect( ( select ) => { const { getEntityRecords } = select( coreStore ); const query = { search: !! search ? search : undefined, per_page: 10, orderby: search ? 'relevance' : 'date', }; return { records: getEntityRecords( 'postType', 'page', query ), isLoading: ! select( coreStore ).hasFinishedResolution( 'getEntityRecords', [ 'postType', 'page', query ] ), }; }, [ search ] ); // 生成命令列表。 const commands = useMemo( () => { return ( records ?? [] ).slice( 0, 10 ).map( ( record ) => { return { name: record.title?.rendered + ' ' + record.id, label: record.title?.rendered ? record.title?.rendered : __( '(no title)' ), icon: page, category: 'edit', callback: ( { close } ) => { const args = { p: '/page', postId: record.id, }; document.location = addQueryArgs( 'site-editor.php', args ); close(); }, }; } ); }, [ records ] ); return { commands, isLoading, }; } useCommandLoader( { name: 'myplugin/page-search', hook: usePageSearchCommandLoader, } );示例中的关键点:
- 加载器 Hook 必须返回
{ commands, isLoading },其中commands为满足条件的命令数组,isLoading告知面板当前是否仍在异步获取数据(如等待 REST 请求返回)。 - 查询参数
search为空时传undefined,此时orderby回退为date,即未输入时展示最近页面,输入后才按相关性排序——这是面板默认展示内容的常见策略。 useMemo以[ records ]为依赖缓存命令列表,避免每次渲染都重建。- 命令
name由标题与 ID 拼接(record.title?.rendered + ' ' + record.id),保证唯一性。 isLoading通过hasFinishedResolution取反获得,与 core-data 的实体请求解析状态联动。
从 hooks/use-command-loader.js 的实现可以看到两个细节:
- 加载器的
hook被包在一个稳定引用(useCallback空依赖)中,真正执行业务逻辑的 hook 保存在currentHookRef中——因此"面板总是调用最新的 hook",但更换 hook 实例不会触发面板重渲染、也不会重复注册加载器。 - 与
useCommand相同,组件卸载时自动执行unregisterCommandLoader( loader.name )清理,且disabled为true时跳过注册。
同样地,动态命令也可通过 Data 层注册:wp.data.dispatch( commandsStore ).registerCommandLoader( { name, hook, context, category } ),对应 action 见 store/actions.js。
三、上下文命令(Contextual commands)
静态命令与动态命令都可以声明为上下文命令:在特定上下文(例如处于站点编辑器导航中、正在编辑某个模板)下,这些命令在打开面板时优先显示,并且在输入搜索时排在其他命令之前。
当前已实现三种上下文,可在 hooks/use-command-context.js 与文档中对应确认:
| 上下文值 | 触发场景 |
|---|---|
site-editor | 正在站点编辑器(Site Editor)中导航,侧边栏可见 |
entity-edit | 正在编辑某个文档实体(模板、模板部件或页面) |
block-selection-edit | 有块被选中时 |
给命令挂载上下文,只需在useCommand/useCommandLoader的配置中加上context属性:
useCommand( { name: 'myplugin/template-actions', label: __( 'Template actions' ), icon: layout, context: 'entity-edit', // 仅编辑实体时高优先级显示 category: 'command', callback: ( { close } ) => { /* ... */ close(); }, } );底层实现:上下文由 store 维护,reducer 中context状态默认值为'root'(见 store/reducer.js),仅能通过私有 actionsetContext修改。useCommandContext( context )Hook(use-command-context.js)负责在组件挂载时设置上下文、卸载时恢复进入前的上下文,实现"进入站点编辑器→设置site-editor上下文→离开后还原"的自动管理。
Selector 层面,getCommands( contextual )与getCommandLoaders( contextual )接受布尔参数:传true只返回与当前上下文匹配的命令,传false(默认)返回其余命令(见 store/selectors.js)。面板渲染时正是据此把上下文命令单独取出、优先展示(见下文"Suggestions 与 Results 分组")。
四、命令分类(Command categories)
category用于描述命令执行的动作类型,面板据此对命令做视觉区分(在命令项右侧显示分类标签)。可用分类如下:
| 分类 | 含义 | 典型例子 |
|---|---|---|
command | 执行代码或切换状态 | 添加块、复制块 |
view | 导航到后台某区域或打开面板 | "前往:模板" |
edit | 导航去编辑某个文档 | 编辑模板、编辑页面 |
action | 其他分类都不匹配时的通用回退;传入非法分类时也默认回退到此 | 兜底 |
默认值与校验:不指定category时命令自动为action。在 store/actions.js 中定义了可注册分类集合:
const REGISTERABLE_CATEGORIES = new Set( [ 'command', 'view', 'edit', 'action' ] );registerCommand与registerCommandLoader都会执行校验:分类不存在于集合中时静默回退为action(源码注释注明未来版本将补充 development 模式下的警告输出)。此外,源码还预留了'workflow'分类(面板侧 command-menu.jsx 的CATEGORY_LABELS中包含workflow: __( 'Workflow' )),但它仅供内部保留使用,不允许通过公开 API 注册。
分类回退图标:分类还可提供回退图标——当命令未传自己的icon时使用。目前仅view分类定义了回退图标arrowRight(右箭头,见 command-menu.jsx),使得"导航到某处"类命令在整个面板中视觉语义一致。该分类下的命令被期望依赖回退图标而非自行传图,除非自身图标能表达箭头之外的额外信息(例如"在新标签页打开")。
五、WordPress Data API:编程式控制命令面板
命令面板的状态托管在名为core/commands的 Redux store 中(STORE_NAME = 'core/commands',见 store/index.js),可通过 WordPress Data API 读写。官方数据文档提供 selectors 与 actions 的完整清单,此处结合本仓库源码给出核心成员:
Selectors(store/selectors.js):
| Selector | 说明 |
|---|---|
getCommands( contextual = false ) | 获取已注册的静态命令列表,contextual=true时仅返回当前上下文命中的命令 |
getCommandLoaders( contextual = false ) | 获取已注册的命令加载器列表,同上 |
isOpen() | 命令面板当前是否打开 |
getContext() | 当前激活的上下文(root/site-editor/entity-edit/block-selection-edit) |
Actions(store/actions.js):
| Action | 说明 |
|---|---|
open() | 编程式打开命令面板 |
close() | 编程式关闭命令面板 |
registerCommand( config )/unregisterCommand( name ) | 注册 / 注销静态命令 |
registerCommandLoader( config )/unregisterCommandLoader( name ) | 注册 / 注销命令加载器 |
用法示例(如文档 API 部分所示):
import { store as commandsStore } from '@wordpress/commands'; import { useDispatch } from '@wordpress/data'; // 在组件内通过 Hook 打开命令中心: const { open: openCommandCenter } = useDispatch( commandsStore ); // 或在任意 JS 中直接派发: wp.data.dispatch( commandsStore ).open(); wp.data.select( commandsStore ).isOpen(); // => true / falsestore 的创建与注册见 store/index.js:通过createReduxStore构建、register全局注册,并用unlock( store ).registerPrivateActions / registerPrivateSelectors挂载私有 API(setContext、setLoaderLoading、isLoading等),私有成员需借助@wordpress/private-apis的 unlock 机制访问。
六、安装与样式引入
6.1 安装
npm install @wordpress/commands --save环境要求:该包假设运行环境为ES2015+。若你的目标环境对这类语言特性与 API 支持有限或完全不支持,应在代码中引入@wordpress/babel-preset-default提供的 polyfill(参见仓库内 packages/babel-preset-default 的 polyfill 说明)。
版本与依赖:当前仓库中该包版本为1.55.0,Node 要求>=18.12.0、npm>=8.19.2,React 18 / 19 均作为 peer 依赖支持(见 packages/commands/package.json)。核心运行依赖包括@wordpress/components、@wordpress/data、@wordpress/i18n、@wordpress/icons、@wordpress/keyboard-shortcuts、@wordpress/keycodes、@wordpress/preferences以及面板 UI 底层库cmdk。
6.2 样式引入
为保证命令面板正常显示,需要引入以下样式表(node_modules内):
/* From node_modules: */ @import '@wordpress/components/build-style/style.css'; @import '@wordpress/commands/build-style/style.css';第一行引入底层组件库(Modal、HStack、TextHighlight 等)样式,第二行引入命令面板自身样式。本包自身的 SCSS 源文件位于 packages/commands/src/style.scss 与 packages/commands/src/components/style.scss。
七、渲染与交互实现:面板内部工作机制
从组件源码(components/command-menu.jsx)可以完整还原面板的渲染流程,有助于理解文档所述各机制的实际落地:
- 命令项渲染:
CommandItem根据命令的category选择图标(command.icon ?? CATEGORY_FALLBACK_ICONS[ category ]),用TextHighlight对label做搜索词高亮,并在右侧渲染CATEGORY_LABELS[ category ]分类标签;keywords数组会注入cmdk的匹配逻辑,用于辅助搜索。选中命令时调用recordUsage( command.name )记录使用痕迹(供"最近使用"分组排序),随后执行command.callback( { close } )。 - 三种分组策略:
RecentGroup(未输入搜索词时):展示最近使用过的命令,是提升高频操作效率的关键交互;SuggestionsGroup(未输入搜索词时):通过getCommands( true )/getCommandLoaders( true )拉取当前上下文的命令作为建议项——这就是"上下文命令优先可见"的渲染落点;ResultsGroup(输入搜索词后):先渲染常规结果(getCommands( false )),再把上下文命令(getCommands( true ))追加在后面,实现文档所述"上下文命令排在其他命令之上"。
- 空状态:搜索非空且所有 loader 加载完毕(
loadersLoading为假)但仍无结果时,展示 "No results found."。 - 加载器渲染:每个 loader 的
hook被CommandMenuLoaderWrapper以 key 强制的重挂载方式调用(避免违反 React Hooks 规则),isLoading状态同步进 store 的loaderStates,供空状态判断使用。
八、小结与延伸阅读
@wordpress/commands以"静态命令 + 动态加载器"双轨注册模型,配合上下文优先级与分类视觉体系,为编辑器提供了一个可无限扩展的键盘驱动命令入口。无论是注册一个固定动作、实现按搜索词实时过滤的实体命令,还是把面板能力包装成语义清晰的分类,都可以在本文示例的基础上直接落地。
想深入底层,建议继续阅读本仓库内以下文件:
- 官方包文档:packages/commands/README.md
- 数据层:actions 与 selectors 定义 packages/commands/src/store/actions.js、packages/commands/src/store/selectors.js、状态归约 packages/commands/src/store/reducer.js
- Hooks 层:packages/commands/src/hooks/use-command.js、packages/commands/src/hooks/use-command-loader.js、packages/commands/src/hooks/use-command-context.js
- 面板组件:packages/commands/src/components/command-menu.jsx
- 包元信息与依赖:packages/commands/package.json
- Redux store 机制参考:packages/data/README.md;私有 API 解锁机制参考:packages/private-apis
【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考