服务端 API:Getters 与用法(Getters & usage)

页面摘要: 通过顶层 getter(strapi.plugin('my-plugin').service('name'))或全局 getter(strapi.service('plugin::my-plugin.name'))访问插件资源。两者返回相同的对象。在你自己的插件内部使用顶层 getter,在应用代码或其他插件中使用全局 getter。路由没有对应的全局 getter。配置使用专门的配置 API。

插件的服务端资源(如控制器、服务、策略、中间件和内容类型)可以通过 strapi 实例从任何服务端位置访问:其他插件、生命周期 hook、应用控制器或自定义脚本。路由和配置使用专门的 API —— 请参阅下面的 getter 参考。

WARNING

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

Getter 风格

Strapi 提供 2 种风格来访问插件资源。两者返回相同的底层对象,区别纯粹在于语法。

顶层 getter 通过插件名链式调用:

strapi.plugin('plugin-name').service('service-name')
strapi.plugin('plugin-name').controller('controller-name')

全局 getter 直接在 strapi 实例上使用完整的 UID:

strapi.service('plugin::plugin-name.service-name')
strapi.controller('plugin::plugin-name.controller-name')

选择取决于上下文和可读性:

  • 在你自己的插件内部,顶层 getter 更简洁,并使插件边界更加明确。
  • 从应用代码或另一个插件中,全局 getter 与 api:: UID 一起阅读起来更自然。

有 2 个资源是例外:

  • 路由(strapi.plugin('plugin-name').routes)没有对应的全局 getter,
  • 配置使用专门的配置 API(strapi.plugin('plugin-name').config() 和 strapi.config.get(...))而不是资源 getter。

完整 getter 参考

下表列出了名为 todo 的插件中名为 task 的资源的所有可用 getter:

Top-levelGlobal
Servicestrapi.plugin('todo').service('task')strapi.service('plugin::todo.task')
Controllerstrapi.plugin('todo').controller('task')strapi.controller('plugin::todo.task')
Content-typestrapi.plugin('todo').contentType('task')strapi.contentType('plugin::todo.task')
Policystrapi.plugin('todo').policy('is-owner')strapi.policy('plugin::todo.is-owner')
Middlewarestrapi.plugin('todo').middleware('audit-log')strapi.middleware('plugin::todo.audit-log')
Routesstrapi.plugin('todo').routes—
Configurationstrapi.plugin('todo').config('featureFlag')strapi.config.get('plugin::todo.featureFlag')

两种风格返回相同的底层对象。路由没有对应的全局 getter。配置使用专门的配置 API 而不是资源 getter,两种形式读取的都是相同的合并后的值。

TIP

运行 yarn strapi console 或 npm run strapi console,在实时控制台中检查 strapi 对象,并以交互方式探索可用的插件及其资源。

用法示例

从控制器调用插件服务

最常见的模式:控制器委托给其自己插件的服务:

JavaScript

'use strict';

module.exports = ({ strapi }) => ({
  async find(ctx) {
    // highlight-next-line
    const tasks = await strapi.plugin('todo').service('task').findAll(); // top-level getter: preferred inside your own plugin
    ctx.body = tasks;
  },

  async create(ctx) {
    const task = await strapi
      .plugin('todo')
      .service('task')
      .create(ctx.request.body);
    ctx.status = 201;
    ctx.body = task;
  },
});

TypeScript

import type { Context } from 'koa';
import type { Core } from '@strapi/strapi';

type TaskService = {
  findAll(): Promise<unknown[]>;
  create(data: unknown): Promise<unknown>;
};

export default ({ strapi }: { strapi: Core.Strapi }) => ({
  async find(ctx: Context) {
    // Narrow cast: plugin services require app-level type augmentation for full typing.
    const tasks = await (strapi.plugin('todo').service('task') as TaskService).findAll();
    ctx.body = tasks;
  },

  async create(ctx: Context) {
    const task = await (strapi.plugin('todo').service('task') as TaskService).create(
      (ctx.request as any).body
    );
    (ctx as any).status = 201;
    ctx.body = task;
  },
});

从 bootstrap 调用插件服务

在 bootstrap() 中调用的服务可以访问完整的 strapi 实例,包括其他插件的服务:

JavaScript

'use strict';

module.exports = async ({ strapi }) => {
  // Call own plugin service to seed initial data
  const count = await strapi.plugin('todo').service('task').count();

  if (count === 0) {
    await strapi.plugin('todo').service('task').create({
      title: 'Welcome task',
      done: false,
    });
  }
};

TypeScript

import type { Core } from '@strapi/strapi';

