news 2026/9/17 3:50:09

在 Gutenberg 中使用 ResponsiveBlockControl 构建按视口区分的区块控件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
在 Gutenberg 中使用 ResponsiveBlockControl 构建按视口区分的区块控件

在 Gutenberg 中使用 ResponsiveBlockControl 构建按视口区分的区块控件

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

本文围绕 WordPress Gutenberg(块编辑器)中@wordpress/block-editor包提供的__experimentalResponsiveBlockControl组件展开,讲解如何为标准化的"按视口/屏幕尺寸分别设置"的区块控件提供统一接口。你将掌握该组件的全部 Props 用法、默认渲染行为、自定义响应式渲染方案、可访问性实现原理,并结合仓库源码与测试用例理解其底层工作机制,最终能够在自己的区块edit实现中落地一套可复用的响应式设置界面。

组件定位:为什么需要按视口区分的区块设置

在开发自定义区块时,一个常见需求是:某个布局属性(例如区块的内边距padding、外边距margin)在"大屏"上表现良好,但在"小屏"上可能过大或过小。此时就需要为每种屏幕尺寸分别提供一套设置值,即"响应式设置"。

ResponsiveBlockControl正是 Gutenberg 为这类场景提供的标准化组件:它把"默认(All,所有视口统一)"与"按视口分别设置(Small / Medium / Large)"两种模式的切换界面统一封装起来,开发者只需专注于渲染具体的值控件(如SelectControlNumberControl),而把切换开关、分组渲染、可访问性标签等公共逻辑交给组件处理。

它的核心特点可以概括为三点:

  • 渲染完全可控:通过renderDefaultControl与可选的renderResponsiveControls两个渲染函数,开发者可以完全掌控控件输出;
  • 视口完全可定制:通过viewports属性可以自定义任意数量的视口配置,不局限于 Small / Medium / Large 三档;
  • 纯"受控组件"ResponsiveBlockControl本身不负责持久化任何控件值,你传入的控件需要自行处理值的保存(例如写入区块 attributes)。

从源码看,该组件定义在 packages/block-editor/src/components/responsive-block-control/index.jsx,并通过 packages/block-editor/src/components/index.js 以__experimentalResponsiveBlockControl的名称对外导出(组件名含__experimental前缀,表明其 API 仍在实验阶段,可能在后续版本调整)。

基本用法:在区块 edit 中接入

在区块的edit实现中,将<ResponsiveBlockControl />放入InspectorControls(设置侧栏)内即可。你需要提供:

  1. renderDefaultControl函数——渲染具体的界面控件;
  2. isResponsive布尔状态——标记当前是否处于"响应式模式";
  3. onIsResponsiveChange回调——切换模式时更新状态。

默认情况下,renderDefaultControl返回的控件既用于渲染"All"默认状态,也(在未提供renderResponsiveControls时)用于逐个渲染每个视口的响应式控件。

以下是最小可运行的示例(来自组件 README.md):

