管理面板 API:Hooks

页面摘要: Hooks API 让插件能够创建扩展点(在 register 中调用 createHook)并订阅它们(在 bootstrap 中调用 registerHook)。Hook 可以按 series(串行)、waterfall(瀑布流)或 parallel(并行)方式运行。Strapi 为内容管理器的列表视图和编辑视图提供了预定义的 hooks。

Hooks API 允许一个插件创建并注册 hooks,即应用中插件可以添加个性化行为的位置。

WARNING

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

创建 hooks

在 register 生命周期内,使用 createHook() 创建 hook 扩展点。这会声明你的插件提供了一个扩展点,其他插件可以订阅它。

JavaScript

export default {
  register(app) {
    app.createHook('My-PLUGIN/MY_HOOK');
  },
};

TypeScript

import type { StrapiApp } from '@strapi/admin/strapi-admin';

export default {
  register(app: StrapiApp) {
    app.createHook('My-PLUGIN/MY_HOOK');
  },
};
NOTE

为保证插件之间的可预期互操作性,请使用稳定的、带命名空间的 hook ID,例如 my-plugin/my-hook。

订阅 hooks

在 bootstrap 生命周期内(即所有插件都已加载后),使用 registerHook() 订阅 hooks。回调函数会接收到来自 hook 调用方的参数,并应返回(可选地被修改过的)数据。

JavaScript

export default {
  bootstrap(app) {
    app.registerHook('My-PLUGIN/MY_HOOK', (...args) => {
      console.log(args);

      // important: return the mutated data
      return args;
    });
  },
};

TypeScript

import type { StrapiApp } from '@strapi/admin/strapi-admin';

export default {
  bootstrap(app: StrapiApp) {
    app.registerHook('My-PLUGIN/MY_HOOK', (...args: unknown[]) => {
      console.log(args);

      // important: return the mutated data
      return args;
    });
  },
};

异步回调同样受支持:

JavaScript

export default {
  bootstrap(app) {
    app.registerHook('My-PLUGIN/MY_HOOK', async (data) => {
      const enrichedData = await fetchExternalData(data);

      // always return data for waterfall hooks
      return enrichedData;
    });
  },
};

TypeScript

import type { StrapiApp } from '@strapi/admin/strapi-admin';

export default {
  bootstrap(app: StrapiApp) {
    app.registerHook('My-PLUGIN/MY_HOOK', async (data: unknown) => {
      const enrichedData = await fetchExternalData(data);

      // always return data for waterfall hooks
      return enrichedData;
    });
  },
};

运行 hooks

Hook 可以按 3 种模式运行:

ModeFunctionReturn value
SeriesrunHookSeries按顺序返回每个函数结果的数组
ParallelrunHookParallel按顺序返回的已决议 promise 结果数组
WaterfallrunHookWaterfall依次应用所有转换后的单一值
WARNING

对于 runHookWaterfall,每个订阅者都必须返回转换后的值,以便链中的下一个订阅者能够接收到它。未能返回值将导致链中断。

使用预定义 hooks

Strapi 为内容管理器的列表视图和编辑视图提供了预定义的 hooks。

INJECT-COLUMN-IN-TABLE

Admin/CM/pages/ListView/inject-column-in-table hook 可以向 内容管理器 的列表视图添加或修改列:

runHookWaterfall(INJECT_COLUMN_IN_TABLE, {
  displayedHeaders: ListFieldLayout[],
  layout: ListFieldLayout,
});

以下示例订阅此 hook,以添加一个自定义的 “External id” 列:

JavaScript

export default {
  bootstrap(app) {
    app.registerHook(
      'Admin/CM/pages/ListView/inject-column-in-table',
      ({ displayedHeaders, layout }) => {
        return {
          displayedHeaders: [
            ...displayedHeaders,
            {
              attribute: { type: 'custom' },
              label: 'External id',
              name: 'externalId',
              searchable: false,
              sortable: false,
              cellFormatter: (document) => document.externalId,
            },
          ],
          layout,
        };
      }
    );
  },
};

TypeScript

import type { StrapiApp } from '@strapi/admin/strapi-admin';

export default {
  bootstrap(app: StrapiApp) {
    app.registerHook(
      'Admin/CM/pages/ListView/inject-column-in-table',
      ({ displayedHeaders, layout }) => {
        return {
          displayedHeaders: [
            ...displayedHeaders,
            {
              attribute: { type: 'custom' },
              label: 'External id',
              name: 'externalId',
              searchable: false,
              sortable: false,
              cellFormatter: (document) => document.externalId,
            },
          ],
          layout,
        };
      }
    );
  },
};

