首页
/ Strapi Content Releases 前端技术解析:Redux Toolkit 状态管理、Formik 表单与许可证限额控制

Strapi Content Releases 前端技术解析:Redux Toolkit 状态管理、Formik 表单与许可证限额控制

2026-09-06 11:41:35作者:宗隆裙

本文以 Strapi(开源 headless CMS)中 Content Releases(内容发布)功能的前端技术设计文档为核心,系统讲解该功能在管理后台中的页面组织、基于 Redux Toolkit(RTK Query)的数据流管理、Formik 受控表单的使用,以及企业版许可证(License)如何限制“待处理发布(pending releases)”的数量。读完后,你可以掌握 Strapi 插件前端的标准接入方式:特性开关(feature flag)、RBAC 权限门控、RTK Query 缓存失效策略,以及与 Content Manager 的扩展点集成。

一、功能定位与访问门槛

Content Releases 是 Strapi 的企业版(Enterprise Edition)功能:一个 Release(发布)可以包含多条内容条目(entries),每条条目被分配一个具体动作——publish(发布)或 unpublish(取消发布)。同一个 Release 中的条目可以来自不同内容类型、不同语言版本(locale),点击一次按钮即可批量执行所有指定动作。

前端设计文档明确了访问这两个页面(ReleasesPage 与 ReleaseDetailsPage)的两个硬性前提:

  1. 用户拥有启用了该功能的合法 Strapi 许可证
  2. 用户至少拥有 plugin::content-releases.read 权限。

这两条在源码中都能找到直接对应。插件入口 index.ts 中,注册逻辑首先检查特性开关:

if (window.strapi.features.isEnabled('cms-content-releases')) {
  app.addMenuLink({
    to: `plugins/${pluginId}`,
    icon: PaperPlane,
    intlLabel: { id: `${pluginId}.plugin.name`, defaultMessage: 'Releases' },
    Component: () => import('./pages/App').then((mod) => ({ default: mod.App })),
    permissions: PERMISSIONS.main,
    position: 2,
  });
  // ...
}
  • window.strapi.features.isEnabled('cms-content-releases') 对应“许可证已启用该功能”这一门槛——许可证未购买该功能时,侧边栏根本不会出现 Releases 菜单;
  • permissions: PERMISSIONS.main 对应第二条门槛,PERMISSIONS.mainconstants.ts 中定义为 plugin::content-releases.read 动作。

值得注意的是,index.ts 还处理了“特性未启用但开启 promoteEE 标记”的场景:此时会在设置页注入一个 PurchaseContentReleases 购买引导页,引导用户升级到企业版——这就是文档中“需要合法许可证”在 UI 层的兜底表现。

完整权限清单

插件在 constants.ts 中声明了完整的 RBAC 动作集合,前端各页面按动作粒度门控:

