控制器
🌐 Controllers
Page summary:
控制器将处理业务逻辑的操作打包在 Strapi 的 MVC 模式中的每个路由下。本文档演示了如何生成控制器、使用
createCoreController扩展核心控制器,以及将繁重的逻辑委托给服务。
控制器是包含一组方法(称为动作)的 JavaScript 文件,客户端可以根据请求的路由访问这些方法。每当客户端请求该路由时,动作会执行业务逻辑代码并返回响应。控制器代表模型-视图-控制器(MVC)模式中的 C。
🌐 Controllers are JavaScript files that contain a set of methods, called actions, reached by the client according to the requested route. Whenever a client requests the route, the action performs the business logic code and sends back the response. Controllers represent the C in the model-view-controller (MVC) pattern.
在大多数情况下,控制器将包含项目大部分的业务逻辑。但随着控制器的逻辑变得越来越复杂,使用服务将代码组织成可重用的部分是一种好习惯。
🌐 In most cases, the controllers will contain the bulk of a project's business logic. But as a controller's logic becomes more and more complicated, it's a good practice to use services to organize the code into re-usable parts.

在重写核心操作时,始终验证和清理查询和响应,以避免泄露私有字段或绕过访问规则。在从自定义操作返回数据之前,使用 validateQuery(可选)、sanitizeQuery(推荐)和 sanitizeOutput。请参见下面的示例,了解安全的 find 重写。
🌐 When overriding core actions, always validate and sanitize queries and responses to avoid leaking private fields or bypassing access rules. Use validateQuery (optional), sanitizeQuery (recommended), and sanitizeOutput before returning data from custom actions. See the example below for a safe find override.
实现
🌐 Implementation
控制器可以生成或手动添加。Strapi 提供了一个 createCoreController 工厂函数,可以自动生成核心控制器,并允许构建自定义控制器或扩展或替换生成的控制器。
🌐 Controllers can be generated or added manually. Strapi provides a createCoreController factory function that automatically generates core controllers and allows building custom ones or extend or replace the generated controllers.
添加新控制器
🌐 Adding a new controller
可以实现一个新的控制器:
🌐 A new controller can be implemented:
- 使用 交互式 CLI 命令
strapi generate - 或通过创建 JavaScript 文件手动:
- 在
./src/api/[api-name]/controllers/中用于 API 控制器(此位置很重要,因为 Strapi 会从这里自动加载控制器) - 或者在像
./src/plugins/[plugin-name]/server/controllers/这样的文件夹中用于插件控制器,尽管它们可以创建在其他地方,只要插件接口在strapi-server.js文件中正确导出(参见 插件的服务器 API 文档)
- 在
- JavaScript
- TypeScript
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::restaurant.restaurant', ({ strapi }) => ({
// Method 1: Creating an entirely custom action
async exampleAction(ctx) {
try {
ctx.body = 'ok';
} catch (err) {
ctx.body = err;
}
},
// Method 2: Wrapping a core action (leaves core logic in place)
async find(ctx) {
// some custom logic here
ctx.query = { ...ctx.query, local: 'en' }
// Calling the default core action
const { data, meta } = await super.find(ctx);
// some more custom logic
meta.date = Date.now()
return { data, meta };
},
// Method 3: Replacing a core action with proper sanitization
async find(ctx) {
// validateQuery (optional)
// to throw an error on query params that are invalid or the user does not have access to
await this.validateQuery(ctx);
// sanitizeQuery to remove any query params that are invalid or the user does not have access to
// It is strongly recommended to use sanitizeQuery even if validateQuery is used
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 });
}
}));
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::restaurant.restaurant', ({ strapi }) => ({
// Method 1: Creating an entirely custom action
async exampleAction(ctx) {
try {
ctx.body = 'ok';
} catch (err) {
ctx.body = err;
}
},
// Method 2: Wrapping a core action (leaves core logic in place)
async find(ctx) {
// some custom logic here
ctx.query = { ...ctx.query, local: 'en' }
// Calling the default core action
const { data, meta } = await super.find(ctx);
// some more custom logic
meta.date = Date.now()
return { data, meta };
},
// Method 3: Replacing a core action with proper sanitization
async find(ctx) {
// validateQuery (optional)
// to throw an error on query params that are invalid or the user does not have access to
await this.validateQuery(ctx);
// sanitizeQuery to remove any query params that are invalid or the user does not have access to
// It is strongly recommended to use sanitizeQuery even if validateQuery is used
const sanitizedQueryParams = await this.sanitizeQuery(ctx);
const { results, pagination } = await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams);
// sanitizeOutput to ensure the user does not receive any data they do not have access to
const sanitizedResults = await this.sanitizeOutput(results, ctx);
return this.transformResponse(sanitizedResults, { pagination });
}
}));
每个控制器动作可以是 async 或 sync 函数。每个动作都接收一个上下文对象(ctx)作为参数。ctx 包含 请求上下文 和 响应上下文。
🌐 Each controller action can be an async or sync function.
Every action receives a context object (ctx) as a parameter. ctx contains the request context and the response context.
示例:GET /hello 路由调用一个基本控制器
定义了一个特定的 GET /hello 路由,路由文件的名称(即 index)用于调用控制器处理程序(即 index)。每次向服务器发送 GET /hello 请求时,Strapi 会在 hello.js 控制器中调用 index 操作,并返回 Hello World!:
🌐 A specific GET /hello route is defined, the name of the router file (i.e. index) is used to call the controller handler (i.e. index). Every time a GET /hello request is sent to the server, Strapi calls the index action in the hello.js controller, which returns Hello World!:
- JavaScript
- TypeScript
module.exports = {
routes: [
{
method: 'GET',
path: '/hello',
handler: 'api::hello.hello.index',
}
]
}
module.exports = {
async index(ctx, next) { // called by GET /hello
ctx.body = 'Hello World!'; // we could also send a JSON
},
};
export default {
routes: [
{
method: 'GET',
path: '/hello',
handler: 'api::hello.hello.index',
}
]
}
export default {
async index(ctx, next) { // called by GET /hello
ctx.body = 'Hello World!'; // we could also send a JSON
},
};
当创建一个新的 内容类型 时,Strapi 会生成一 个带有占位代码的通用控制器,准备进行自定义。
🌐 When a new content-type is created, Strapi builds a generic controller with placeholder code, ready to be customized.
要了解自定义控制器可能的高级用法,请阅读后端自定义示例手册的 服务和控制器 页面。
🌐 To see a possible advanced usage for custom controllers, read the services and controllers page of the backend customization examples cookbook.
控制器与路由:路由如何到达控制器动作
🌐 Controllers & Routes: How routes reach controller actions
- 核心映射是自动补齐的:当你生成一个内容类型时,Strapi 会创建匹配的控制器和一个已经针对标准操作(
find、findOne、create、update和delete)的路由文件。在生成的控制器中覆盖这些操作中的任何一个不需要修改路由——路由保持相同的处理程序字符串并执行你更新后的逻辑。 - 添加路由应仅针对新的操作或路径进行。如果引入一个全新的方法,例如
exampleAction,则创建或更新一个路由条目, 其handler指向该操作,以便 HTTP 请求可以访问它。使用完整限定的处理程序语法<scope>::<api-or-plugin-name>.<controllerName>.<actionName>(例如 API 控制器的api::restaurant.restaurant.exampleAction或插件控制器的plugin::menus.menu.exampleAction)。 - 关于控制器和路由文件名:默认的控制器名称来自
./src/api/[api-name]/controllers/中的文件名。使用createCoreRouter创建的核心路由采用相同的名称,因此生成的处理程序字符串会自动匹配。自定义路由可以遵循任何文件命名方案,只要handler字符串引用一个导出的控制器操作。
以下示例添加了一个新的控制器操作,并通过自定义路由将其公开,而不会重复现有的 CRUD 路由定义:
🌐 The example below adds a new controller action and exposes it through a custom route without duplicating the existing CRUD route definitions:
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::restaurant.restaurant', ({ strapi }) => ({
async exampleAction(ctx) {
const specials = await strapi.service('api::restaurant.restaurant').find({ filters: { isSpecial: true } });
return this.transformResponse(specials.results);
},
}));
module.exports = {
routes: [
{
method: 'GET',
path: '/restaurants/specials',
handler: 'api::restaurant.restaurant.exampleAction',
},
],
};
控制器中的清理和验证
🌐 Sanitization and Validation in controllers
强烈建议你使用新的 sanitizeQuery 和 validateQuery 函数对传入的请求查询进行消毒(v4.8.0+)和/或验证(v4.13.0+),以防止私有数据泄露。
🌐 It's strongly recommended you sanitize (v4.8.0+) and/or validate (v4.13.0+) your incoming request query utilizing the new sanitizeQuery and validateQuery functions to prevent the leaking of private data.
清理意味着对象被“清理”并返回。
🌐 Sanitization means that the object is “cleaned” and returned.
验证意味着断言数据已经干净,如果发现不应该存在的内容,则会引发错误。
🌐 Validation means an assertion is made that the data is already clean and throws an error if something is found that shouldn't be there.
在 Strapi 5 中,查询参数和输入数据(即创建和更新的请求体数据)都会被验证。任何包含以下无效输入的创建和更新数据请求都会抛出 400 Bad Request 错误:
🌐 In Strapi 5, both query parameters and input data (i.e., create and update body data) are validated. Any create and update data requests with the following invalid input will throw a 400 Bad Request error:
- 用户无权创建的关系
- 模式中不存在的无法识别的值
- 不可写字段和内部时间戳,如
createdAt和createdBy字段 - 设置或更新
id字段(连接关系除外)
使用控制器工厂时的消毒
🌐 Sanitization when utilizing controller factories
在 Strapi 工厂中,公开了以下可用于清理和验证的函数:
🌐 Within the Strapi factories the following functions are exposed that can be used for sanitization and validation:
| 函数名称 | 参数 | 描述 ||------------------|----------------------------|--------------------------------------------------------------------------------------|| sanitizeQuery | ctx | 清理请求查询 || sanitizeOutput | entity/entities, ctx | 清理输出数据,其中实体/实体集合应为对象或数据数组 || sanitizeInput | data, ctx | 清理输入数据 || validateQuery | ctx | 验证请求查询(在参数无效时抛出错误) || validateInput | data, ctx | (实验性)验证输入数据(在数据无效时抛出错误) |
这些函数自动从模型继承清理设置,并根据内容类型架构和任何内容 API 身份验证策略(例如用户和权限插件或 API 令牌)相应地清理数据。
🌐 These functions automatically inherit the sanitization settings from the model and sanitize the data accordingly based on the content-type schema and any of the content API authentication strategies, such as the Users & Permissions plugin or API tokens.
因为这些方法使用的是与当前控制器关联的模型,如果你查询的数据来自另一个模型(例如,在“restaurant”控制器方法中查找“menus”),你必须改为使用 strapi.contentAPI 方法,例如在 Sanitizing Custom Controllers 中描述的 strapi.contentAPI.sanitize.query,否则你的查询结果将会被错误的模型进行清理。
🌐 Because these methods use the model associated with the current controller, if you query data that is from another model (i.e., doing a find for "menus" within a "restaurant" controller method), you must instead use the strapi.contentAPI methods, such as strapi.contentAPI.sanitize.query described in Sanitizing Custom Controllers, or else the result of your query will be sanitized against the wrong model.
- JavaScript
- TypeScript
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 });
}
}));
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::restaurant.restaurant', ({ strapi }) => ({
async find(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 });
}
}));
构建自定义控制器时的清理和验证
🌐 Sanitization and validation when building custom controllers
在自定义控制器中,Strapi 通过 strapi.contentAPI 提供以下用于清理和验证的函数。要向内容 API 路由(例如在 register 中)添加自定义查询或请求体参数,请参见 自定义内容 API 参数。
🌐 Within custom controllers, Strapi exposes the following functions via strapi.contentAPI for sanitization and validation. To add custom query or body parameters to Content API routes (e.g. in register), see Custom Content API parameters.
| 函数名 | 参数 | 描述 |
|---|---|---|
strapi.contentAPI.sanitize.input | data,schema,auth | 清理请求输入,包括不可写字段,移除受限制的关系,以及其他由插件添加的嵌套“访问者” |
strapi.contentAPI.sanitize.output | data、schema、auth | 清理响应输出,包括受限制的关系、私有字段、密码以及插件添加的其他嵌套“访问者” |
strapi.contentAPI.sanitize.query | ctx.query、schema、auth | 清理请求查询,包括过滤器、排序、字段和填充 |
strapi.contentAPI.validate.query | ctx.query、schema、auth | 验证请求查询,包括过滤器、排序、字段(当前未填充) |
strapi.contentAPI.validate.input | data、schema、auth | (实验性)验证请求输入,包括不可写字段、移除受限关系,以及插件添加的其他嵌套“访问器” |
根据自定义控制器的复杂性,你可能需要 Strapi 目前无法考虑的额外清理,尤其是在组合多个来源的数据时。
🌐 Depending on the complexity of your custom controllers, you may need additional sanitization that Strapi cannot currently account for, especially when combining the data from multiple sources.
- JavaScript
- TypeScript
module.exports = {
async findCustom(ctx) {
const contentType = strapi.contentType('api::test.test');
await strapi.contentAPI.validate.query(ctx.query, contentType, { auth: ctx.state.auth });
const sanitizedQueryParams = await strapi.contentAPI.sanitize.query(ctx.query, contentType, { auth: ctx.state.auth });
const documents = await strapi.documents(contentType.uid).findMany(sanitizedQueryParams);
return await strapi.contentAPI.sanitize.output(documents, contentType, { auth: ctx.state.auth });
}
}
export default {
async findCustom(ctx) {
const contentType = strapi.contentType('api::test.test');
await strapi.contentAPI.validate.query(ctx.query, contentType, { auth: ctx.state.auth });
const sanitizedQueryParams = await strapi.contentAPI.sanitize.query(ctx.query, contentType, { auth: ctx.state.auth });
const documents = await strapi.documents(contentType.uid).findMany(sanitizedQueryParams);
return await strapi.contentAPI.sanitize.output(documents, contentType, { auth: ctx.state.auth });
}
}
扩展核心控制器
🌐 Extending core controllers
每种内容类型都会创建默认的控制器和操作。这些默认控制器用于响应 API 请求(例如,当访问 GET /api/articles/3 时,会调用 “Article” 内容类型默认控制器的 findOne 操作)。默认控制器可以自定义以实现你自己的逻辑。以下代码示例应能帮助你入门。
🌐 Default controllers and actions are created for each content-type. These default controllers are used to return responses to API requests (e.g. when GET /api/articles/3 is accessed, the findOne action of the default controller for the "Article" content-type is called). Default controllers can be customized to implement your own logic. The following code examples should help you get started.
核心控制器的一个操作可以完全通过创建自定义操作来替换,并将该操作命名为与原操作相同的名称(例如 find、findOne、create、update 或 delete)。
🌐 An action from a core controller can be replaced entirely by creating a custom action and naming the action the same as the original action (e.g. find, findOne, create, update, or delete).
在扩展核心控制器时,你无需重新实现任何清理功能,因为这些功能已经由你扩展的核心控制器处理。尽可能强烈建议扩展核心控制器,而不是创建自定义控制器。
🌐 When extending a core controller, you do not need to re-implement any sanitization as it will already be handled by the core controller you are extending. Where possible it's strongly recommended to extend the core controller instead of creating a custom controller.
集合类型示例
后端自定义示例手册展示了如何覆盖默认的控制器操作,例如针对create操作。
🌐 The backend customization examples cookbook shows how you can overwrite a default controller action, for instance for the create action.
- `find()`
- findOne()
- create()
- update()
- delete()
async find(ctx) {
// some logic here
const { data, meta } = await super.find(ctx);
// some more logic
return { data, meta };
}
async findOne(ctx) {
// some logic here
const response = await super.findOne(ctx);
// some more logic
return response;
}
async create(ctx) {
// some logic here
const response = await super.create(ctx);
// some more logic
return response;
}
async update(ctx) {
// some logic here
const response = await super.update(ctx);
// some more logic
return response;
}
async delete(ctx) {
// some logic here
const response = await super.delete(ctx);
// some more logic
return response;
}
单类型示例
- find()
- update()
- delete()
async find(ctx) {
// some logic here
const response = await super.find(ctx);
// some more logic
return response;
}
async update(ctx) {
// some logic here
const response = await super.update(ctx);
// some more logic
return response;
}
async delete(ctx) {
// some logic here
const response = await super.delete(ctx);
// some more logic
return response;
}
使用
🌐 Usage
控制器被声明并附加到一个路由上。当路由被调用时,控制器会自动被调用,因此通常不需要显式调用控制器。但是,服务 可以调用控制器,在这种情况下应使用以下语法:
🌐 Controllers are declared and attached to a route. Controllers are automatically called when the route is called, so controllers usually do not need to be called explicitly. However, services can call controllers, and in this case the following syntax should be used:
// access an API controller
strapi.controller('api::api-name.controller-name');
// access a plugin controller
strapi.controller('plugin::plugin-name.controller-name');
要列出所有可用的控制器,请运行 yarn strapi controllers:list。
🌐 To list all the available controllers, run yarn strapi controllers:list.