控制器 Controllers

控制器是 JavaScript 文件,其中包含一组方法(称为操作),客户端可根据请求的路由访问这些方法。每当客户端请求路由时,操作都会执行业务逻辑代码并发回响应。控制器代表模型-视图-控制器 (MVC) 模式中的 C。

在大多数情况下,控制器将包含项目的大部分业务逻辑。但随着控制器的逻辑变得越来越复杂,使用 服务 将代码组织成可重复使用的部分是一种很好的做法。

简化的 Strapi 后端图,其中突出显示了控制器

该图表示请求如何通过 Strapi 后端的简化版本,其中突出显示了控制器。后端自定义介绍页面包含一个完整的交互式图表

在决定自定义核心控制器之前,请考虑创建自定义路由中间件(请参阅路由文档)。

实施

可以手动生成或添加 控制器。 Strapi 提供了一个 createCoreController 工厂函数,可以自动生成核心控制器并允许构建自定义控制器或 扩展或替换生成的控制器

添加新控制器

可以通过以下方式实现新控制器:

  • 使用 交互式 CLI 命令 strapi generate
  • 或手动创建 JavaScript 文件:
  • 对于 API 控制器,位于 ./src/api/[api-name]/controllers/(此位置很重要,因为 Strapi 会从那里自动加载控制器)
  • 或对于插件控制器,位于 ./src/plugins/[plugin-name]/server/controllers/ 等文件夹中,但只要在 strapi-server.js 文件中正确导出插件接口,它们就可以在其他地方创建(请参阅 插件服务器 API 文档
js
Javascript
js
// ./src/api/restaurant/controllers/restaurant.js
const { createCoreController } = require('@strapi/strapi').factories

module.exports = createCoreController(
  'api::restaurant.restaurant',
  ({ strapi }) => ({
    /**
     * 示例 1:修改 Strapi 控制器函数
     *
     * 如果需要修改预定义 Strapi 控制器方法的输入或输出,
     * 编写一个同名的方法,并使用“super”调用父方法。
     */
    async find(ctx) {
      // 用于修改输入的自定义逻辑
      ctx.query = { ...ctx.query, locale: 'en' } // 无论请求的内容如何,强制将 ctx.query.locale 设置为“en”

      // 调用默认父控制器操作
      const result = await super.find(ctx)

      // 用于修改输出的自定义逻辑
      result.meta.date = Date.now() // 更改返回的日期

      return result
    },

    /**
     * 示例 2:替换 Strapi 控制器函数
     *
     * 如果您需要完全替换预定义 Strapi 控制器方法的行为,
     * 您只需实现同名方法即可。
     *
     * 注意:您需要自行管理请求和结果的安全性,
     * 如本例所示。
     */
    // eslint-disable-next-line no-dupe-keys
    async find(ctx) {
      // 如果任何查询参数无法被 ctx.user 访问,validateQuery 会抛出错误
      // 即尝试访问私有字段、用户无权访问的字段、错误的数据类型等
      await this.validateQuery(ctx)

      // sanitizeQuery 会默默删除任何无效或用户无权访问的查询参数
      // 即使使用了validateQuery,也建议使用 sanitizeQuery,因为validateQuery 允许
      // 传递许多与安全无关的情况,例如字符串字段中的空对象,而 sanitizeQuery
      // 会完全删除它们
      const sanitizedQueryParams = await this.sanitizeQuery(ctx)

      // 执行所需的任何自定义操作
      const { results, pagination } = await strapi
        .service('api::restaurant.restaurant')
        .find(sanitizedQueryParams)

      // sanitizeOutput 会删除我们的查询返回的任何数据ctx.user 不应具有以下权限
      const sanitizedResults = await this.sanitizeOutput(results, ctx)

      // transformResponse 正确格式化结果的数据和元字段以返回到 API
      return this.transformResponse(sanitizedResults, { pagination })
    },

    /**
     * 示例 3:编写您自己的新控制器函数
     * 如果您需要创建一些与预配置的 Strapi 方法之一不匹配的新操作,
     * 您只需添加具有所需名称的方法并实现您想要的任何功能即可。
     *
     * 注意:与替换控制器类似,您需要自己管理请求的安全性,
     * 因此请记住根据需要使用清理器和验证器。
     */
    async healthCheck(ctx) {
      ctx.body = 'ok'
    },
  })
)

每个控制器操作都可以是 asyncsync 函数。 每个操作都接收一个上下文对象 (ctx) 作为参数。ctx 包含 请求上下文响应上下文

示例:GET /hello路由调用基本控制器

定义了一个特定的 GET /hello 路由,路由器文件的名称(即 index)用于调用控制器处理程序(即 index)。每次将 GET /hello 请求发送到服务器时,Strapi 都会调用 hello.js 控制器中的 index 操作,该操作返回 Hello World!

js
JavaScript
js
// ./src/api/hello/routes/hello.js
module.exports = {
  routes: [
    {
      method: 'GET',
      path: '/hello',
      handler: 'hello.index',
    },
  ],
}
// ./src/api/hello/controllers/hello.js
module.exports = {
  async index(ctx, next) {
    // called by GET /hello
    ctx.body = 'Hello World!' // we could also send a JSON
  },
}

当创建新的 content-type 时,Strapi 会构建一个带有占位符代码的通用控制器,随时可以进行自定义。

  • 要查看自定义控制器的可能高级用法,请阅读后端自定义示例手册的 服务和控制器 页面。
  • 如果您想对控制器实施单元测试,这篇 博客文章 应该可以满足您的要求。

控制器中的清理和验证

清理意味着对象被“清理”并返回。

验证意味着断言数据已经干净,如果发现不应该存在的内容,则会抛出错误。

在 Strapi 中:

  • 验证应用于查询参数,
  • 并且仅对输入数据(创建和更新正文数据)进行清理。

强烈建议您使用新的 sanitizeQueryvalidateQuery 函数清理(v4.8.0+)和/或验证(v4.13.0+)传入的请求查询,以防止泄露私人数据。

使用控制器工厂时的清理

在 Strapi 工厂中,公开了以下可用于清理和验证的函数:

函数名称参数说明
sanitizeQueryctx清理请求查询
sanitizeOutputentity/entities, ctx清理输出数据,其中 entity/entities 应该是对象或数据数组
sanitizeInputdata, ctx清理输入数据
validateQueryctx验证请求查询(无效参数抛出错误)
validateInputdata, ctx(实验)验证输入数据(无效数据抛出错误)

这些函数会自动从模型继承清理设置,并根据内容类型架构和任何内容 API 身份验证策略(例如用户和权限插件或 API 令牌)相应地清理数据。

由于这些方法使用与当前控制器关联的模型,因此如果您查询来自另一个模型的数据(即在“餐厅”控制器方法中查找“菜单”),则必须改用 @strapi/utils 工具,例如 清理自定义控制器 中描述的 sanitize.contentAPI.query,否则您的查询结果将针对错误的模型进行清理。

js
JavaScript
js
// ./src/api/restaurant/controllers/restaurant.js
const { createCoreController } = require('@strapi/strapi').factories

module.exports = createCoreController(
  'api::restaurant.restaurant',
  ({ strapi }) => ({
    async find(ctx) {
      await this.validateQuery(ctx)
      const sanitizedQueryParams = await this.sanitizeQuery(ctx)
      const { results, pagination } = await strapi
        .service('api::restaurant.restaurant')
        .find(sanitizedQueryParams)
      const sanitizedResults = await this.sanitizeOutput(results, ctx)

      return this.transformResponse(sanitizedResults, { pagination })
    },
  })
)

构建自定义控制器时的清理和验证 {#sanitize-validate-custom-controllers}

在自定义控制器中,通过 @strapi/utils 包公开了 5 个主要函数,可用于清理和验证:

函数名称参数说明
sanitize.contentAPI.inputdataschemaauth清理请求输入,包括不可写字段、删除受限关系和插件添加的其他嵌套“访问者”
sanitize.contentAPI.outputdataschemaauth清理响应输出,包括受限关系、私有字段、密码和插件添加的其他嵌套“访问者”
sanitize.contentAPI.queryctx.queryschemaauth清理请求查询,包括过滤器、排序、字段和填充
validate.contentAPI.queryctx.queryschemaauth验证请求查询,包括过滤器、排序、字段(当前不填充)
validate.contentAPI.inputdataschemaauth(实验性)验证请求输入,包括不可写字段、删除受限关系和插件添加的其他嵌套“访问者”

根据自定义控制器的复杂性,您可能需要额外的清理,而 Strapi 目前无法考虑到这一点,尤其是在组合来自多个来源的数据时。

js
JavaScript
js
// ./src/api/restaurant/controllers/restaurant.js
const { sanitize, validate } = require('@strapi/utils')

module.exports = {
  async findCustom(ctx) {
    const contentType = strapi.contentType('api::test.test')
    await validate.contentAPI.query(ctx.query, contentType, {
      auth: ctx.state.auth,
    })
    const sanitizedQueryParams = await sanitize.contentAPI.query(
      ctx.query,
      contentType,
      { auth: ctx.state.auth }
    )

    const entities = await strapi.entityService.findMany(
      contentType.uid,
      sanitizedQueryParams
    )

    return await sanitize.contentAPI.output(entities, contentType, {
      auth: ctx.state.auth,
    })
  },
}

扩展核心控制器

为每种内容类型创建默认控制器和操作。这些默认控制器用于返回对 API 请求的响应(例如,当访问 GET /api/articles/3 时,将调用“Article”内容类型的默认控制器的 findOne 操作)。可以自定义默认控制器以实现您自己的逻辑。以下代码示例应该可以帮助您入门。

可以通过创建自定义操作 并将操作命名为与原始操作相同的名称(例如 findfindOnecreateupdatedelete)来完全替换核心控制器中的操作。

扩展核心控制器时,您无需重新实现任何清理,因为它已经由您正在扩展的核心控制器处理。如果可能,强烈建议扩展核心控制器,而不是创建自定义控制器。

集合类型示例

后端自定义示例手册 展示了如何覆盖默认控制器操作,例如 create 操作

js
find()
js
async find(ctx) {
  // some logic here
  const { data, meta } = await super.find(ctx);
  // some more logic

  return { data, meta };
}

Single type examples

js
find()
js
async find(ctx) {
  // some logic here
  const response = await super.find(ctx);
  // some more logic

  return response;
}

Usage

控制器被声明并附加到路由。当路由被调用时,控制器会自动调用,因此通常不需要显式调用控制器。但是,服务 可以调用控制器,在这种情况下应使用以下语法:

js
// 访问 API 控制器
strapi.controller('api::api-name.controller-name')
// 访问插件控制器
strapi.controller('plugin::plugin-name.controller-name')

要列出所有可用的控制器,请运行 yarn strapi controllers:list