管理面板 API:注入区(Injection zones)

页面摘要: 注入区是管理 UI 中插件可以注入 React 组件的预定义区域。使用 getPlugin('content-manager').injectComponent() 来扩展内置视图,或在 registerPlugin 中通过 injectionZones 定义你自己的区域。

插件可以通过将自定义的 React 组件注入到预定义区域,来扩展并自定义现有的管理面板板块。这样无需修改核心代码,即可为 Strapi 内置界面添加功能。

WARNING

在深入阅读本页概念之前,请确保你已经:

NOTE

注入区在 register 生命周期函数中定义,但组件是在 bootstrap 生命周期函数中注入的。

注入区与内容管理器 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 的内容管理器提供了插件可以使用的预定义注入区:

ViewInjection zoneLocation
List viewlistView.actions在筛选器与齿轮图标之间
List viewlistView.publishModalAdditionalInfos发布确认弹窗中的信息内容
List viewlistView.unpublishModalAdditionalInfos取消发布确认弹窗中的信息内容
List viewlistView.deleteModalAdditionalInfos删除确认弹窗中的信息内容
Edit vieweditView.right-links在 “Configure the view” 与 “Edit” 按钮之间
Edit vieweditView.informations在编辑视图的信息框中(内部使用,见下方说明)
Previewpreview.actions在预览视图的操作区域中

listView.*ModalAdditionalInfos 区域用于在发布、取消发布和删除确认弹窗中丰富所显示的信息内容。

WARNING

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() 方法接受以下参数:

ParameterTypeDescription
viewstring应注入组件的视图名称
zonestring视图中应注入组件的区域名称
componentobject配置对象,包含 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>;
};
WARNING

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,
        });
      }
    }
    
  • 使用唯一的组件名称。 确保组件名称唯一,以避免与其他插件发生冲突。

  • 优雅处理缺失的区域。 组件应处理注入区可能不可用的情况。

  • 为你的注入区编写文档。 清楚记录你的插件提供了哪些注入区及其预期用途。