import { useState } from 'react'; import { registerBlockType } from '@wordpress/blocks'; import { InspectorControls, __experimentalResponsiveBlockControl as ResponsiveBlockControl, } from '@wordpress/block-editor'; registerBlockType( 'my-plugin/my-block', { // ... edit( { attributes, setAttributes } ) { const [ isResponsive, setIsResponsive ] = useState( false ); const { paddingSize } = attributes; // 你的自定义控件可以是任何你想要的界面元素。 const paddingControl = ( labelComponent, viewport ) => { return ( <input type="number" label={ viewport.label } onChange={ /* 在此处理 padding 值更新 */ } value={ paddingSize } /> ); }; return ( <> <InspectorControls> <ResponsiveBlockControl title='Block Padding' property='padding' renderDefaultControl={ paddingControl } isResponsive={ isResponsive } onIsResponsiveChange={ () => { setIsResponsive( ! isResponsive ); } } /> </InspectorControls> <div> { /* 你的区块渲染 */ } </div> </> ); } } );

需要注意:上面示例仅为演示renderDefaultControl的签名。在实践中,若要让每个视口分别保存各自的值,更常见的做法是把viewport.id并入属性键(例如paddingSizepaddingSizeSmall/paddingSizeMedium/paddingSizeLarge分开存储),并在onChange中根据viewport.id写入对应的 attributes 字段。

Props 全解:从必填到可选的每一项

title

  • 类型:String
  • 默认值:undefined
  • 必填:true

整个控件组的标题,被用作<fieldset><legend>元素的文本,用来标记整组控件。从测试用例可见,渲染后会被辅助技术识别为role="group"(名称为 title 文本)——详见 test/index.jsdom.test.jsx。

property

  • 类型:String
  • 默认值:undefined
  • 必填:true

用于构建控件组的可访问标签与 ARIA 角色,应表示该组件控制的布局属性(如paddingmargin等)。在源码中,property被传入ResponsiveBlockControlLabel,最终拼装成"Controls the padding property for All viewports."这类完整的可访问描述文本。

isResponsive

  • 类型:Boolean
  • 默认值:false
  • 必填:false

决定组件显示默认控件还是响应式控件,同时驱动切换开关(ToggleControl)的勾选状态。源码中切换开关的checked值为! isResponsive(即勾选代表"所有屏幕使用相同值"),onChange直接绑定onIsResponsiveChange。当isResponsivefalse时渲染默认控件;为true时渲染响应式控件组。

onIsResponsiveChange

  • 类型:Function
  • 默认值:undefined
  • 必填:true

切换开关值变化时被调用的回调函数,用于把isResponsive状态更新为最新值,使组件成为标准的"受控"组件。

renderDefaultControl

  • 类型:Function
  • 默认值:undefined
  • 必填:true
  • 参数:
    • labelComponent:Function)一个已渲染好的ResponsiveBlockControlLabel组件,可直接作为你控件的<label>
    • viewport:Object)表示当前视口属性的对象(含idlabel)。

用于渲染需要按视口展示的控件。例如一个控制 padding 大小的SelectControl,把它作为renderDefaultControl传入后,组件会在"All"状态用它渲染默认控件;若未提供renderResponsiveControls,还会在响应式模式下用它自动渲染每个视口的控件。

组件会为你的控件传入一个预先创建好的、可访问的<label>,你还可以利用viewport参数中的上下文信息(如当前渲染的是All还是某个响应式视口)来调整控件输出。下面是一个结合SelectControl的示例:

const renderDefaultControl = ( labelComponent, viewport ) => { const { id, label } = viewport; // eg: // { // id: 'small', // label: 'All' // } return <SelectControl label={ labelComponent } />; };

从 index.jsx 源码可以看出,renderDefaultControl无论处于哪种模式都会被调用一次(用于渲染默认控件),响应式模式下还会为每个视口各调用一次(见defaultResponsiveControls中的循环,index.jsx)。测试用例也验证了这一调用次数规律:自定义视口集合有 N 个时,renderDefaultControl共被调用N + 1次(默认一次 + 每个视口一次)。

renderResponsiveControls

  • 类型:Function
  • 默认值:undefined
  • 必填:false
  • 参数:
    • viewports:Array)viewport 对象数组,每个对象含idlabel属性。

可选的响应式控件渲染函数。如果不提供,组件会使用renderDefaultControl返回的控件自动为每个视口生成响应式控件;如果提供,则在响应式模式下完全用你返回的组件替换输出,实现 100% 的控制权。

let uniqueId = 0; const renderResponsiveControls = ( viewports ) => { const inputId = ++uniqueId; return viewports.map( ( { id, label } ) => { return ( <Fragment key={ `${ inputId }-${ id }` }> <label htmlFor={ `${ inputId }-${ id }` }> Custom Viewport { label } </label> <input id={ `${ inputId }-${ id }` } defaultValue={ label } type="range" /> </Fragment> ); } ); };

注意源码中的渲染分支(index.jsx):

{ isResponsive && ( renderResponsiveControls ? renderResponsiveControls( viewports ) : defaultResponsiveControls() ) }