权限动作 用途(前端消费位置)
plugin::content-releases.read 访问 Releases 菜单与列表页(PERMISSIONS.main
plugin::content-releases.create 显示“New release”按钮
plugin::content-releases.update 编辑 Release 名称(详情页“三点”菜单 → Edit)
plugin::content-releases.delete 删除 Release(详情页“三点”菜单 → Delete,触发确认弹窗)
plugin::content-releases.create-action 向 Release 添加条目动作
plugin::content-releases.delete-action 移除 Release 中的条目动作
plugin::content-releases.publish 显示详情页“Publish”按钮,执行发布
plugin::content-releases.settings.read / .settings.update Settings 页中 Releases 设置的读取与修改(PERMISSIONS_SETTINGS

二、两个核心页面:ReleasesPage 与 ReleaseDetailsPage

前端设计文档指出,功能由两个页面组成,源码位于 packages/core/content-releases/admin/src/pages/

2.1 ReleasesPage(发布列表页)

ReleasesPage.tsx 提供所有 Release 的综合展示,内容按两个 Tab 组织:

  • pending:尚未发布的 Release(通过 filters: { releasedAt: { $notNull: false } } 过滤,见 handleTabChange);
  • done:已发布的 Release(releasedAt: { $notNull: true })。

页面上每个 Release 以卡片形式展示,卡片包含名称、计划时间(未计划时显示“Not scheduled”)以及状态徽章。Release 有五种状态,前端通过 getBadgeProps 将状态映射为不同颜色:

  • Ready(success/绿色):发布已完全就绪,无无效条目;
  • Blocked(warning/黄色):存在至少一条无效条目,阻止发布;
  • Empty(neutral/灰色):发布不含任何条目,不可发布;
  • Failed(danger/红色):上次发布尝试遇到错误且此后无变化;
  • Done(primary):发布已成功完成,无错误。

分页通过 Pagination 组件实现,每页条数可选 8/16/32/64源码)。

新建 Release:若用户拥有 plugin::content-releases.update 权限(页面内使用 useRBAC(PERMISSIONS).allowedActions.canCreate 判断,见 第 198-200 行),页头会显示“New release”按钮;点击后打开表单弹窗,要求填写名称——待处理(pending)Release 的名称必须唯一

2.2 ReleaseDetailsPage(发布详情页)

ReleaseDetailsPage.tsx 承载单个 Release 的条目管理与发布操作,具备以下能力:

  • 条目分组:Release 内条目可按三种方式分组——Content-Types(同内容类型)、Locales(同语言版本)、Actions(同动作 publish/unpublish);
  • 编辑 Release:拥有 plugin::content-releases.update 权限时,页头“三点”按钮菜单中出现“Edit”选项,打开弹窗修改名称。保存时新名称必须唯一(仅针对 pending release)、非空且确实发生了变化
  • 删除 Release:拥有 plugin::content-releases.delete 权限时,菜单中出现“Delete”选项,选择后触发确认弹窗;
  • 条目状态查看:每条条目带有状态,指示其是否可执行 Publish/Unpublish 或存在校验错误。有校验错误的条目可通过其“三点”菜单中的“Edit entry”直接跳转到内容管理器(CM)中的对应条目,用户解决错误后回到详情页点击“Refresh”按钮刷新各条目状态;
  • 执行发布:拥有 plugin::content-releases.publish 权限的用户可点击“Publish”按钮。若任一条目存在校验错误,“Publish”动作会触发通知提示用户错误所在。

三、状态管理:Redux Toolkit(RTK Query)

设计文档明确:Redux Toolkit 负责管理 content releases 的全部数据流——数据获取(data retrieval)、Release 的创建与编辑、Release Actions 的拉取。这一职责集中在 services/release.ts 中,其实现有三个关键设计点。

3.1 基于 adminApi 的 injectEndpoints

插件没有自建独立的 RTK Query API,而是复用 Strapi Admin 全局的 adminApi(来自 @strapi/admin/strapi-admin),通过 enhanceEndpoints + injectEndpoints 注入自己的端点(源码)。这样做的收益是跨插件缓存协同:例如当用户在内容管理器中更新/删除文档时,RTK Query 的 updateDocumentdeleteDocumentdeleteManyDocumentsdiscardDocument 以及工作流(Review Workflows)相关端点被 extendInvalidatesTags 扩展,统一使 Release/ReleaseAction 列表缓存失效(第 91-132 行)——这正对应后端设计中“条目被更新或删除时,包含该条目的所有 Release 状态会被重算”的联动需求。

3.2 标签(Tag)驱动的缓存失效

第 80-89 行 声明了六个缓存标签类型:ReleaseReleaseActionEntriesInReleaseReleaseSettingsDocumentUpcomingReleasesList。每个 mutation 通过 invalidatesTags 声明自己会使哪些缓存失效,例如 publishRelease 会失效 Release(对应 id)、DocumentUpcomingReleasesList第 362-374 行),确保发布动作完成后首页“Upcoming releases”小部件与 CM 中的文档状态同步刷新。

3.3 端点清单

injectEndpoints 注入的端点与后端路由一一对应,导出的 React hooks 即页面层使用的全部数据接口(第 424-459 行):

