Strapi Content Releases 前端技术解析:Redux Toolkit 状态管理、Formik 表单与许可证限额控制
【免费下载链接】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(开源 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)的两个硬性前提:
- 用户拥有启用了该功能的合法 Strapi 许可证;
- 用户至少拥有
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.main在 constants.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 的updateDocument、deleteDocument、deleteManyDocuments、discardDocument以及工作流(Review Workflows)相关端点被extendInvalidatesTags扩展,统一使Release/ReleaseAction列表缓存失效(第 91-132 行)——这正对应后端设计中“条目被更新或删除时,包含该条目的所有 Release 状态会被重算”的联动需求。
3.2 标签(Tag)驱动的缓存失效
第 80-89 行 声明了六个缓存标签类型:Release、ReleaseAction、EntriesInRelease、ReleaseSettings、Document、UpcomingReleasesList。每个 mutation 通过invalidatesTags声明自己会使哪些缓存失效,例如publishRelease会失效Release(对应 id)、Document和UpcomingReleasesList(第 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:name、scheduledAt、timezone) |
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;几个实现细节值得注意:
- 时区默认值来自设置:弹窗打开时,若 useGetReleaseSettingsQuery 返回了
defaultTimezone(格式为xxx&zoneName),则取&之后的部分作为表单timezone初始值; - 提交处理:handleAddRelease 调用
useCreateReleaseMutation,成功时弹出成功通知、上报trackUsage('didCreateRelease')埋点并跳转到新 Release 详情页;isFetchError分支通过useAPIErrorHandler格式化展示服务端错误; - 名称唯一性: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; };useLicenseLimitshook 本体位于管理后台 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,其中Release与Release Action是两个隐藏内容类型,分别落库为strapi_releases与strapi_release_actions。v5 中 Release Action 不再使用内置多态关联,而是存储contentType、locale、entryDocumentId字段建立“手动”关联——关联的是文档 ID(documentId)而非可能随时变化的条目 ID,这一设计保证了关联的长期可靠性。
七、插件注册与 Strapi Admin 扩展点
admin/src/index.ts 是理解“该功能如何长进 Strapi Admin”的钥匙,它在特性启用时注入了多个扩展点:
- 主菜单:
app.addMenuLink添加 Releases 一级菜单(position: 2,懒加载 App.tsx 路由容器); - CM 编辑视图侧边栏:通过
contentManagerPluginApis.addEditViewSidePanel([ReleasesPanel])在内容编辑器右侧注入 ReleasesPanel,让用户编辑条目时直接看到其所属的 Release; - CM 文档操作:
addDocumentAction在编辑视图动作列表中、unpublish动作之前插入“Add to release”动作(ReleaseActionModalForm); - CM 批量操作:
addBulkAction在批量操作列表中delete动作之前插入ReleaseAction,支持列表视图多选条目加入 Release; - CM 列表表格列:
app.registerHook('Admin/CM/pages/ListView/inject-column-in-table', addColumnToTableHook)注入“所属 Release”列(数据来自mapEntriesToReleases端点); - 首页小部件:
app.widgets.register注册“Upcoming releases”小部件(Widgets.tsx),展示即将发布的 Release 并链接到 Releases 页; - 设置页:
app.addSettingsLink注册 Releases 设置入口(licenseOnly: true,组件为 ReleasesSettingsPage); - 国际化:
registerTrads按语言动态加载 translations/ 目录下的翻译文件,并经prefixPluginTranslations统一加content-releases.前缀; - 自定义 Hook:
app.createHook('ContentReleases/pages/ReleaseDetails/add-locale-in-releases')暴露给第三方插件向详情页表格追加 locale 列。
八、测试覆盖与延伸阅读
前端实现的测试用例可直接用于验证本文描述的行为:
- ReleasesPage.test.tsx:列表页渲染、Tab 切换、限额横幅等;
- ReleaseDetailsPage.test.tsx 及数据桩 mockReleaseDetailsPageData.ts:详情页分组、条目状态、发布流程;
- 组件级测试位于 admin/src/components/tests/,覆盖 ReleaseModal(ReleaseModal.test.tsx)、EntryValidationPopover、ReleaseActionMenu 等。
相关文档与源码路径,便于继续深入:
- 前端页面设计:Releases 页、Release 详情页;
- 后端设计(内容类型、路由、控制器、服务、迁移与生命周期事件):01-backend.md;
- 定时发布(Scheduling):03-scheduling.md。文档提示该能力仍在开发中,可通过 future flag
contentReleasesScheduling自行开启试用。前端表单中的isScheduled/scheduledAt/timezone字段及卡片上的计划时间展示,正是该能力的 UI 承载; - 后端服务实现(Release CRUD、状态重算触发器、调度):server 目录;
- 许可证 Hook 及其测试:useLicenseLimits.ts、useLicenseLimits.test.ts。
小结
Strapi Content Releases 的前端实现是一套典型的企业版插件样板:以cms-content-releases特性开关 +plugin::content-releases.*权限动作构成双层访问门控;以 RTK Query(复用全局adminApi、标签失效 + 乐观更新)统一管理数据流;以 Formik 受控表单承载创建/编辑交互;以useLicenseLimits将 Chargebee 侧的许可证限额落到按钮禁用与升级横幅上。理解这套模式,对开发任何需要嵌入 Strapi Admin 的企业级插件都有直接参考价值。
【免费下载链接】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),仅供参考