Gutenberg 中 `core/blocks` 数据存储详解:块类型注册表的 Selector 与 Action 全参考

Gutenberg 中 `core/blocks` 数据存储详解:块类型注册表的 Selector 与 Action 全参考 Gutenberg 中core/blocks数据存储详解块类型注册表的 Selector 与 Action 全参考【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenbergcore/blocks是 GutenbergWordPress 区块编辑器项目中存放全部区块类型元数据的 Redux 数据仓库注册好的块类型、块样式、变体Variations、分类与集合、各类回退块名都在此集中管理。本文基于仓库文档 Block Types Data 及其对应的源码实现 selectors.ts、actions.ts、reducer.ts逐一讲解每个 Selector 的用法、参数与返回值并结合 reducer 与工具函数剖析其底层实现帮助你在开发插件、自定义块插入器或调试编辑器行为时正确使用该数据仓库。存储命名空间与初始化该数据仓库的命名空间为core/blocks。在 constants.ts 中定义export const STORE_NAME core/blocks;在 store/index.ts 中仓库通过wordpress/data的createReduxStore创建并注册export const store createReduxStore( STORE_NAME, { reducer, selectors, actions, } ); register( store ); unlock( store ).registerPrivateSelectors( privateSelectors ); unlock( store ).registerPrivateActions( privateActions );值得注意的两个细节仓库通过unlock( store )机制额外注册了一组私有 Selector / Action见 private-selectors.ts它们不属于公开 API不应在插件中使用从源码结构看公开导出入口 packages/blocks/src/index.ts 直接export { store } from ./store因此import { store as blocksStore } from wordpress/blocks是访问该仓库的标准方式。状态结构BlockStoreState文档中所有 Selector 的第一个参数都是state: BlockStoreState。该接口的完整定义见 types.ts各字段含义如下字段类型含义bootstrappedBlockTypesRecordstring, PartialBlockType由服务端定义引导bootstrap进来的块类型unprocessedBlockTypesRecordstring, PartialBlockType已注册但尚未应用过滤器的块类型供按需重算blockTypesRecordstring, BlockType按名称索引的完整块类型blockStylesRecordstring, BlockStyle[]按块名索引的块样式blockVariationsRecordstring, BlockVariation[]按块名索引的块变体defaultBlockNamestring \| null默认块名称编辑器新建块时使用freeformFallbackBlockNamestring \| null处理非区块内容自由格式内容的块名unregisteredFallbackBlockNamestring \| null处理未注册块的回退块名groupingBlockNamestring \| null用于“分组/取消分组”操作的块名categoriesBlockCategory[]可用的块分类列表collectionsRecordstring, BlockCollection可用的块集合插入器中的命名空间分组blockBindingsSourcesRecordstring, OmitBlockBindingsSource, name按名称索引的绑定源这些字段与 reducer.ts 末尾combineReducers合并出的各子 reducer 一一对应理解状态结构是正确解读下文各 Selector 返回值的基础。Selector 参考以下 Selector 全部由 selectors.ts 自动生成导出可通过select( blocksStore )调用或在 React 组件中用useSelect订阅。按功能分为四类。块类型查询getBlockTypes返回所有可用的块类型。import { store as blocksStore } from wordpress/blocks; import { useSelect } from wordpress/data; const ExampleComponent () { const blockTypes useSelect( ( select ) select( blocksStore ).getBlockTypes(), [] ); return ( ul { blockTypes.map( ( block ) ( li key{ block.name }{ block.title }/li ) ) } /ul ); };参数stateBlockStoreState数据状态。返回值BlockType[]块类型数组。源码实现selectors.ts使用createSelector包裹Object.values( state.blockTypes )依赖项仅为state.blockTypes因此只有块类型集合变化时才会触发重新计算与组件重渲染export const getBlockTypes createSelector( ( state: BlockStoreState ): BlockType[] Object.values( state.blockTypes ), ( state: BlockStoreState ) [ state.blockTypes ] );getBlockType按名称返回单个块类型。import { store as blocksStore } from wordpress/blocks; import { useSelect } from wordpress/data; const ExampleComponent () { const paragraphBlock useSelect( ( select ) select( blocksStore ).getBlockType( core/paragraph ), [] ); return ( ul { paragraphBlock Object.entries( paragraphBlock.supports ).map( ( blockSupportsEntry ) { const [ propertyName, value ] blockSupportsEntry; return ( li key{ propertyName } { ${ propertyName } : ${ value } }/li ); } ) } /ul ); };参数stateBlockStoreState数据状态。namestring块类型名称。返回值BlockType | undefined块类型对象。实现极简selectors.ts直接按名称查表export function getBlockType( state: BlockStoreState, name: string ): BlockType | undefined { return state.blockTypes[ name ]; }样式与变体getBlockStyles按块名返回块样式列表。import { store as blocksStore } from wordpress/blocks; import { useSelect } from wordpress/data; const ExampleComponent () { const buttonBlockStyles useSelect( ( select ) select( blocksStore ).getBlockStyles( core/button ), [] ); return ( ul { buttonBlockStyles buttonBlockStyles.map( ( style ) ( li key{ style.name }{ style.label }/li ) ) } /ul ); };参数stateBlockStoreState数据状态。namestring块类型名称。返回值BlockStyle[]块样式数组。getBlockVariations按块名返回块变体支持按作用域scope过滤。import { store as blocksStore } from wordpress/blocks; import { useSelect } from wordpress/data; const ExampleComponent () { const socialLinkVariations useSelect( ( select ) select( blocksStore ).getBlockVariations( core/social-link ), [] ); return ( ul { socialLinkVariations socialLinkVariations.map( ( variation ) ( li key{ variation.name }{ variation.title }/li ) ) } /ul ); };参数stateBlockStoreState数据状态。blockNamestring块类型名称。scopeBlockVariationScope变体作用域名称block、inserter或transform。返回值BlockVariation[] | undefined块变体数组。从源码结构看selectors.ts当传入scope时会过滤出variation.scope包含该作用域的变体出于向后兼容未显式设置scope的变体默认被视为同时属于block和inserter两个作用域return ( variation.scope || [ block, inserter ] ).includes( scope );getActiveBlockVariation基于块的属性返回当前激活的变体。变体由其isActive属性判定而isActive可以是块属性键数组或函数属性键数组将块的attributes与变体声明的属性用严格相等深比较方式逐一比对函数接收“块属性”与“变体属性”两个参数自行判定该变体是否激活。import { __ } from wordpress/i18n; import { store as blocksStore } from wordpress/blocks; import { store as blockEditorStore } from wordpress/block-editor; import { useSelect } from wordpress/data; const ExampleComponent () { // This example assumes that a core/embed block is the first block in the Block Editor. const activeBlockVariation useSelect( ( select ) { // Retrieve the list of blocks. const [ firstBlock ] select( blockEditorStore ).getBlocks(); // Return the active block variation for the first block. return select( blocksStore ).getActiveBlockVariation( firstBlock.name, firstBlock.attributes ); }, [] ); return activeBlockVariation activeBlockVariation.name spotify ? ( p{ __( Spotify variation ) }/p ) : ( p{ __( Other variation ) }/p ); };参数stateBlockStoreState数据状态。blockNamestring块名称例如core/columns。attributesRecord string, unknown 用于判定激活变体的块属性。scopeBlockVariationScope变体作用域名称。innerContentArray string | null 块内部内容用于判定那些声明了静态内部内容的块如 Custom HTML 块的激活变体。返回值BlockVariation | undefined当前激活的变体。这个 Selector 的匹配算法比文档描述更丰富值得结合 selectors.ts 的实现深入理解其判定顺序为静态内部内容匹配优先若传入了innerContent先寻找variation.innerContent与之“深度相等”使用fastDeepEqual的变体并立即返回。由于内部内容保存的是静态片段与null插槽标记而非内部块本身编辑内部块不会破坏匹配但修改静态标记会破坏匹配——这正是 Custom HTML 等块的变体识别机制。属性键数组匹配含嵌套路径与优先级对isActive为数组的变体源码先过滤出“顶层属性名存在于块类型属性表”的键支持layout.type这类嵌套路径用点号分割后检查第一段随后用工具函数getValueFromObjectPath取值、matchesAttributes递归深比较对RichTextData类型的块属性会先转为 HTML 字符串再比较。多个变体同时命中时命中属性数量更多的变体胜出maxMatchedAttributes即变体优先级/特异性机制。isActive函数匹配若函数返回真由于无法衡量其匹配了多少属性源码会直接返回“此前找到的最佳匹配或该变体”return match || variation。默认变体回退若以上均未命中且scope为block或transform不包括inserter以免影响插入器中的块名称显示回退到“isDefault为真且未声明isActive”的变体。其中getValueFromObjectPath与matchesAttributes两个工具函数定义在 utils.ts前者支持点分字符串或数组形式的路径取值并支持默认值后者对两个对象递归检查“变体属性是否为块属性的子集”最终值用严格相等判定。getDefaultBlockVariation返回给定块类型的默认变体。当有多个变体被标注为默认时取最后注册的那一个——这简化了注册覆盖override的场景若没有任何默认变体则返回数组第一项。import { __, sprintf } from wordpress/i18n; import { store as blocksStore } from wordpress/blocks; import { useSelect } from wordpress/data; const ExampleComponent () { const defaultEmbedBlockVariation useSelect( ( select ) select( blocksStore ).getDefaultBlockVariation( core/embed ), [] ); return ( defaultEmbedBlockVariation ( p { sprintf( __( core/embed default variation: %s ), defaultEmbedBlockVariation.title ) } /p ) ); };参数stateBlockStoreState数据状态。blockNamestring块类型名称。scopeBlockVariationScope变体作用域名称。返回值BlockVariation | undefined默认变体。“取最后一个默认项”的实现即先反转数组再查找第一个isDefaultselectors.tsconst defaultVariation [ ...( variations || [] ) ] .reverse() .find( ( { isDefault } ) !! isDefault ); return defaultVariation || variations?.[ 0 ];分类、集合与回退块名getCategories返回所有可用的块分类。import { store as blocksStore } from wordpress/blocks; import { useSelect } from wordpress/data; const ExampleComponent () { const blockCategories useSelect( ( select ) select( blocksStore ).getCategories(), [] ); return ( ul { blockCategories.map( ( category ) ( li key{ category.slug }{ category.title }/li ) ) } /ul ); };参数stateBlockStoreState数据状态。返回值BlockCategory[]分类列表。从源码结构看若没有任何插件调用setCategoriesreducer 会返回内置的七类默认分类reducer.tsexport const DEFAULT_CATEGORIES: BlockCategory[] [ { slug: text, title: __( Text ) }, { slug: media, title: __( Media ) }, { slug: design, title: __( Design ) }, { slug: widgets, title: __( Widgets ) }, { slug: theme, title: __( Theme ) }, { slug: embed, title: __( Embeds ) }, { slug: reusable, title: __( Reusable blocks ) }, ];getCollections返回所有可用的块集合插入器中按命名空间展示的分组。import { store as blocksStore } from wordpress/blocks; import { useSelect } from wordpress/data; const ExampleComponent () { const blockCollections useSelect( ( select ) select( blocksStore ).getCollections(), [] ); return ( ul { Object.values( blockCollections ).length 0 Object.values( blockCollections ).map( ( collection ) ( li key{ collection.title }{ collection.title }/li ) ) } /ul ); };参数stateBlockStoreState数据状态。返回值Record string, BlockCollection 以命名空间为键的集合记录。getDefaultBlockName返回默认块名称点击插入器“”时创建的第一个块。import { __, sprintf } from wordpress/i18n; import { store as blocksStore } from wordpress/blocks; import { useSelect } from wordpress/data; const ExampleComponent () { const defaultBlockName useSelect( ( select ) select( blocksStore ).getDefaultBlockName(), [] ); return ( defaultBlockName ( p { sprintf( __( Default block name: %s ), defaultBlockName ) } /p ) ); };参数stateBlockStoreState数据状态。返回值string | null默认块名称。getFreeformFallbackBlockName返回处理“非区块内容”freeform 段落的块名称。import { __, sprintf } from wordpress/i18n; import { store as blocksStore } from wordpress/blocks; import { useSelect } from wordpress/data; const ExampleComponent () { const freeformFallbackBlockName useSelect( ( select ) select( blocksStore ).getFreeformFallbackBlockName(), [] ); return ( freeformFallbackBlockName ( p { sprintf( __( Freeform fallback block name: %s ), freeformFallbackBlockName ) } /p ) ); };参数stateBlockStoreState数据状态。返回值string | null处理非区块内容的块名称。getGroupingBlockName返回用于“块分组”操作的块名称如“组合/取消组合”交互。import { __, sprintf } from wordpress/i18n; import { store as blocksStore } from wordpress/blocks; import { useSelect } from wordpress/data; const ExampleComponent () { const groupingBlockName useSelect( ( select ) select( blocksStore ).getGroupingBlockName(), [] ); return ( groupingBlockName ( p { sprintf( __( Default grouping block name: %s ), groupingBlockName ) } /p ) ); };参数stateBlockStoreState数据状态。返回值string | null处理块分组的块名称。getUnregisteredFallbackBlockName返回处理“未注册块”解析出但当前未注册的块名的回退块名称。import { __, sprintf } from wordpress/i18n; import { store as blocksStore } from wordpress/blocks; import { useSelect } from wordpress/data; const ExampleComponent () { const unregisteredFallbackBlockName useSelect( ( select ) select( blocksStore ).getUnregisteredFallbackBlockName(), [] ); return ( unregisteredFallbackBlockName ( p { sprintf( __( Unregistered fallback block name: %s ), unregisteredFallbackBlockName ) } /p ) ); };参数stateBlockStoreState数据状态。返回值string | null处理未注册块的块名称。这四个“块名设置”Selector 在 reducer 中由同一个高阶 reducercreateBlockNameSetterReducer派生reducer.ts。有一个容易忽略的细节当被设置的块名本身被REMOVE_BLOCK_TYPES移除时对应状态会自动重置为null避免编辑器继续引用一个已注销的块名。Block Supports 特性查询getBlockSupport返回某特性block support在块类型上的取值若已定义。import { __, sprintf } from wordpress/i18n; import { store as blocksStore } from wordpress/blocks; import { useSelect } from wordpress/data; const ExampleComponent () { const paragraphBlockSupportValue useSelect( ( select ) select( blocksStore ).getBlockSupport( core/paragraph, anchor ), [] ); return ( p { sprintf( __( core/paragraph supports.anchor value: %s ), paragraphBlockSupportValue ) } /p ); };参数stateBlockStoreState数据状态。nameOrTypestring | BlockType块名称或类型对象。featurestring | string[]要查询的特性支持spacing或[ typography, fontSize ]这类路径形式。defaultSupportsunknown未显式定义时返回的默认值。返回值unknownblock support 取值。实现上selectors.tsnameOrType先经getNormalizedBlockType归一化为BlockType对象字符串则查表再用getValueFromObjectPath从blockType.supports上按特性路径取值取不到时返回defaultSupports。这也意味着该 Selector 天然支持嵌套 support如anchor、html等扁平键之外的深层结构。hasBlockSupport若块定义了某特性的支持则返回true否则返回false。import { __, sprintf } from wordpress/i18n; import { store as blocksStore } from wordpress/blocks; import { useSelect } from wordpress/data; const ExampleComponent () { const paragraphBlockSupportClassName useSelect( ( select ) select( blocksStore ).hasBlockSupport( core/paragraph, className ), [] ); return ( p { sprintf( __( core/paragraph supports custom class name?: %s ), paragraphBlockSupportClassName ) } /p ); };参数stateBlockStoreState数据状态。nameOrTypestring | BlockType块名称或类型对象。featurestring | string[]要测试的特性。defaultSupportsunknown特性未显式定义时是否默认支持。返回值boolean块是否支持该特性。实现就是对getBlockSupport的结果做布尔化selectors.tsdefaultSupports在这里的含义因此变为“未定义时按支持还是按不支持处理”export function hasBlockSupport( state: BlockStoreState, nameOrType: string | BlockType, feature: string | string[], defaultSupports?: unknown ): boolean { return !! getBlockSupport( state, nameOrType, feature, defaultSupports ); }子块与搜索getChildBlockNames返回给定块的子块名称数组。import { store as blocksStore } from wordpress/blocks; import { useSelect } from wordpress/data; const ExampleComponent () { const childBlockNames useSelect( ( select ) select( blocksStore ).getChildBlockNames( core/navigation ), [] ); return ( ul { childBlockNames childBlockNames.map( ( child ) ( li key{ child }{ child }/li ) ) } /ul ); };参数stateBlockStoreState数据状态。blockNamestring块类型名称。返回值string[]子块名称数组。实现selectors.ts是遍历所有块类型筛出parent数组包含目标块名的类型再映射为名称。子块关系完全由块类型的parent字段声明驱动export const getChildBlockNames createSelector( ( state: BlockStoreState, blockName: string ): string[] { return getBlockTypes( state ) .filter( ( blockType ) { return blockType.parent?.includes( blockName ); } ) .map( ( { name } ) name ); }, ( state: BlockStoreState ) [ state.blockTypes ] );hasChildBlocks返回布尔值指示块是否有子块。import { __, sprintf } from wordpress/i18n; import { store as blocksStore } from wordpress/blocks; import { useSelect } from wordpress/data; const ExampleComponent () { const navigationBlockHasChildBlocks useSelect( ( select ) select( blocksStore ).hasChildBlocks( core/navigation ), [] ); return ( p { sprintf( __( core/navigation has child blocks: %s ), navigationBlockHasChildBlocks ) } /p ); };参数stateBlockStoreState数据状态。blockNamestring块类型名称。返回值boolean含子块为true否则为false。实现即getChildBlockNames( state, blockName ).length 0selectors.ts。hasChildBlocksWithInserterSupport返回布尔值指示块是否至少有一个“支持插入器”的子块。import { __, sprintf } from wordpress/i18n; import { store as blocksStore } from wordpress/blocks; import { useSelect } from wordpress/data; const ExampleComponent () { const navigationBlockHasChildBlocksWithInserterSupport useSelect( ( select ) select( blocksStore ).hasChildBlocksWithInserterSupport( core/navigation ), [] ); return ( p { sprintf( __( core/navigation has child blocks with inserter support: %s ), navigationBlockHasChildBlocksWithInserterSupport ) } /p ); };参数stateBlockStoreState数据状态。blockNamestring块类型名称。返回值boolean含至少一个支持插入器的子块为true否则为false。实现中selectors.ts对每个子块调用hasBlockSupport( state, childBlockName, inserter, true )——注意第四个参数传true即“未显式定义insertersupport 的子块默认视为支持插入器”只有显式设置inserter: false才会被排除。isMatchingSearchTerm判断按名称或对象值给定的块类型是否匹配搜索词。import { __, sprintf } from wordpress/i18n; import { store as blocksStore } from wordpress/blocks; import { useSelect } from wordpress/data; const ExampleComponent () { const termFound useSelect( ( select ) select( blocksStore ).isMatchingSearchTerm( core/navigation, theme ), [] ); return ( p { sprintf( __( Search term was found in the title, keywords, category or description in block.json: %s ), termFound ) } /p ); };参数stateBlockStoreState块状态。nameOrTypestring | BlockType块名称或类型对象。searchTermstring用于过滤的搜索词。返回值boolean块类型是否匹配搜索词。匹配范围与归一化规则可结合源码确认selectors.ts搜索目标为title、keywords、category以及字符串形式的description即 block.json 中的对应字段两侧文本都会先经过getNormalizedSearchTerm处理——去除音标符号remove-accents、转小写并去除首尾空白因此Thème与theme可以互相命中。ActionreapplyBlockTypeFilters文档特别指出core/blocks包中的 action不应被直接使用而应改用公开 API见 packages/blocks/README.md 中列出的函数如registerBlockType、registerBlockStyle、registerBlockVariation等。它们只是公开函数内部 dispatch 的信号载体。文档中唯一公开记录的 action 是reapplyBlockTypeFilters发出信号指示应重新计算所有块类型。它使用存储的“未处理”块类型与最新的已注册过滤器列表。它解决的是“第三方块过滤器在第三方块之后才注册”的问题。示例时序1. Filter A2. Block B3. Block C4. Filter D5. Filter E6. Block F7. Filter G。在这种场景下部分过滤器会因注册过晚而无法应用于所有块。其实现是一个 thunkactions.tsexport function reapplyBlockTypeFilters() { return ( { dispatch, select }: BlocksStoreThunkArgs ) { const processedBlockTypes: BlockType[] []; for ( const [ name, settings ] of Object.entries( select.getUnprocessedBlockTypes() ) ) { const result dispatch( processBlockType( name, settings ) ); if ( result ) { processedBlockTypes.push( result ); } } if ( ! processedBlockTypes.length ) { return; } dispatch.addBlockTypes( processedBlockTypes ); }; }配合 reducer 可以还原完整的“两段式注册”数据流reducer.ts块注册时原始设置被存入unprocessedBlockTypesADD_UNPROCESSED_BLOCK_TYPE不做任何变换reapplyBlockTypeFilters遍历这份原始副本通过processBlockType依次应用当前所有已注册的blockTypes过滤器产出最终的BlockType结果一次性经addBlockTypesADD_BLOCK_TYPES写入blockTypes同时blockStyles、blockVariations两个子 reducer 会在同一次 action 中联动更新。这也解释了 types.ts 中unprocessedBlockTypes的注释“Unprocessed block types pending filter application”——它是过滤器重放的输入源。其余 action 与公开函数的对应关系从源码结构看actions.ts 中还有多个标注ignore即不进入自动生成文档的 action 创建函数其 JSDoc 均指明了对应的推荐公开入口action内部信号推荐的公开 APIaddBlockTypesregisterBlockTyperemoveBlockTypesunregisterBlockTypeaddBlockStylesregisterBlockStyleremoveBlockStylesunregisterBlockStyleaddBlockVariationsregisterBlockVariationremoveBlockVariationsunregisterBlockVariationsetDefaultBlockNamesetDefaultBlockNamesetFreeformFallbackBlockNamesetFreeformContentHandlerNamesetUnregisteredFallbackBlockNamesetUnregisteredTypeHandlerNamesetGroupingBlockNamesetGroupingBlockNamesetCategories/updateCategorysetCategories/updateCategoryaddBlockCollection/removeBlockCollectionregisterBlockCollection/unregisterBlockCollection此外__experimentalReapplyBlockFilters已被废弃自 6.4 版本起替代方案为reapplyBlockFiltersactions.ts。状态更新的关键行为阅读 reducer.ts 中各子 reducer可以确认几个影响插件开发行为的具体规则块类型按名称去重合并blockTypesreducer 在ADD_BLOCK_TYPES时用keyBlockTypesByName把数组转为“名称 → 类型”映射后展开合并同名块后注册者覆盖先注册者reducer.ts。样式与变体区分来源blockStyles与blockVariationsreducer 在ADD_BLOCK_TYPES时会把新注册块自带的styles/variations打上source: block标记并保留旧状态中非block来源的条目ADD_BLOCK_STYLES/ADD_BLOCK_VARIATIONS则用getUniqueItemsByName按名称去重后追加。这意味着块重新注册不会抹掉第三方单独注册的样式与变体。分类按 slug 唯一化SET_CATEGORIES通过Map slug, category 去重reducer.tsUPDATE_CATEGORY则按 slug 合并部分属性。服务端引导不覆盖本地bootstrappedBlockTypesreducer 在收到ADD_BOOTSTRAPPED_BLOCK_TYPE时若该名称已有定义则直接跳过以保护从服务端初始化的元数据reducer.ts同时服务端下发的 snake_case 字段会被camelCase转换为与前端一致的属性命名。测试覆盖该数据仓库的行为有独立的单元测试支撑selectors.jsdom.test.jsx验证各 Selector 的返回值包括变体作用域过滤、support 取值、搜索词匹配等reducer.js 与 actions.js验证状态合并、去重与reapplyBlockTypeFilters的重放逻辑。在修改块注册逻辑或依赖core/blocks数据的插件中这些测试文件是确认“源码行为与本文描述一致”的最直接依据。小结core/blocks数据仓库是 Gutenberg 块注册体系的“只读事实层”所有register*/unregister*公开 API 最终都通过 action 信号写入它而插入器、块列表、编辑器 UI 则通过本文列举的 Selector 读取它。掌握两点即可安全使用读取数据一律经select( blocksStore )或useSelect调用文档中的 Selector并留意createSelector包裹的 SelectorgetBlockTypes、getBlockVariations、getChildBlockNames具备基于状态依赖的缓存能力写入数据不要直接 dispatch 内部 action而应使用公开 API只有处理“过滤器晚于块注册”的时序问题时才需要reapplyBlockTypeFilters。【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考