Strapi 内容管理器 Document 机制详解:useDocument 与 useDocumentActions 实战指南
2026/9/7 18:50:24 网站建设 项目流程

Strapi 内容管理器 Document 机制详解:useDocument 与 useDocumentActions 实战指南

【免费下载链接】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

Strapi(V5)内容管理器(Content Manager,简称 CM)的核心抽象是document(文档)——它由 draft & publish 状态与国际化 locale 等维度组合成的 entry 矩阵构成。本文讲解 document 的数据模型、如何通过useDocument钩子按维度定位并获取特定 entry,以及如何用useDocumentActions执行 create / update / delete / clone / publish / unpublish 等操作,并结合开源仓库源码剖析两个钩子的真实签名、缓存机制与底层 REST 端点,帮助你在自定义插件与编辑器扩展中正确操作文档。

Document 的数据模型:一个 entry 矩阵

在理解任何 API 之前,必须先理解 CM 中最底层的概念:document。

  • 创建 document 的逻辑主要位于@strapi/core包中(对应源码目录packages/core/core/src);
  • document 这个概念是为 V5 的新版 draft & publish 特性(2024 年 Q1 引入)设计的:从本质上看,document 是entry(条目)组成的矩阵,矩阵的复杂度取决于该 document 启用的维度数量。

一个典型例子:某个 collection-type 同时启用了 Draft & Publish 和国际化(配置了 2 个 locale),则该 document 的矩阵包含 4 个 entry——每个 locale 各有一个草稿(draft)和一个已发布(published)条目。

需要特别注意的两点约束:

  1. Draft & Publish 是 content-type 级别的可选配置——并非所有 content-type 都启用了草稿/发布维度;
  2. 所有维度都应可查询(如published & 'en-GB'),以便把 document 矩阵收敛到具体的某一行 entry。

获取文档:useDocument 钩子

读取任意文档的统一入口是useDocument钩子。调用它需要理解三个关键参数:

参数含义示例
model内容类型的完整 UID,即"模型"api::article.article
collectionType内容类型的"种类"(kind)collection-typesingle-type
documentId文档的特定id数据库主键

不传locale等具体维度参数时,拿到的是用户应用定义的默认版本(由服务端决定收敛到矩阵的哪个 entry);而通过params传入维度(如{ locale: 'en-GB' })即可把结果精确定位到矩阵中的特定 entry,例如"已发布 + en-GB"。

官方文档给出的标准用法如下:

