# 管理员钩子

> Source: https://strapi.nodejs.cn/cms/plugins-development/admin-hooks

🌐 Admin Panel API: Hooks

Hooks API 允许插件创建扩展点（`createHook` 在 `register` 中）并订阅它们（`registerHook` 在 `bootstrap` 中）。Hooks 可以串行、瀑布式或并行运行。Strapi 为内容管理器的列表和编辑视图包含了预定义的 hooks。

🌐 The Hooks API lets plugins create extension points (`createHook` in `register`) and subscribe to them (`registerHook` in `bootstrap`). Hooks run in series, waterfall, or parallel. Strapi includes predefined hooks for the Content Manager's List and Edit views.

Hooks API 允许插件创建和注册钩子，即应用中插件可以添加个性化行为的位置。

🌐 The Hooks API allows a plugin to create and register hooks, i.e. places in the application where plugins can add personalized behavior.

:::prerequisites

在深入了解本页的概念之前，请确保你已经：

🌐 Before diving deeper into the concepts on this page, please ensure you have:

- [创建了一个 Strapi 插件](/cms/plugins-development/create-a-plugin)，
- 已阅读并理解了 [管理面板 API](/cms/plugins-development/admin-panel-api) 的基础知识

:::

## 创建钩子 {#creating-hooks}

🌐 Creating hooks

在[`register`](/cms/plugins-development/admin-panel-api#register)生命周期期间使用`createHook()`创建钩子扩展点。这表明你的插件提供了一个其他插件可以订阅的扩展点。

🌐 Create hook extension points with `createHook()` during the [`register`](/cms/plugins-development/admin-panel-api#register) lifecycle. This declares that your plugin provides an extension point that other plugins can subscribe to.

```js title="my-plugin/admin/src/index.js"

  register(app) {
    app.createHook('My-PLUGIN/MY_HOOK');
  },
};
```
```ts title="my-plugin/admin/src/index.ts"

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

:::note

为了插件之间可预测的互操作性，请使用稳定的命名空间钩子 ID，例如 `my-plugin/my-hook`。

🌐 For predictable interoperability between plugins, use stable namespaced hook IDs such as `my-plugin/my-hook`.

:::

## 订阅钩子 {#subscribing-to-hooks}

🌐 Subscribing to hooks

在所有插件加载完成后，在[`bootstrap`](/cms/plugins-development/admin-panel-api#bootstrap)生命周期中使用`registerHook()`订阅钩子。回调函数会接收来自钩子调用者的参数，并应返回（可选修改的）数据。

🌐 Subscribe to hooks with `registerHook()` during the [`bootstrap`](/cms/plugins-development/admin-panel-api#bootstrap) lifecycle, once all plugins are loaded. The callback receives arguments from the hook caller and should return the (optionally mutated) data.

```js title="my-other-plugin/admin/src/index.js"

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

      // important: return the mutated data
      return args;
    });
  },
};
```
```ts title="my-other-plugin/admin/src/index.ts"

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

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

异步回调也被支持：

🌐 Async callbacks are also supported:

```js title="my-other-plugin/admin/src/index.js"

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

      // always return data for waterfall hooks
      return enrichedData;
    });
  },
};
```
```ts title="my-other-plugin/admin/src/index.ts"

  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;
    });
  },
};
```

## 运行钩子 {#running-hooks}

🌐 Running hooks

钩子可以在三种模式下运行：

🌐 Hooks can be run in 3 modes:

| 模式 | 功能 | 返回值 |
|---|---|---|
| 串行 | `runHookSeries` | 每个函数的结果数组，按顺序 |
| 并行 | `runHookParallel` | 已解决的 Promise 结果数组，按顺序 |
| 瀑布 | `runHookWaterfall` | 应用所有转换后得到的单个值 |

:::caution

对于 `runHookWaterfall`，每个订阅者必须返回转换后的值，以便链中的下一个订阅者接收它。不返回值将会打断链。

🌐 For `runHookWaterfall`, each subscriber must return the transformed value so that the next subscriber in the chain receives it. Failing to return a value will break the chain.

:::

## 使用预定义的钩子 {#using-predefined-hooks}

🌐 Using predefined hooks

Strapi 包含为内容管理器的列表和编辑视图预定义的钩子。

🌐 Strapi includes predefined hooks for the Content Manager's List and Edit views.

### `INJECT-COLUMN-IN-TABLE`

`Admin/CM/pages/ListView/inject-column-in-table` 钩子可以在 [内容管理器](/cms/intro) 的列表视图中添加或修改列：

🌐 The `Admin/CM/pages/ListView/inject-column-in-table` hook can add or mutate columns in the List View of the [Content Manager](/cms/intro):

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

以下示例订阅此钩子以添加自定义“外部 ID”列：

🌐 The following example subscribes to this hook to add a custom "External id" column:

```js title="my-plugin/admin/src/index.js"

  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,
        };
      }
    );
  },
};
```
```ts title="my-plugin/admin/src/index.ts"

  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 类型定义**：

<ExpandableContent>

```tsx
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` 钩子可以修改 [内容管理器](/cms/intro) 的编辑视图布局。

🌐 The `Admin/CM/pages/EditView/mutate-edit-view-layout` hook can mutate the Edit View layout of the [Content Manager](/cms/intro).

下面的示例订阅这个钩子以将所有字段强制为全宽：

🌐 The following example subscribes to this hook to force all fields to full width:

```js title="my-plugin/admin/src/index.js"

  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,
        };
      }
    );
  },
};
```
```ts title="my-plugin/admin/src/index.ts"

  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 类型定义:**

<ExpandableContent>

```tsx
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` 钩子（参见 [source code](https://github.com/strapi/strapi/blob/develop/packages/core/content-manager/admin/src/hooks/useDocumentLayout.ts)）。内部包命名可能有所不同，但插件作者应依赖本页中公开的 `EditLayout` 和 `ListLayout` 形状。

:::