ListFieldLayout 与 ListLayout 类型定义:

interface ListFieldLayout {
  /**
   * The attribute data from the content-type's schema for the field
   */
  attribute: Attribute.Any | { type: 'custom' };
  /**
   * Typically used by plugins to render a custom cell
   */
  cellFormatter?: (
    data: Document,
    header: Omit<ListFieldLayout, 'cellFormatter'>,
    { collectionType, model }: { collectionType: string; model: string }
  ) => React.ReactNode;
  label: string | MessageDescriptor;
  /**
   * the name of the attribute we use to display the actual name e.g. relations
   * are just ids, so we use the mainField to display something meaninginful by
   * looking at the target's schema
   */
  mainField?: string;
  name: string;
  searchable?: boolean;
  sortable?: boolean;
}

interface ListLayout {
  layout: ListFieldLayout[];
  components?: never;
  metadatas: {
    [K in keyof Contracts.ContentTypes.Metadatas]: Contracts.ContentTypes.Metadatas[K]['list'];
  };
  options: LayoutOptions;
  settings: LayoutSettings;
}

type LayoutOptions = Schema['options'] & Schema['pluginOptions'] & object;

interface LayoutSettings extends Contracts.ContentTypes.Settings {
  displayName?: string;
  icon?: never;
}

MUTATE-EDIT-VIEW-LAYOUT

Admin/CM/pages/EditView/mutate-edit-view-layout hook 可以修改 内容管理器 的编辑视图布局。

以下示例订阅此 hook,以强制所有字段占据整行宽度:

JavaScript

export default {
  bootstrap(app) {
    app.registerHook(
      'Admin/CM/pages/EditView/mutate-edit-view-layout',
      ({ layout, ...rest }) => {
        // Force all fields to full width in the default edit layout
        const updatedLayout = layout.map((rowGroup) =>
          rowGroup.map((row) => row.map((field) => ({ ...field, size: 12 })))
        );

        return {
          ...rest,
          layout: updatedLayout,
        };
      }
    );
  },
};

TypeScript

import type { StrapiApp } from '@strapi/admin/strapi-admin';

export default {
  bootstrap(app: StrapiApp) {
    app.registerHook(
      'Admin/CM/pages/EditView/mutate-edit-view-layout',
      ({ layout, ...rest }) => {
        // Force all fields to full width in the default edit layout
        const updatedLayout = layout.map((rowGroup) =>
          rowGroup.map((row) => row.map((field) => ({ ...field, size: 12 })))
        );

        return {
          ...rest,
          layout: updatedLayout,
        };
      }
    );
  },
};

EditLayout 与 EditFieldLayout 类型定义:

interface EditLayout {
  layout: Array<Array<EditFieldLayout[]>>;
  components: {
    [uid: string]: {
      layout: Array<EditFieldLayout[]>;
      settings: Contracts.Components.ComponentConfiguration['settings'] & {
        displayName?: string;
        icon?: string;
      };
    };
  };
  metadatas: {
    [K in keyof Contracts.ContentTypes.Metadatas]: Contracts.ContentTypes.Metadatas[K]['edit'];
  };
  options: LayoutOptions;
  settings: LayoutSettings;
}

interface EditFieldSharedProps extends Omit<InputProps, 'hint' | 'type'> {
  hint?: string;
  mainField?: string;
  size: number;
  unique?: boolean;
  visible?: boolean;
}

/**
 * Map over all the types in Attribute Types and use that to create a union of new types where the attribute type
 * is under the property attribute and the type is under the property type.
 */
type EditFieldLayout = {
  [K in Attribute.Kind]: EditFieldSharedProps & {
    attribute: Extract<Attribute.Any, { type: K }>;
    type: K;
  };
}[Attribute.Kind];

type LayoutOptions = Schema['options'] & Schema['pluginOptions'] & object;

interface LayoutSettings extends Contracts.ContentTypes.Settings {
  displayName?: string;
  icon?: never;
}
NOTE

此处记录的 EditLayout 与 ListLayout 形状来自 useDocumentLayout hook(见 源代码)。内部包命名可能有所不同,但插件作者应依赖本页面中暴露的 EditLayout 和 ListLayout 形状。