尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

深入理解 Gutenberg 的 RecursionProvider 与 useHasRecursion:在 WordPress 块编辑器中防御递归渲染

深入理解 Gutenberg 的 RecursionProvider 与 useHasRecursion:在 WordPress 块编辑器中防御递归渲染 深入理解 Gutenberg 的 RecursionProvider 与 useHasRecursion在 WordPress 块编辑器中防御递归渲染【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg导读在 GutenbergWordPress 块编辑器的渲染架构中任何可能自我递归的块类型都必须自行处理无限循环问题。RecursionProvider组件与useHasRecursion()Hook 正是官方为此提供的客户端检测机制用于判断某个块实例是否已被渲染过从而在编辑器中优雅地拦截块嵌套自身这类场景例如同步/可复用块、模板部件互相引用。读完本文你将掌握这两个 API 的完整用法、Props 语义、底层实现原理以及它们在核心块源码中的真实应用方式。设计背景为什么需要客户端递归检测根据 Gutenberg 的块渲染架构约定见 recursion-provider/README.md任何具备递归能力的块类型都应当负责处理自身的无限循环。服务端渲染端如core/block、core/template-part这类块各有自己的递归防护策略而客户端编辑器画布内同样存在无限渲染的风险——比如一个同步块可复用块内部又引用了它自身或两个模板部件互相嵌套引用。为了在客户端检测这类无限循环RecursionProvider组件和useHasRecursion()Hook 被引入wordpress/block-editor。其核心思路是在块的Edit函数中先调用useHasRecursion( uniqueId )查询当前块是否已经在渲染链中出现过若已出现则直接渲染警告占位如Warning组件提示 Block cannot be rendered inside itself.终止递归若未出现则用RecursionProvider uniqueId{ uniqueId }包裹块的真实编辑内容把该唯一标识登记进上下文中供其后代块查询。完整用法示例以下代码摘自 recursion-provider/README.md展示了如何在一个可能递归的自定义块中同时使用useHasRecursion与RecursionProviderimport { RecursionProvider, useHasRecursion, useBlockProps, Warning, } from wordpress/block-editor; import { __ } from wordpress/i18n; export default function MyRecursiveBlockEdit( { attributes: { ref } } ) { const hasAlreadyRendered useHasRecursion( ref ); const blockProps useBlockProps( { className: my-block__custom-class, } ); if ( hasAlreadyRendered ) { return ( div { ...blockProps } Warning { __( Block cannot be rendered inside itself. ) } /Warning /div ); } return ( RecursionProvider uniqueId{ ref } Block editing code here.... /RecursionProvider ); }使用要点同一个uniqueId必须在 Hook 与 Provider 中保持一致useHasRecursion负责查RecursionProvider负责登记两者使用相同的uniqueId值才能形成闭环。在Edit函数的任何副作用之前调用从核心块源码见下文ReusableBlockEditRecursionWrapper可以推断递归检测应在块编辑逻辑真正执行前完成短路以避免多余的网络请求或数据加载。返回内容不必局限于Warning检测到递归时你可以渲染任意占位 UI警告、空占位等但官方Warning组件提供了标准化的错误提示外观。Props 详解RecursionProvider的 PropsProp类型必填默认值说明uniqueIdany是—充当块实例唯一标识符的任意值通常来自块属性如ref、templatePartIdchildrenElement是—作为内容渲染的子组件blockNameString否可选的块名未提供时自动取useBlockEditContext()中的块名useHasRecursion()的参数Prop类型必填默认值说明uniqueIdany是—充当块实例唯一标识符的任意值blockNameString否可选的块名未提供时自动取useBlockEditContext()中的块名返回值为boolean表示该唯一标识是否已被渲染过。源码级实现原理组件实现位于 recursion-provider/index.jsx。整个机制建立在 React Context 之上1. 共享上下文RenderedRefsContextconst RenderedRefsContext createContext( {} ); RenderedRefsContext.displayName RenderedRefsContext;上下文的值是一个按块名分组、值为Set存放已渲染的uniqueId的对象。默认值为空对象{}。2. 不可变登记addToBlockTypefunction addToBlockType( renderedBlocks, blockName, uniqueId ) { const result { ...renderedBlocks, [ blockName ]: renderedBlocks[ blockName ] ? new Set( renderedBlocks[ blockName ] ) : new Set(), }; result[ blockName ].add( uniqueId ); return result; }注意其不可变性复制外层对象、为对应块名创建新Set再add绝不原地修改祖先 Context 中的集合。这是为了保证 React Context 值变化能被正确触发重新渲染。3.RecursionProvider登记唯一标识export function RecursionProvider( { children, uniqueId, blockName } ) { const previouslyRenderedBlocks useContext( RenderedRefsContext ); const { name } useBlockEditContext(); blockName blockName || name; const newRenderedBlocks useMemo( () addToBlockType( previouslyRenderedBlocks, blockName, uniqueId ), [ previouslyRenderedBlocks, blockName, uniqueId ] ); return ( RenderedRefsContext.Provider value{ newRenderedBlocks } { children } /RenderedRefsContext.Provider ); }关键点块名自动推断blockName未显式传入时从useBlockEditContext()读取当前块的name该 Context 定义见 block-edit/context.js默认{ name: , isSelected: false }。useMemo优化仅当祖先登记集合、块名或uniqueId变化时才生成新值避免无谓的重渲染。4.useHasRecursion查询唯一标识export function useHasRecursion( uniqueId, blockName ) { const previouslyRenderedBlocks useContext( RenderedRefsContext ); const { name } useBlockEditContext(); blockName blockName || name; return Boolean( previouslyRenderedBlocks[ blockName ]?.has( uniqueId ) ); }查询逻辑同样按块名 唯一标识双重维度previouslyRenderedBlocks[ blockName ]?.has( uniqueId )结果用Boolean()规范化为布尔值。5. 语义要点按块类型隔离从实现可以清晰看到递归判定是按块类型blockName隔离的。两个不同块类型即使共享同一个uniqueId值也不会互相判定为递归——这与测试用例ANOTHER-BLOCK-SAME-ID的预期完全一致见下文。因此uniqueId只需要在同一块类型的作用域内唯一即可。测试用例验证单元测试位于 recursion-provider/test/index.jsdom.test.jsx覆盖了五种关键场景是理解 API 边界行为的最佳参考单个块正常渲染SIMPLE场景下块正常渲染不出现--halted占位。兄弟块允许共存两个拥有相同uniqueId的兄弟块都能正常渲染——因为判定依赖的是祖先链上的登记记录而非全局唯一性。块阻止渲染自身SINGLY-RECURSIVE场景下外层块正常渲染而内层自引用渲染出--halted占位成功拦截自递归。仅同类型才阻止ANOTHER-BLOCK-SAME-ID场景下通过BlockEditContextProvider将内层块的name改为another-block后内层块照常渲染其更深层的自引用才被拦截——证明同类型 同 ID才是判定条件。阻止两个块互递归MUTUALLY-RECURSIVE-1与MUTUALLY-RECURSIVE-2互相引用时两层均正常渲染第三层出现--halted占位成功拦截互递归。测试使用BlockEditContextProvider导出自 block-edit/context.js来模拟块的编辑上下文这也印证了blockName默认取自编辑上下文的行为。核心块中的真实应用该机制并非仅为第三方开发者准备Gutenberg 核心块库中已有多处实战使用可通过搜索useHasRecursion在packages目录中定位同步块可复用块core/blockblock-library/src/block/edit.jsx 中的ReusableBlockEditRecursionWrapper是最典型的应用范式export default function ReusableBlockEditRecursionWrapper( props ) { const { ref } props.attributes; const hasAlreadyRendered useHasRecursion( ref ); if ( hasAlreadyRendered ) { return RecursionWarning /; } return ( RecursionProvider uniqueId{ ref } ReusableBlockEdit { ...props } / /RecursionProvider ); }源码注释特别说明该包装层让递归短路尽早发生先于ReusableBlockEdit中任何其他副作用如useEntityRecord数据请求执行从而避免无谓的网络与渲染开销。模板部件core/template-partblock-library/src/template-part/edit/index.jsx 同样使用useHasRecursion( templatePartId )做检测并在 第 235 行 用RecursionProvider uniqueId{ templatePartId }包裹渲染内容防止模板部件互相引用造成死循环。其他引用场景此外block-library/src/post-content/edit.jsx、block-library/src/navigation/edit/index.jsx 以及 editor/src/components/visual-editor/index.jsx 也都引用了这套机制用于各自场景的递归防护。公开导出与 API 演进这两个 API 通过 block-editor/src/components/index.js 以RecursionProvider、useHasRecursion之名从wordpress/block-editor包对外导出同时保留了兼容别名__experimentalRecursionProvider与__experimentalUseHasRecursion。值得关注的是其去实验化历史在 recursion-provider/index.jsx 中DeprecatedExperimentalRecursionProvider与DeprecatedExperimentalUseHasRecursion两个包装组件在调用时会触发wordpress/deprecated警告标注since: 6.5自 WordPress 6.5 起alternative: wp.blockEditor.RecursionProvider/wp.blockEditor.useHasRecursion也就是说旧代码中使用的__experimental*前缀版本已正式弃用新代码应直接使用无前缀的稳定 API。最佳实践小结一个递归块类型的标准模板Edit函数开头先useHasRecursion( uniqueId )命中则渲染警告占位未命中则用RecursionProvider uniqueId{ uniqueId }包裹编辑内容。uniqueId的选择应选取能唯一标识块实例链的属性值如core/block的ref、core/template-part的templatePartId同一块类型内重复使用同一值才会被拦截。尽早短路如ReusableBlockEditRecursionWrapper所示将递归检测放在最外层包装函数让副作用和数据请求尽可能不执行。不必担心兄弟块判定基于祖先链上的登记集合兄弟块间互不影响同一uniqueId可在不同分支正常重复渲染。保持依赖版本使用稳定 APIRecursionProvider/useHasRecursion避免引用 6.5 起已弃用的__experimental*别名。这套Provider 登记 Hook 查询的组合是 Gutenberg 客户端递归防护的标准答案无论你是为自定义块增加递归能力还是排查同步块、模板部件的循环引用问题RecursionProvider与useHasRecursion都是首先应掌握的防御工具。【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表