尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Strapi Content Manager Layouts:列表视图与编辑视图的布局数据结构及插件 Hook 扩展机制
Strapi Content Manager Layouts列表视图与编辑视图的布局数据结构及插件 Hook 扩展机制【免费下载链接】strapi Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapiLayouts 是 Strapi 内容管理器Content Manager, 简称 CM渲染列表视图List View与编辑视图Edit View的底层驱动结构两个视图本质上都是对一份可迭代的数据结构做三层遍历再由统一的字段渲染组件把每个字段布局映射成真实的表单输入或表格列。本文基于 官方 Layouts 文档 梳理这套机制的核心概念并结合packages/core/content-manager的真实源码讲清楚布局是如何由内容类型的 schema 与配置文件合成的、编辑视图如何被插件 Hook 改写以及插件如何通过cellFormatter向列表表格注入自定义列。读完本文你可以掌握 CM 布局的完整数据链路schema 配置 →EditLayout/ListLayout→ Hook waterfall → 渲染并具备为 CM 扩展视图字段与表格列的能力。什么是 LayoutList 与 Edit 两种同构的数据结构文档的定义非常直接Layout 就是我们遍历它以理解视图如何渲染的数据结构。List View 与 Edit View 的结构在设计上被刻意做成相似的这样可以用同一套遍历逻辑渲染两种视图interface ListLayout { layout: ListFieldLayout[]; components?: never; metadatas: object; settings: object; } interface EditLayout { layout: ArrayArrayEditFieldLayout[]; components: Recordstring, OmitEditLayout, metadatas | components; metadatas: object; settings: object; }两者的关键差异在于layout的嵌套层级与components属性ListLayout.layout是一维数组每个元素对应表格的一列因为列表视图本质上是一张巨大的表格EditLayout.layout是ArrayArrayEditFieldLayout[]三层结构分别对应面板Panel、行Row、字段FieldEditLayout.components是一个以组件 UID 为键的字典每个值本质上与其父结构相同只是按组件做了嵌套与组织。这种自相似self-similar的设计让 CM 可以用完全相同的方式迭代主表单布局和组件内部布局从而递归渲染任意深度的嵌套组件component与动态区块dynamic-zone。在源码中这些类型并非停留在文档层面而是真实定义在 useDocumentLayout.ts 中且比文档版本更具体interface ListFieldLayout extends Table.HeaderDocument, ListFieldLayout, PickFilters.Filter, mainField { attribute: SchemaUtils.Attribute.AnyAttribute | { type: custom }; } interface ListLayout { layout: ListFieldLayout[]; components?: never; metadatas: { [K in keyof Metadatas]: Metadatas[K][list] }; options: LayoutOptions; settings: LayoutSettings; } interface EditLayout { layout: ArrayArrayEditFieldLayout[]; components: { [uid: string]: { layout: ArrayEditFieldLayout[]; settings: ComponentConfiguration[settings] { displayName?: string; icon?: string }; }; }; metadatas: { [K in keyof Metadatas]: Metadatas[K][edit] }; options: LayoutOptions; settings: LayoutSettings; }注意ListFieldLayout.attribute允许{ type: custom }这个特殊值——从源码结构看这就是为非 schema 属性例如 draft publish 的 status 列、插件注入的自定义列预留的类型通道文档中也明确注释了 custom 属性预期使用cellFormatter。Layout 是如何生成的schema 与配置文件的合成文档指出Layout 是内容类型的 schema 与其配置文件configuration file的组合尽管两者的数据结构相似但在根接口层面存在差异。生成过程发生在 useDocumentLayout.ts 的useDocumentLayoutHook 中核心调用链为通过useGetContentTypeConfigurationQuery(model)拉取该模型持久化的配置FindContentTypeConfiguration响应包含contentType.layouts、contentType.metadatas、contentType.settings以及各组件的配置调用normalizeContentManagerLayout(data, { schemas, schema, components })归一化分别执行formatEditLayout(...)与formatListLayout(...)产出两种布局。编辑视图的面板划分规则formatEditLayout 中最值得关注的逻辑是面板panel的切分const panelledEditAttributes convertEditLayoutToFieldLayouts( data.contentType.layouts.edit, schema?.attributes, data.contentType.metadatas, { configurations: data.components, schemas: components }, schemas ).reduceArrayEditFieldLayout[][]((panels, row) { if (row.some((field) field.type dynamiczone)) { panels.push([row]); // dynamic zone 独占一个面板 currentPanelIndex 2; } else { if (!panels[currentPanelIndex]) { panels.push([row]); } else { panels[currentPanelIndex].push(row); } } return panels; }, []);规则非常明确只有 dynamic-zone 会强制开启新面板其余字段行按配置的 panel 归属归入同一面板。组件的布局则在同一个 reduce 中遍历data.components对每个组件 UID 用其自身的layouts.edit调用同样的convertEditLayoutToFieldLayouts并附带组件 schema 的info.icon与info.displayName作为设置——这正呼应了前文components 字典与父结构同构的设计。另外两个值得了解的实现细节默认设置兜底布局未加载完成时Hook 会返回DEFAULT_SETTINGS兜底的空布局其默认值为 useDocumentLayout.tsconst DEFAULT_SETTINGS { bulkable: false, filterable: false, searchable: false, pagination: false, defaultSortBy: , defaultSortOrder: asc, mainField: id, pageSize: 10, relationOpenMode: modal as const, };解析结果缓存为避免每次渲染都重算布局对象导致引用变化该文件维护了一个模块级 LRU 缓存RESOLVED_LAYOUT_CACHE_LIMIT 25见 useDocumentLayout.ts以components/data/model/schema/schemas五个引用相等作为命中条件保持布局对象身份的稳定性。EditView从 EditFieldLayout 到 InputRenderer文档指出编辑视图布局的最底层单元是EditFieldLayout它主要派生自表单组件的InputPropsinterface InputProps { disabled?: boolean; hint?: string; label: string; name: string; placeholder?: string; required?: boolean; type: Exclude Attribute.Kind, media | blocks | richtext | uid | dynamiczone | component | relation ; } interface EditFieldSharedProps extends OmitInputProps, type { mainField?: string; size: number; unique?: boolean; visible?: boolean; } /** * Map over all the types in Attribute Types and use that to create a union of new types where the attribute type * is under the property attribute and the type is under the property type. */ type EditFieldLayout { [K in Attribute.Kind]: EditFieldSharedProps { attribute: ExtractAttribute.Any, { type: K }; type: K; }; }[Attribute.Kind];这里有三个设计要点被排除的 7 种类型是 CM 专属的media、blocks、richtext、uid、dynamiczone、component、relation不会由通用表单输入组件渲染它们由 CM 自己的专用输入接管因此不出现在InputProps.type的联合里。EditFieldLayout用一个映射类型对Attribute.Kind全体做枚举保证每个属性类型都携带对应的attribute原始 schema 与type标识。字段值不放在布局里注意这些输入不会收到value、onChange、errorprops——它们由 React Hook Form 的useField(name)在组件内部取回。通用StringInput的例子源码见 String.tsx印证了这一点export const StringInput forwardRefHTMLInputElement, InputProps( ({ disabled, label, hint, name, placeholder, required }, ref) { const field useField(name); return ( TextInput ref{ref} disabled{disabled} hint{hint} label{label} name{name} defaultValue{field.initialValue} onChange{field.onChange} placeholder{placeholder} required{required} value{field.value} / ); } );size即 12 列网格中的跨列数在 FormLayout.tsx 中普通字段行渲染为 12 列响应式网格每个字段占grid-column: span {size}ResponsiveGridRoot key{gridRowIndex} gap{{ initial: 6, medium: 4 }} {row.map(({ size, ...field }) ( ResponsiveGridItem col{size} key{field.name} s{12} xs{12} ... InputRenderer {...field} label{getLabel(field.name, field.label)} document{document} / /ResponsiveGridItem ))} /ResponsiveGridRoot而含 dynamic-zone 的面板则整块独占一列渲染与formatEditLayout的面板切分规则一一对应。CM 专属的 InputRenderer文档说明CM 的 EditView 域拥有自己的InputRenderer组件历史名称为 helper-plugin 的GenericInputsEditView 只是简单地遍历它的布局把 props 传给InputRenderer遇到 dynamic-zone 或 component 时利用EditLayout的components属性递归渲染布局。真实的 CM 版 InputRenderer 在文档描述的原则之上还承担了大量 CM 特有的职责源码注释给出了准确概括理解完整的EditFieldLayout并处理 RBAC 条件以及 Blocks / Relations 等 CM 专属组件的渲染。具体行为包括权限裁剪通过useDocumentRBAC取canReadFields/canUpdateFields/canCreateFields/canUserAction无读权限的字段直接渲染NotAllowedInput占位无编辑权限或表单禁用时置disabledCustom Fields 优先若attribute带有customField属性则通过useLazyComponents懒加载对应的自定义字段输入CustomInput {...props} {...field} /完全绕开通用输入分发——这就是文档所说额外处理 custom-fields 等属性的实现位置值与状态仍来自useField(props.name)与InputProps设计保持一致组件/动态区块递归遇到component/dynamiczone时借助renderComponentInput回调内部递归渲染MemoizedInputRenderer与useDocumentLayout(currentDocumentMeta.model)取回的edit.components字典继续下钻实现文档强调的以完全相同的方式遍历任意深度的嵌套字段。插件改写编辑布局mutate-edit-view-layout Hook文档开篇即点明编辑视图的布局可以通过插件使用Admin/CM/pages/EditView/mutate-edit-view-layoutHook 进行操纵。源码链路分三步Hook 注册应用启动时在 StrapiApp.tsx 中执行this.createHook(MUTATE_EDIT_VIEW_LAYOUT)Hook 名称常量定义于 constants.tswaterfall 触发点在 useDocumentLayout.ts 中格式化完成的editLayout会被送入 Hook waterfallconst { layout: edit } React.useMemo( () runHookWaterfall(HOOKS.MUTATE_EDIT_VIEW_LAYOUT, { layout: editLayout, query, }), [editLayout, query, runHookWaterfall] );每个挂载该 Hook 的插件按注册顺序接收{ layout, query }并返回改写后的布局还可携带查询参数以区分场景最终返回的edit即被FormLayout消费。CM 侧完整的 Hook 清单集中在 hooks.ts除编辑布局外还包括列表视图列注入、列表过滤器注入以及集合类型/单例类型侧边栏链接的改写export const HOOKS { INJECT_COLUMN_IN_TABLE: Admin/CM/pages/ListView/inject-column-in-table, INJECT_LIST_VIEW_FILTERS: Admin/CM/pages/ListView/inject-in-filters, MUTATE_COLLECTION_TYPES_LINKS: Admin/CM/pages/App/mutate-collection-types-links, MUTATE_EDIT_VIEW_LAYOUT: Admin/CM/pages/EditView/mutate-edit-view-layout, MUTATE_SINGLE_TYPES_LINKS: Admin/CM/pages/App/mutate-single-types-links, };ListViewListFieldLayout 与 cellFormatter 列注入文档指出由于列表视图本质上是一张巨大的表格其数据结构比编辑视图简得多当前阶段插件能做的只有向表格注入列。核心类型如下interface ListFieldLayout { /** * The attribute data from the content-types schema for the field */ attribute: Attribute.Any | { type: custom }; // custom attributes are expected to use cellFormatter. /** * Typically used by plugins to render a custom cell */ cellFormatter?: ( data: { [key: string]: unknown }, header: OmitListFieldLayout, cellFormatter ) React.ReactNode; label: string | MessageDescriptor; /** * the name of the attribute we use to display the actual name e.g. relations * are just ids, so we use the mainField to display something meaningful by * looking at the targets schema */ mainField?: string; name: string; searchable?: boolean; sortable?: boolean; }各字段职责attribute取自内容类型 schema 的属性原始数据{ type: custom }为插件自定义列的占位cellFormatter自定义单元格渲染函数收到整行数据与注入的表头定义返回React.ReactNodemainField用于属性本身只是一个 ID的场景典型如 relation通过查看目标 schema 找到有意义的展示字段searchable/sortable控制该列是否参与全局搜索与排序。文档中convertListLayoutToFieldLayouts的实现印证了默认值searchable: metadata.searchable ?? true、sortable: metadata.sortable ?? true见 useDocumentLayout.ts。列注入 waterfall 与标签翻译列表页的消费点在 ListViewPage.tsxconst tableHeaders React.useMemo(() { const headers runHookWaterfall(INJECT_COLUMN_IN_TABLE, { displayedHeaders, layout: list, }); const formattedHeaders headers.displayedHeaders.mapListFieldLayout((header) { /** * When the header label is a string, it is an attribute on the current content-type: * Use the attribute name value to compute the translation. * Otherwise, it should be a translation object coming from a plugin that injects * into the table (ie i18n, content-releases, review-workflows): * Use the translation object as is. */ const translation typeof header.label string ? { id: content-manager.content-types.${model}.${header.name}, defaultMessage: header.label, } : header.label; return { ...header, label: formatMessage(translation), name: ${header.name}${header.mainField?.name ? .${header.mainField.name} : }, }; }); if (schema?.options?.draftAndPublish) { formattedHeaders.push({ attribute: { type: custom }, name: status, label: formatMessage({ id: getTranslation(containers.list.table-headers.status), defaultMessage: status }), searchable: false, sortable: !hasStatusFilter, } satisfies ListFieldLayout); } return formattedHeaders; }, [/* deps */]);这段代码完整展示了文档所述机制的两个关键设计label的类型双态label为string时说明该列对应内容类型自身属性CM 用content-manager.content-types.{model}.{name}的命名空间去查翻译label为MessageDescriptorreact-intl 对象时说明是插件注入的列i18n、content-releases、review-workflows 即注释中点名的三类插件直接使用该对象自带的翻译。这也解释了类型定义中label: string | MessageDescriptor的由来{ type: custom }的真实用例开启了 draft publish 的内容类型CM 自身就会追加一个status列——它不属于 schema因此走 custom 通道。实战示例i18n 插件注入 Available in 列仓库内 i18n 插件提供了一个标准的cellFormatter注入实现listView.tsxconst addColumnToTableHook ({ displayedHeaders, layout }: AddColumnToTableHookArgs) { const { options } layout; const isFieldLocalized doesPluginOptionsHaveI18nLocalized(options) ? options.i18n.localized : false; if (!isFieldLocalized) { return { displayedHeaders, layout }; } return { displayedHeaders: [ ...displayedHeaders, { attribute: { type: string }, label: { id: getTranslation(list-view.table.header.label), defaultMessage: Available in, }, searchable: false, sortable: false, name: locales, cellFormatter: (props, _header, meta) ( LocaleListCell {...props} {...meta} documentId{props.documentId} / ), }, ], layout, }; };这个例子恰好覆盖了cellFormatter设计的动机本地化列表locales数据并不来自内容类型的可渲染 schema 组件插件必须自带LocaleListCell组件来呈现因此列定义里直接内嵌渲染函数同时它演示了按layout.options判断是否激活仅i18n.localized的内容类型才注入列与用MessageDescriptor携带插件自身翻译两个惯例。对应的测试用例见 listView.test.ts。小结CM 布局机制的完整心智模型将文档与源码串起来CM 视图渲染可以归纳为一条清晰的单向数据流阶段输入输出关键位置配置合成schema 持久化配置 metadatas 组件配置EditLayout/ListLayoutuseDocumentLayout.ts插件改写格式化后的布局 query被 waterfall 改写后的布局mutate-edit-view-layout/inject-column-in-table视图渲染最终布局面板/行/字段三层遍历 →InputRenderer表格列 →TableFormLayout.tsx、ListViewPage.tsx由此可记住三条扩展原则改编辑视图→ 挂载Admin/CM/pages/EditView/mutate-edit-view-layout你拿到的是完整EditLayout含components字典可以自由增删改任何层级的EditFieldLayout改列表视图→ 挂载Admin/CM/pages/ListView/inject-column-in-table向displayedHeaders追加带cellFormatter的列定义字段值永远不进布局布局只描述结构 元数据value/onChange/error由表单内部经useField(name)解析RBAC 裁剪与 custom field 分发集中在 CM 版InputRenderer。这条链路也是理解 Strapi CM 一切高级功能多语言状态列、发布工作流状态、审核工作流列等插件注入列的共同基础它们全部只是上述两个 waterfall 上的不同参与方。【免费下载链接】strapi Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED

相关推荐

Pixelle-Video:AI 短视频自动生成工具,输入一个主题 3 分钟出片

Pixelle-Video:AI 短视频自动生成工具,输入一个主题 3 分钟出片

Pixelle-Video:AI 短视频自动生成工具,输入一个主题 3 分钟出片 【免费下载链接】Pixelle-Video 🚀 AI 全自动短视频引擎 | AI Fully Automated Short Video Engine 项目地址: https://gitcode.com/GitHub_Trending/pi/Pixelle-Video …

📅 2026/10/6 3:32:47
MaaAssistantArknights(MAA)明日方舟全日常一键长草自动化完整指南

MaaAssistantArknights(MAA)明日方舟全日常一键长草自动化完整指南

MaaAssistantArknights(MAA)明日方舟全日常一键长草自动化完整指南 【免费下载链接】MaaAssistantArknights 《明日方舟》小助手,全日常一键长草!| A one-click tool for the daily tasks of Arknights, supporting all clients. …

📅 2026/10/8 10:22:44
猫抓资源嗅探扩展:三步跑通M3U8流媒体下载

猫抓资源嗅探扩展:三步跑通M3U8流媒体下载

猫抓资源嗅探扩展:三步跑通M3U8流媒体下载 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 你有没有点开一个网页,想保存里面…

📅 2026/10/16 15:36:58
MORE NEWS

更多资讯

📰

电气考电网电工证要吗?通过率揭秘与跨省转证全攻略

电气考电网电工证要吗?通过率揭秘与跨省转证全攻略 之前考的证在外省能不能转过来?你不是一个人。很多人在外地打工、做工程时,都遇到过证书不通用的问题,尤其是电网电工证,因为涉及电力安全,国家管得严,各地执行标准也不同,这就导致很多人拿着证却干…

📰

电工证当老师千万别踩坑,怕考不过白交培训费怎么办?

电工证当老师千万别踩坑,怕考不过白交培训费怎么办? 怕考不过白交培训费?电工证当老师这事儿,现在越来越火,但坑也越来越多。很多洛阳涧西区公路工程从业者,想利用自己电工证当老师赚点外快,结果选错机构、培训没用、考试挂了,钱花了不说,还浪费了时…

📰

你学历不高怕报不上名?看看沈阳快速办理电工证的真实经历

你学历不高怕报不上名?看看沈阳快速办理电工证的真实经历 你学历不高怕报不上名?看看沈阳快速办理电工证的真实经历。前两天刚帮一个初中毕业的工友顺利拿到低压电工证,他告诉我,最怕的就是自己学历不够,连报名系统都进不去。后来我们发现,其实只要流程…

📰

查真假电工证怎么查考试真题与备考技巧

电工证真假怎么查?官方回应来了,学历不高也能考! 你是不是也担心自己学历不高,考不了电工证?别急,我来给你说说【查真假电工证怎么查】,还有官方回应怎么看待这个事儿,让你安心报考,少走弯路。 一、查真假电工证怎么查?官方渠道一清二楚…

📰

中宁县电工证报考地点怎么报名?实操考试心里没底怕挂科别慌

中宁县电工证报考地点怎么报名?实操考试心里没底怕挂科别慌 实操考试心里没底怕挂科?这是很多准备报考中宁县电工证的朋友们的真实写照。尤其是刚入行的电工新手,面对严格的考试流程和实操项目,难免会紧张。但别担心,只要掌握好报名流程、考试内容和备考…

📰

低压电工作业审考核题内部消息:学历不高怕报不上名?其实不用愁

低压电工作业审考核题内部消息:学历不高怕报不上名?其实不用愁 你是不是也担心自己学历不够,根本报不上低压电工作业审考核题?别急,这不是你的错,很多人刚开始都是这么想的。不过,我来告诉你个 内部消息…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