type TaskService = {
  count(): Promise<number>;
  create(data: unknown): Promise<unknown>;
};

export default async ({ strapi }: { strapi: Core.Strapi }) => {
  // Narrow cast: plugin services are resolved dynamically unless your project augments Strapi service typings.
  const taskService = strapi.plugin('todo').service('task') as TaskService;
  // highlight-next-line
  const count = await taskService.count();

  if (count === 0) {
    await taskService.create({ title: 'Welcome task', done: false });
  }
};

跨插件调用或从应用代码调用

从应用级控制器或服务(插件外部),或从另一个插件调用时,使用完整 UID 的全局 getter 通常更清晰:

JavaScript

'use strict';

const { createCoreController } = require('@strapi/strapi').factories;

module.exports = createCoreController('api::project.project', ({ strapi }) => ({
  async create(ctx) {
    const { data, meta } = await super.create(ctx);

    // highlight-next-line
    await strapi.service('plugin::todo.task').create({ // global getter: preferred in application code
      title: `Review project: ${data.attributes.name}`,
      done: false,
    });

    return { data, meta };
  },
}));

TypeScript

import { factories } from '@strapi/strapi';

type TaskService = {
  create(data: unknown): Promise<unknown>;
};

export default factories.createCoreController(
  'api::project.project',
  ({ strapi }) => ({
    async create(ctx: any) {
      const { data, meta } = await super.create(ctx);

      // highlight-next-line
      // Narrow cast: this generic documentation cannot infer your app-specific service signatures.
      await (strapi.service('plugin::todo.task') as TaskService).create({
        title: `Review project: ${data.attributes.name}`,
        done: false,
      });

      return { data, meta };
    },
  })
);

在运行时读取插件配置

// Read a single key
const maxItems = strapi.plugin('todo').config('maxItems');
// Read the full config object
const todoConfig = strapi.config.get('plugin::todo');
// Read a nested key
const endpoint = strapi.config.get('plugin::todo.endpoint');
NOTE

strapi.plugin('my-plugin').config('key') 读取合并后的配置(在插件默认值之上应用了用户覆盖)。这是在插件代码内部读取配置的首选方式。有关插件配置如何声明和合并的信息,请参阅 服务端配置。

访问内容类型 schema

当你需要 schema 对象时(例如将其传递给清理 API),请使用内容类型 getter:

JavaScript

// Access the content-type schema
const schema = strapi.contentType('plugin::todo.task');

const sanitizedOutput = await strapi.contentAPI.sanitize.output(
  data,
  schema,
  { auth: ctx.state.auth }
);

TypeScript

// highlight-next-line
const schema = strapi.contentType('plugin::todo.task'); // access the content-type schema

const sanitizedOutput = await strapi.contentAPI.sanitize.output(
  data,
  schema,
  { auth: ctx.state.auth }
);

常见错误

  • 路由处理程序与控制器键之间的命名不匹配。 如果你的路由声明了 handler: 'task.find',你的 controllers index 必须导出一个名为 task 的键,并且该控制器必须有一个名为 find 的方法。不匹配会在路由匹配时抛出运行时错误。

  • 误用策略上下文参数。 策略函数的第一个参数是一个策略上下文对象,而不是原始的 Koa ctx。它封装了请求上下文,但暴露了不同的接口。在代码中将其命名为 ctx 不会导致错误,但将其当作 Koa 上下文(例如调用 ctx.body 或 ctx.status)将无法按预期工作。使用 policyContext.state 访问 auth 状态,并调用 return false 或抛出 PolicyError 来阻止请求。

  • 在模块加载时调用服务。 在模块首次加载时,strapi 对象尚未初始化。务必在函数体内调用 getter。切勿在模块文件的顶层调用它们。

  • 在全局 getter 中使用不完整的 UID。 strapi.service('todo.task') 不是有效的插件 UID。请使用完整的 plugin::todo.task 形式。如果没有正确的命名空间,服务调用会在运行时失败或返回 undefined。

    ScopeExample UID
    Plugin serviceplugin::todo.task
    API serviceapi::project.project

最佳实践

  • 在你自己的插件内部优先使用顶层 getter。 当两者都在同一个插件内部时,strapi.plugin('my-plugin').service('task') 比全局形式更具可读性。

  • 在应用代码和跨插件调用中使用全局 getter。 当从 src/api/ 或从另一个插件调用时,完整的 UID plugin::todo.task 使依赖关系明确且更易于搜索。

  • 在服务中访问服务,而不是在声明时。 避免在模块初始化时于闭包中捕获服务引用。始终在调用时通过 getter 解析它们,以确保 Strapi 已完全加载。