即:renderResponsiveControls存在时优先使用它,否则回退到用renderDefaultControl自动生成。同时,即便提供了renderResponsiveControlsrenderDefaultControl仍会被调用一次(用于默认模式),这一点在测试中也有明确断言。

toggleLabel

  • 类型:String
  • 默认值:Use the same %s on all screen sizes.(其中%s会被替换为property属性的值)
  • 必填:false

切换开关的显示文本。不传时,源码会通过sprintf基于property动态生成,例如property="padding"时显示 "Use the same padding on all screen sizes."(见 index.jsx)。

defaultLabel

  • 类型:Object
  • 默认值:
{ id: 'all', label: 'All', }
  • 必填:false

描述默认值的对象。默认All表示该控件作用于"所有视口/屏幕尺寸"。需要自定义"默认档"的名称与 id 时(例如测试中的{ id: 'everything', label: 'Everything' }),可传入此属性覆盖。

viewports

  • 类型:Array
  • 默认值:
[ { id: 'small', label: 'Small screens', }, { id: 'medium', label: 'Medium screens', }, { id: 'large', label: 'Large screens', }, ];
  • 必填:false

viewport 对象数组,每个对象描述一种视口尺寸的配置(idlabel)。它决定了响应式模式下渲染的控件数量与各自配置。你可以自由定制视口集合——例如测试用例中使用了tiny / small / medium / huge四个自定义视口,验证了renderDefaultControl恰好被调用4 + 1 = 5次(test/index.jsdom.test.jsx)。

可访问性设计:legend、label 与屏幕阅读器文本

该组件在可访问性上做了完整设计,这是它区别于"自己手写切换逻辑"的关键价值:

  • 最外层使用<fieldset>+<legend>语义化分组(class 为block-editor-responsive-block-control),title作为 legend 文本,辅助技术可将其识别为具名分组;
  • 切换开关使用ToggleControl,其help文本为"Choose whether to use the same value for all screen sizes or a unique value for each screen size."(源码 index.jsx);
  • 每个控件都会收到一个由 label.jsx 渲染的ResponsiveBlockControlLabel:可见文本为视口标签(如 "All"),同时通过VisuallyHidden附带完整的描述文本——形如 "Controls the padding property for All viewports.",并用aria-describedby建立关联(id 通过useInstanceId生成,保证唯一性)。

从测试断言中可以看到这一可访问性文本的完整形态:screen.getByRole( 'combobox', { name: 'All Controls the padding property for All viewports.' } )——即"视口标签 + 隐藏描述"拼接后的名称。这意味着你的控件只要把labelComponent用作 label 或传给组件的 label 属性,即可免费获得无障碍命名。

样式方面,style.scss 定义了分组底部分隔线、标题间距等视觉细节,并将切换开关的 help 文本通过screen-reader-text混入隐藏(仅对屏幕阅读器可见),保证界面整洁而不牺牲可访问性。

状态管理与持久化:受控组件的正确姿势

ResponsiveBlockControl严格受控的:

  • 模式状态(isResponsive)由你维护,通过onIsResponsiveChange反向更新;
  • 各控件的值由你的控件自行维护与持久化(通常写入区块 attributes),组件本身不持有任何值状态。

因此,一个完整的落地实现通常形如:

edit( { attributes, setAttributes } ) { const [ isResponsive, setIsResponsive ] = useState( false ); const { padding } = attributes; const renderPaddingControl = ( labelComponent, viewport ) => { const propKey = viewport.id === 'all' ? 'padding' : `padding_${ viewport.id }`; return ( <RangeControl label={ labelComponent } value={ attributes[ propKey ] } onChange={ ( value ) => setAttributes( { [ propKey ]: value } ) } /> ); }; return ( <InspectorControls> <ResponsiveBlockControl title="区块内边距" property="padding" renderDefaultControl={ renderPaddingControl } isResponsive={ isResponsive } onIsResponsiveChange={ setIsResponsive } /> </InspectorControls> ); }

其中viewport.id === 'all'对应默认档(即defaultLabel.id),其余 id 对应各响应式视口;渲染区块时再根据当前媒体查询选择对应值(响应式 CSS 变量或 JS 媒体查询皆可)。这样既复用了组件的界面与切换逻辑,又保持了数据层完全透明可控。

