管理面板 API:注入区(Injection zones)
页面摘要: 注入区是管理 UI 中插件可以注入 React 组件的预定义区域。使用
getPlugin('content-manager').injectComponent()来扩展内置视图,或在registerPlugin中通过injectionZones定义你自己的区域。
插件可以通过将自定义的 React 组件注入到预定义区域,来扩展并自定义现有的管理面板板块。这样无需修改核心代码,即可为 Strapi 内置界面添加功能。
在深入阅读本页概念之前,请确保你已经:
- 创建了一个 Strapi 插件,
- 阅读并理解了 Admin Panel API 的基础知识。
注入区与内容管理器 API 对比
要为内容管理器添加面板、操作或按钮,Content Manager APIs(addDocumentAction、addEditViewSidePanel 等)通常比注入区更健壮、类型更好。当需要将组件插入到 Content Manager APIs 未覆盖的特定 UI 区域时,使用注入区。
Content Manager APIs 和注入区都是自定义管理面板的扩展点,但它们解决不同的需求:
| 需求 | 推荐 API | 原因 |
|---|---|---|
| 在编辑视图侧边区域添加自定义面板 | Content Manager API(addEditViewSidePanel) | 最适合编辑时保持可见的上下文信息或控件。 |
| 在文档操作菜单中添加操作 | Content Manager API(addDocumentAction) | 最适合编辑视图操作菜单中的文档级操作。 |
| 在编辑视图页头添加操作 | Content Manager API(addDocumentHeaderAction) | 最适合文档标题旁的快速、醒目操作。 |
| 为列表视图中选中的条目添加操作 | Content Manager API(addBulkAction) | 最适合一次应用于多个条目的工作流。 |
| 在插件视图的预定义区域添加 UI(本地化视觉自定义) | Injection Zones API(injectComponent) | 最适合针对插件暴露的特定区域。 |
实现细节和最新的 API 签名请参阅 Strapi 代码库中的 content-manager 文件。
预定义注入区
Strapi 的内容管理器提供了插件可以使用的预定义注入区:
| View | Injection zone | Location |
|---|---|---|
| List view | listView.actions | 在筛选器与齿轮图标之间 |
| List view | listView.publishModalAdditionalInfos | 发布确认弹窗中的信息内容 |
| List view | listView.unpublishModalAdditionalInfos | 取消发布确认弹窗中的信息内容 |
| List view | listView.deleteModalAdditionalInfos | 删除确认弹窗中的信息内容 |
| Edit view | editView.right-links | 在 “Configure the view” 与 “Edit” 按钮之间 |
| Edit view | editView.informations | 在编辑视图的信息框中(内部使用,见下方说明) |
| Preview | preview.actions | 在预览视图的操作区域中 |
listView.*ModalAdditionalInfos 区域用于在发布、取消发布和删除确认弹窗中丰富所显示的信息内容。
editView.informations 区域存在于内容管理器源代码中,但被视为内部区域。对于第三方插件,editView.right-links 是最稳定且官方推荐的编辑视图扩展点。仅当你明确需要信息面板区域并接受版本之间可能出现的 UI 变动时,才使用 editView.informations。
注入到内容管理器区域
要将组件注入到内容管理器的注入区,请在 bootstrap 生命周期中使用 getPlugin('content-manager').injectComponent():
JavaScript
import { MyCustomButton } from './components/MyCustomButton';
import { PreviewAction } from './components/PreviewAction';
import { PublishModalInfo } from './components/PublishModalInfo';
export default {
register(app) {
app.registerPlugin({
id: 'my-plugin',
name: 'My Plugin',
});
},
bootstrap(app) {
// Inject a button into the Edit view's right-links zone
// highlight-start
app
.getPlugin('content-manager')
.injectComponent('editView', 'right-links', {
name: 'my-plugin-custom-button',
Component: MyCustomButton,
});
// highlight-end
// Inject a component into the List view's actions zone
// highlight-start
app
.getPlugin('content-manager')
.injectComponent('listView', 'actions', {
name: 'my-plugin-list-action',
Component: () => <button>Custom List Action</button>,
});
// highlight-end
// Inject additional information into the publish modal
// highlight-start
app
.getPlugin('content-manager')
.injectComponent('listView', 'publishModalAdditionalInfos', {
name: 'my-plugin-publish-modal-info',
Component: PublishModalInfo,
});
// highlight-end
// Inject a component into the Preview view's actions zone
// highlight-start
app
.getPlugin('content-manager')
.injectComponent('preview', 'actions', {
name: 'my-plugin-preview-action',
Component: PreviewAction,
});
// highlight-end
},
};
TypeScript
import type { StrapiApp } from '@strapi/admin/strapi-admin';
import { MyCustomButton } from './components/MyCustomButton';
import { PreviewAction } from './components/PreviewAction';
import { PublishModalInfo } from './components/PublishModalInfo';
export default {
register(app: StrapiApp) {
app.registerPlugin({
id: 'my-plugin',
name: 'My Plugin',
});
},
bootstrap(app: StrapiApp) {
// Inject a button into the Edit view's right-links zone
// highlight-start
app
.getPlugin('content-manager')
.injectComponent('editView', 'right-links', {
name: 'my-plugin-custom-button',
Component: MyCustomButton,
});
// highlight-end
// Inject a component into the List view's actions zone
// highlight-start
app
.getPlugin('content-manager')
.injectComponent('listView', 'actions', {
name: 'my-plugin-list-action',
Component: () => <button>Custom List Action</button>,
});
// highlight-end
// Inject additional information into the publish modal
// highlight-start
app
.getPlugin('content-manager')
.injectComponent('listView', 'publishModalAdditionalInfos', {
name: 'my-plugin-publish-modal-info',
Component: PublishModalInfo,
});
// highlight-end
// Inject a component into the Preview view's actions zone
// highlight-start
app
.getPlugin('content-manager')
.injectComponent('preview', 'actions', {
name: 'my-plugin-preview-action',
Component: PreviewAction,
});
// highlight-end
},
};
自定义注入区
插件可以定义自己的注入区,以允许其他插件扩展其 UI。在 registerPlugin 配置中声明注入区:
JavaScript
export default {
register(app) {
app.registerPlugin({
id: 'dashboard',
name: 'Dashboard',
// highlight-start
injectionZones: {
homePage: {
top: [],
middle: [],
bottom: [],
},
sidebar: {
before: [],
after: [],
},
},
// highlight-end
});
},
};
TypeScript
import type { StrapiApp } from '@strapi/admin/strapi-admin';
export default {
register(app: StrapiApp) {
app.registerPlugin({
id: 'dashboard',
name: 'Dashboard',
// highlight-start
injectionZones: {
homePage: {
top: [],
middle: [],
bottom: [],
},
sidebar: {
before: [],
after: [],
},
},
// highlight-end
});
},
};
在组件中渲染注入区
在 Strapi 5 中,@strapi/helper-plugin 中的 InjectionZone 组件已被移除,且没有直接的替代导出。要渲染被注入的组件,请使用 @strapi/strapi/admin 中的 useStrapiApp 创建你自己的组件。
JavaScript
import { useStrapiApp } from '@strapi/strapi/admin';
export const CustomInjectionZone = ({ area, ...props }) => {
const getPlugin = useStrapiApp('CustomInjectionZone', (state) => state.getPlugin);
const [pluginName, view, zone] = area.split('.');
const plugin = getPlugin(pluginName);
const components = plugin?.getInjectedComponents(view, zone);
if (!components?.length) {
return null;
}
return components.map(({ name, Component }) => <Component key={name} {...props} />);
};
import { CustomInjectionZone } from '../components/CustomInjectionZone';
const Dashboard = () => {
return (
<div>
<h1>Dashboard</h1>
{/* Render components injected into the top zone */}
<CustomInjectionZone area="dashboard.homePage.top" />
<div className="main-content">{/* Main dashboard content */}</div>
{/* Render components injected into the bottom zone */}
<CustomInjectionZone area="dashboard.homePage.bottom" />
</div>
);
};
export default Dashboard;
TypeScript
import { useStrapiApp } from '@strapi/strapi/admin';
type CustomInjectionZoneProps = {
area: `${string}.${string}.${string}`;
[key: string]: unknown;
};
export const CustomInjectionZone = ({ area, ...props }: CustomInjectionZoneProps) => {
const getPlugin = useStrapiApp('CustomInjectionZone', (state) => state.getPlugin);
const [pluginName, view, zone] = area.split('.');
const plugin = getPlugin(pluginName);
const components = plugin?.getInjectedComponents(view, zone);
if (!components?.length) {
return null;
}
return components.map(({ name, Component }) => <Component key={name} {...props} />);
};
import { CustomInjectionZone } from '../components/CustomInjectionZone';
const Dashboard = () => {
return (
<div>
<h1>Dashboard</h1>
{/* Render components injected into the top zone */}
<CustomInjectionZone area="dashboard.homePage.top" />
<div className="main-content">{/* Main dashboard content */}</div>
{/* Render components injected into the bottom zone */}
<CustomInjectionZone area="dashboard.homePage.bottom" />
</div>
);
};
export default Dashboard;
注入到自定义区域
其他插件可以使用其 bootstrap 生命周期中的 injectComponent() 方法,将组件注入到你的自定义注入区:
JavaScript
import { Widget } from './components/Widget';
export default {
register(app) {
app.registerPlugin({
id: 'widget-plugin',
name: 'Widget Plugin',
});
},
// highlight-start
bootstrap(app) {
const dashboardPlugin = app.getPlugin('dashboard');
if (dashboardPlugin) {
dashboardPlugin.injectComponent('homePage', 'top', {
name: 'widget-plugin-statistics',
Component: Widget,
});
}
},
// highlight-end
};
TypeScript
import type { StrapiApp } from '@strapi/admin/strapi-admin';
import { Widget } from './components/Widget';
export default {
register(app: StrapiApp) {
app.registerPlugin({
id: 'widget-plugin',
name: 'Widget Plugin',
});
},
// highlight-start
bootstrap(app: StrapiApp) {
const dashboardPlugin = app.getPlugin('dashboard');
if (dashboardPlugin) {
dashboardPlugin.injectComponent('homePage', 'top', {
name: 'widget-plugin-statistics',
Component: Widget,
});
}
},
// highlight-end
};
注入组件参数
injectComponent() 方法接受以下参数:
| Parameter | Type | Description |
|---|---|---|
view | string | 应注入组件的视图名称 |
zone | string | 视图中应注入组件的区域名称 |
component | object | 配置对象,包含 name(唯一字符串)和 Component(要注入的 React 组件) |
内容管理器数据访问
当向内容管理器的注入区注入组件时,可以使用 useContentManagerContext hook 访问编辑视图数据:
JavaScript
import {
unstable_useContentManagerContext as useContentManagerContext,
} from '@strapi/strapi/admin';
export const MyCustomButton = () => {
const {
slug, // Content type slug (e.g., 'api::article.article')
model, // Content type model
id, // Document ID (undefined when creating)
collectionType, // 'single-types' or 'collection-types'
isCreatingEntry, // Whether creating a new entry
isSingleType, // Whether the content type is a single type
hasDraftAndPublish, // Whether draft & publish is enabled
contentType, // Content type schema
components, // Component schemas
layout, // Content type layout
form, // Form state and handlers
} = useContentManagerContext();
const { initialValues, values, onChange } = form;
const handleCustomAction = () => {
onChange({ target: { name: 'customField', value: 'new value' } });
};
return <button onClick={handleCustomAction}>Custom Action</button>;
};
TypeScript
import {
unstable_useContentManagerContext as useContentManagerContext,
} from '@strapi/strapi/admin';
export const MyCustomButton = () => {
const {
slug, // Content type slug (e.g., 'api::article.article')
model, // Content type model
id, // Document ID (undefined when creating)
collectionType, // 'single-types' or 'collection-types'
isCreatingEntry, // Whether creating a new entry
isSingleType, // Whether the content type is a single type
hasDraftAndPublish, // Whether draft & publish is enabled
contentType, // Content type schema
components, // Component schemas
layout, // Content type layout
form, // Form state and handlers
} = useContentManagerContext();
const { initialValues, values, onChange } = form;
const handleCustomAction = () => {
onChange({ target: { name: 'customField', value: 'new value' } });
};
return <button onClick={handleCustomAction}>Custom Action</button>;
};
useContentManagerContext hook 当前以 unstable_useContentManagerContext 形式导出。unstable_ 前缀表示该 API 在未来的版本中可能会发生变化。该 hook 取代了来自 @strapi/helper-plugin 的 已弃用的 useCMEditViewDataManager,后者在 Strapi 5 中已不可用。
最佳实践
-
使用具有描述性的区域名称。 为你的注入区选择清晰的名称(例如
top、bottom、before、after)。 -
检查插件是否可用。 在向某插件的区域注入组件之前,务必验证该插件是否存在:
bootstrap(app) { const targetPlugin = app.getPlugin('target-plugin'); if (targetPlugin) { targetPlugin.injectComponent('view', 'zone', { name: 'my-component', Component: MyComponent, }); } } -
使用唯一的组件名称。 确保组件名称唯一,以避免与其他插件发生冲突。
-
优雅处理缺失的区域。 组件应处理注入区可能不可用的情况。
-
为你的注入区编写文档。 清楚记录你的插件提供了哪些注入区及其预期用途。