Hook HTTP 方法与路径 说明
useGetReleasesQuery GET /content-releases 分页获取 Release 列表(默认 page=1、pageSize=16,pending/done Tab 由 filters.releasedAt.$notNull 区分,见 transformResponse
useGetReleaseQuery GET /content-releases/:id 获取单个 Release
useGetReleaseActionsQuery GET /content-releases/:releaseId/actions 分页获取 Release Actions,支持 groupBy 参数
useGetReleasesForEntryQuery GET /content-releases/getByDocumentAttached contentTypeUid/locale/documentId 查询挂载/未挂载某条目的 Release
useCreateReleaseMutation POST /content-releases 创建 Release(body: namescheduledAttimezone
useUpdateReleaseMutation PUT /content-releases/:id 更新 Release
useDeleteReleaseMutation DELETE /content-releases/:id 删除 Release
usePublishReleaseMutation POST /content-releases/:id/publish 执行发布
useCreateReleaseActionMutation / useCreateManyReleaseActionsMutation POST /content-releases/:releaseId/actions(及 /bulk 添加/批量添加条目动作
useUpdateReleaseActionMutation PUT /content-releases/:releaseId/actions/:actionId 修改动作类型(publish/unpublish)
useDeleteReleaseActionMutation DELETE /content-releases/:releaseId/actions/:actionId 移除条目动作
useGetMappedEntriesInReleasesQuery GET /content-releases/mapEntriesToReleases 获取 CM 表格中“所属 Release”映射列数据
useGetReleaseSettingsQuery / useUpdateReleaseSettingsMutation GET / PUT /content-releases/settings 读取/更新 Releases 设置(如默认时区)

3.4 乐观更新(Optimistic Update)示例

useUpdateReleaseActionMutation 展示了典型的 RTK Query 乐观更新写法(第 315-342 行):在 onQueryStarted 中先用 releaseApi.util.updateQueryData 直接修改 getReleaseActions 缓存中对应条目的 action.type,请求失败时调用 patchResult.undo() 回滚。这使得用户在详情页切换某条目的 publish/unpublish 动作时界面即时响应,无需等待服务端往返。

四、创建与编辑 Release:Formik 受控表单

设计文档指出:创建/编辑 Release 使用 Formik,且所有输入组件均为受控组件(controlled components)。表单由 ReleaseModal.tsx 承载,其表单值类型 FormValues 在列表页中可见初始值定义(ReleasesPage.tsx 第 173-180 行):

const INITIAL_FORM_VALUES = {
  name: '',
  date: format(new Date(), 'yyyy-MM-dd'),
  time: '',
  isScheduled: true,
  scheduledAt: null,
  timezone: null,
} satisfies FormValues;

几个实现细节值得注意:

  1. 时区默认值来自设置:弹窗打开时,若 useGetReleaseSettingsQuery 返回了 defaultTimezone(格式为 xxx&zoneName),则取 & 之后的部分作为表单 timezone 初始值;
  2. 提交处理handleAddRelease 调用 useCreateReleaseMutation,成功时弹出成功通知、上报 trackUsage('didCreateRelease') 埋点并跳转到新 Release 详情页;isFetchError 分支通过 useAPIErrorHandler 格式化展示服务端错误;
  3. 名称唯一性:pending Release 的名称唯一约束同时作用于创建表单与详情页的编辑弹窗(编辑时还要求名称非空且与原名不同)。

五、许可证限额:useLicenseLimits 与 Chargebee

设计文档说明:大多数许可证通过 Chargebee 配置了基于功能的用量限制,这些限制通过 useLicenseLimits 暴露给前端;如果许可证未指定最大待处理 Release 数,则使用硬编码默认值——最多 3 个 pending release。

源码印证(ReleasesPage.tsx 第 193-196 行):

const { getFeature } = useLicenseLimits();
const { maximumReleases = 3 } = getFeature('cms-content-releases') as {
  maximumReleases: number;
};

useLicenseLimits hook 本体位于管理后台 EE 模块:useLicenseLimits.ts,其内部从应用信息接口读取许可证功能配置并按功能 key 返回限额对象。

限额达到时的 UI 行为(源码):

const totalPendingReleases = (isSuccess && response.currentData?.meta?.pendingReleasesCount) || 0;
const hasReachedMaximumPendingReleases = totalPendingReleases >= maximumReleases;
  • 达到上限时,“New release”按钮被 disabled第 304 行);
  • 页面顶部显示 Alert 横幅,文案为“You have reached the {number} pending release(s) limit. Upgrade to manage an unlimited number of releases.”,并提供“Explore plans”入口(第 317-344 行)。

pending 总数 pendingReleasesCount 由后端在列表响应的 meta 中下发,前端不自行统计。

六、后端端点总览(Admin API)

所有 Release 与 Release Action 路由仅挂载在 Admin API 上(即 /admin 前缀,非公开的 API Router)。完整端点如下(详见后端设计文档 01-backend.md):

Release

方法 端点 参数 / 请求体
GET /content-releases/ page: number; pageSize: number
GET /content-releases/getByDocumentAttached contentTypeUid: string; locale?: string; documentId?: string; hasEntryAttached?: boolean
GET /content-releases/:id
POST /content-releases/ { name: string }
PUT /content-releases/:id { name: string }
DELETE /content-releases/:id
POST /content-releases/:id/publish

Release Action

方法 端点 参数 / 请求体
POST /content-releases/:releaseId/actions { type: 'publish' | 'unpublish', contentType: string, locale?: string, entryDocumentId?: string }
GET /content-releases/:releaseId/actions page: number; pageSize: number
PUT /content-releases/:releaseId/actions/:actionId { type: 'publish' | 'unpublish' }
DELETE /content-releases/:releaseId/actions/:actionId

后端实现位于 packages/core/content-releases/server,其中 ReleaseRelease Action 是两个隐藏内容类型,分别落库为 strapi_releasesstrapi_release_actions。v5 中 Release Action 不再使用内置多态关联,而是存储 contentTypelocaleentryDocumentId 字段建立“手动”关联——关联的是文档 ID(documentId)而非可能随时变化的条目 ID,这一设计保证了关联的长期可靠性。

七、插件注册与 Strapi Admin 扩展点

admin/src/index.ts 是理解“该功能如何长进 Strapi Admin”的钥匙,它在特性启用时注入了多个扩展点:

  1. 主菜单app.addMenuLink 添加 Releases 一级菜单(position: 2,懒加载 App.tsx 路由容器);
  2. CM 编辑视图侧边栏:通过 contentManagerPluginApis.addEditViewSidePanel([ReleasesPanel]) 在内容编辑器右侧注入 ReleasesPanel,让用户编辑条目时直接看到其所属的 Release;
  3. CM 文档操作addDocumentAction 在编辑视图动作列表中、unpublish 动作之前插入“Add to release”动作(ReleaseActionModalForm);
  4. CM 批量操作addBulkAction 在批量操作列表中 delete 动作之前插入 ReleaseAction,支持列表视图多选条目加入 Release;
  5. CM 列表表格列app.registerHook('Admin/CM/pages/ListView/inject-column-in-table', addColumnToTableHook) 注入“所属 Release”列(数据来自 mapEntriesToReleases 端点);
  6. 首页小部件app.widgets.register 注册“Upcoming releases”小部件(Widgets.tsx),展示即将发布的 Release 并链接到 Releases 页;
  7. 设置页app.addSettingsLink 注册 Releases 设置入口(licenseOnly: true,组件为 ReleasesSettingsPage);
  8. 国际化registerTrads 按语言动态加载 translations/ 目录下的翻译文件,并经 prefixPluginTranslations 统一加 content-releases. 前缀;
  9. 自定义 Hookapp.createHook('ContentReleases/pages/ReleaseDetails/add-locale-in-releases') 暴露给第三方插件向详情页表格追加 locale 列。

八、测试覆盖与延伸阅读

前端实现的测试用例可直接用于验证本文描述的行为:

相关文档与源码路径,便于继续深入:

  • 前端页面设计:Releases 页Release 详情页
  • 后端设计(内容类型、路由、控制器、服务、迁移与生命周期事件):01-backend.md
  • 定时发布(Scheduling):03-scheduling.md。文档提示该能力仍在开发中,可通过 future flag contentReleasesScheduling 自行开启试用。前端表单中的 isScheduled/scheduledAt/timezone 字段及卡片上的计划时间展示,正是该能力的 UI 承载;
  • 后端服务实现(Release CRUD、状态重算触发器、调度):server 目录
  • 许可证 Hook 及其测试:useLicenseLimits.tsuseLicenseLimits.test.ts

小结

Strapi Content Releases 的前端实现是一套典型的企业版插件样板:以 cms-content-releases 特性开关 + plugin::content-releases.* 权限动作构成双层访问门控;以 RTK Query(复用全局 adminApi、标签失效 + 乐观更新)统一管理数据流;以 Formik 受控表单承载创建/编辑交互;以 useLicenseLimits 将 Chargebee 侧的许可证限额落到按钮禁用与升级横幅上。理解这套模式,对开发任何需要嵌入 Strapi Admin 的企业级插件都有直接参考价值。

登录后查看全文
热门项目推荐
相关项目推荐