测试验证:行为与边界条件的保证

组件仓库附带的单元测试 test/index.jsdom.test.jsx 覆盖了以下关键行为,可以作为你理解和使用该组件的"行为契约":

  • 必填 Props 守卫:缺少titlepropertyrenderDefaultControl任一必填项时,组件返回null(渲染为空),与源码if ( ! title || ! property || ! renderDefaultControl ) return null;(index.jsx)一致;
  • 默认模式渲染:默认渲染 "All" 控件、勾选状态的切换开关,且响应式控件组不存在;
  • 响应式模式渲染isResponsivetrue时渲染响应式控件组、隐藏默认控件;
  • 自定义视口:传入 4 个自定义视口时,renderDefaultControl恰好被调用 5 次,且每个视口都得到正确命名的可访问 label;
  • 模式切换交互:通过userEvent模拟点击切换开关,能在默认/响应式两种模式间往返切换(对应 index.jsx 中的渲染分支);
  • 自定义响应式渲染:提供renderResponsiveControls时,响应式模式渲染 3 个自定义控件,且renderDefaultControl仍被调用 1 次(仅为默认模式准备)。

这些测试同时验证了组件与SelectControlRangeControl@wordpress/components控件的组合可行性,你可以放心将其与任意自己的控件搭配使用。

适用场景与注意事项

适合使用 ResponsiveBlockControl 的场景:

  • 区块需要为 padding、margin、字号、间距等布局属性提供按屏幕尺寸区分的设置;
  • 希望复用 Gutenberg 标准的"同一值用于所有屏幕 vs 各屏幕单独设置"交互范式,保持 UI 一致性;
  • 需要开箱即用的 fieldset/legend 分组与屏幕阅读器标签,降低无障碍实现成本。

注意事项:

  • 组件 API 带__experimental前缀,属于实验性接口,升级 Gutenberg 时需关注变更;
  • 组件不持久化任何值,所有状态(模式 + 控件值)都由外部维护,务必在renderDefaultControl中处理好每个视口的读写;
  • 默认只提供 small/medium/large 三档视口,若产品需要 tablet 等更多断点,请通过viewports自定义;
  • "响应式"在这里指的是"按视口分别取值"的设置界面,实际生效仍需在渲染侧结合媒体查询/CSS 变量输出对应的值。

相关资源:组件实现 index.jsx、标签组件 label.jsx、样式 style.scss、单元测试 test/index.jsdom.test.jsx,以及官方文档 README.md。

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 3:49:03

Spring三级缓存机制:循环依赖与AOP代理全解析

最近有个同事跑来问我&#xff0c;说在智谱清言里搜“三级缓存具体是什么”&#xff0c;AI给讲了一堆Spring源码。他看完还是懵的&#xff0c;就跑来让我用人话再讲一遍。这个题目确实经典&#xff0c;面试问烂了&#xff0c;网上文章也一大把&#xff0c;但能把“为什么非得是…

作者头像 李华
网站建设 2026/9/17 3:48:53

grub> 命令行救援指南:Linux 引导故障修复与预防

开机之后没看到熟悉的桌面或者登录界面&#xff0c;屏幕上顶着一行grub>或者grub rescue>&#xff0c;下面还跟着一句minimal bash-like line editing is supported。第一次遇到的人十有八九会慌&#xff0c;以为系统挂了&#xff0c;其实大部分情况下数据都还在&#xf…

作者头像 李华
网站建设 2026/9/17 3:47:03

GESP三级真题解析:平衡序列如何用前缀和与哈希表从O(n²)优化到O(n)

GESP 2024年9月的三级认证里&#xff0c;第三部分编程题第一题叫"平衡序列"。这道题和前面几道模拟题画风不太一样&#xff0c;它更像一道纯粹的算法题——如果你只会老老实实把每个区间都试一遍&#xff0c;大概率只能过掉前几个小数据点&#xff0c;后面全超时。我…

作者头像 李华