const { id, model, collectionType } = useParams<{ id: string; model: string; collectionType: string; }>(); if (!model || !collectionType) return null; const { document, isLoading, validate } = useDocument({ documentId: id, model, collectionType, params: { locale: 'en-GB' }, }); const { update } = useDocumentActions(); const onSubmit = async (entity: Entity) => { const errors = validate(entity); if (errors) { // handle errors } await update({ collectionType, model, documentId: id }, entity); };

源码视角:useDocument 到底做了什么

查看 useDocument 的实现 可以确认几个关键点:

  1. 数据来自服务端 + RTK Query 缓存层。钩子内部调用useGetDocumentQuery(RTK Query 生成):

    // packages/core/content-manager/admin/src/hooks/useDocument.ts const { currentData: data, isLoading: isLoadingDocument, isFetching: isFetchingDocument, error, refetch, } = useGetDocumentQuery(args, { ...opts, skip: (!args.documentId && args.collectionType !== SINGLE_TYPES) || opts?.skip, });

    文件头部的注释解释了为何可以"直接从服务端拉取"而不走 context provider:redux-toolkit-query本身就是缓存层,只要缓存未被失效(invalidate)就不会重复请求。这也意味着useDocument返回的refetch是手动刷新缓存的入口,skip参数可用于条件性跳过请求。

  2. single-type 的跳过逻辑:从上面的skip表达式可以看出——没有documentId且不是SINGLE_TYPES时跳过请求。这是因为 single-type 恒有且仅有一个文档,documentId可以缺省,collectionTypesingle-type即可定位;而 collection-type 缺documentId时无法定位具体文档。

  3. 返回值远比文档简介丰富。实际返回对象包含:document(文档数据)、meta(含availableLocales等元信息)、schema/schemas(内容类型 schema,用于构建校验规则)、components(该内容类型用到的组件字典)、isLoadinghasErrorvalidate(基于 Yup 的客户端校验)、getTitlegetInitialFormValuesrefetch。其中validate通过createYupSchema(schema.attributes, components)由内容类型 schema 现场生成校验规则(见 useDocument.ts),校验失败时返回 Yup 错误对象,成功返回null——这正是上面示例中validate(entity)能拦截非法数据的底层原因。

  4. 表单初值与国际化继承getInitialFormValues负责编辑/新建两种场景的表单初始化。源码注释说明了一个细节:新建某个 locale 的草稿时,钩子会利用响应中的meta.availableLocales[0](服务端保证默认 locale 排在首位)把非 localized 的标量与媒体字段从兄弟 locale 继承进来;而 component、dynamiczone、relation 字段不在该载荷里,由服务端在新 locale 行首次创建时通过copyNonLocalizedFields补齐(对应packages/core/core/src/services/document-service/internationalization.ts)。这解释了为什么"所有维度可查询"的矩阵模型能在前端表单层面自洽地工作。

  5. 内容管理器内部请优先用useDoc。源码中 useDoc 是useDocument的轻量包装:它直接从 react-router 的 URL 参数(idslugcollectionType)提取参数并做存在性校验,省去手动useParams和空值判断。此外还有useContentManagerContext,在内容管理器路由内聚合了模型信息、表单状态与 layout,适合插件开发时快速接入。

TypeScript 类型参考

官方文档给出的类型签名(摘录自 use-document 文档):

import type { Attribute } from '@strapi/strapi'; interface Document { documentId: string; [key: string]: Attribute.GetValue<Attribute.Any>; } interface UseDocumentArgs { collectionType: string; model: string; documentId?: string; params?: object; } type UseDocument = (args: UseDocumentArgs) => { document?: Document; isLoading: boolean; validate: (entity: Entity) => null | Record<string, TranslationMessage>; };

操作文档:useDocumentActions 钩子

document 支持一组通用操作,这是文档矩阵模型上最基础的行为集合:

操作说明适用条件
create创建新文档所有类型(single-type 恒存在,实际走 update 语义)
update更新已有文档所有类型
delete删除已有文档所有类型
clone克隆文档collection-type
publish/unpublish发布 / 取消发布仅启用 Draft & Publish 的文档

以上能力统一由useDocumentActions钩子暴露。文档的官方示例:

import { Form } from '@strapi/admin/admin'; const { id, model, collectionType } = useParams<{ id: string; model: string; collectionType: string; }>(); const { update } = useDocumentActions(); const handleSubmit = async (data) => { await update({ collectionType, model, documentId: id }, data); }; return <Form method="PUT" onSubmit={handleSubmit} />;

文档同时说明了一个设计约定:这些钩子会处理操作失败时的通知(notification),但无论成败始终返回响应对象,方便调用方处理副作用。

源码视角:比文档更完整的操作面

阅读 useDocumentActions 的实现,实际暴露的操作比文档简介中的三个通用操作更丰富:

  • 单文档:clonecreatedeletediscard(丢弃草稿变更)、getDocumentpublishupdateunpublish(支持discardDraft选项)
  • 批量:deleteManypublishManyunpublishMany
  • 克隆变体:autoClone(按sourceId让服务端自动生成克隆内容)

所有写操作的返回类型统一为OperationResponse

type OperationResponse<TResponse extends { data: unknown; meta?: unknown; error?: unknown }> = | Pick<TResponse, 'data'> | Pick<TResponse, 'data' | 'meta'> | { error: BaseQueryError | SerializedError };

调用方通过'error' in res判别是否失败;失败时钩子内部已经通过toggleNotification弹出错误提示(经formatAPIError格式化 API 错误),并触发trackUsage埋点事件(如willDeleteEntry/didDeleteEntry/didNotDeleteEntrywillPublishEntry等),这些行为在 useDocumentActions.ts 的每个操作实现中都能逐一对应。

另外两个值得注意的实现细节:

  1. clone会先剥离id/documentId再提交,避免把源文档的标识复制进克隆体,并在成功后通过路由跳转到新文档的编辑页(见 useDocumentActions.ts);
  2. isLoading是所有写操作的聚合态,由 delete / deleteMany / publish / publishMany / unpublishMany / update / discard 各自的 mutation 加载状态或运算得出,适合直接驱动按钮的禁用态。

底层 REST 端点

这些钩子最终落到内容管理器的 REST API 上。查看 documents 服务定义,可以看到端点的一一对应关系:

钩子操作HTTP 请求
createPOST /content-manager/collection-types/${model}
updatePUT /content-manager/collection-types/${model}/${documentId}
deleteDELETE /content-manager/collection-types/${model}/${documentId}
clonePOST /content-manager/collection-types/${model}/clone/${sourceId}
autoClonePOST /content-manager/collection-types/${model}/auto-clone/${sourceId}
publish/unpublish对应 model 下的 publish / unpublish 子资源

值得注意的是,每个 mutation 都配置了invalidatesTags(如{ type: 'Document', id:${model}_LIST}'CountDocuments''RecentDocumentList'等),这就是前文所说 RTK Query 缓存的失效机制:一次 create 成功后,列表、计数、最近文档等关联缓存会被自动刷新,多个页面组件因此保持数据一致。createDocument的注释还明确了一条边界:它只能用于 collection-type,single-type 应始终使用updateDocument(因为它恒存在)——这与 document 矩阵模型中"single-type 只有唯一行"的设计完全吻合。

扩展性与边界说明

  • 插件扩展:官方文档指出,插件可以通过 API 向文档追加额外的自定义操作。CM 本身除了这些公共 API 外,还导出了若干通用钩子供用户在 CM 插件内外交互,参见 Content Manager 介绍。
  • 稳定性声明useDocumentuseDocumentActions在官方文档中均被标注为unstable(不稳定),未来版本可能发生变化;源码 JSDoc 中同样以@alpha/@public标记。在自研插件中使用时应做好兼容层封装。
  • 适用前提:本文所述文档矩阵模型(draft & publish 维度 + locale 维度)基于当前仓库(Strapi V5 开发主线)的文档与实现;draftAndPublish是否启用由内容类型自身的schema.options决定(可参考 useContentManagerContext 中hasDraftAndPublish: schema?.options?.draftAndPublish ?? false的取值方式),在编写 UI 逻辑时应先读取该配置再决定是否展示 publish/unpublish 入口。
  • 想继续深入,可对照阅读同目录下的 use-content-types.mdx、use-document.mdx 与 use-document-actions.mdx 的完整 API 参考,以及packages/core/content-manager/shared/contracts/collection-types.ts中各操作的请求/响应契约类型。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询