控制器
¥Controllers
控制器是 JavaScript 文件,其中包含一组称为操作的方法,客户端根据请求的 route。每当客户端请求路由时,该操作都会执行业务逻辑代码并发回 response。控制器代表模型-视图-控制器 (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.
在大多数情况下,控制器将包含项目的大部分业务逻辑。但随着控制器的逻辑变得越来越复杂,使用 services 将代码组织成可重用的部分是一个很好的做法。
¥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.
执行
¥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:
-
¥with the interactive CLI command
strapi generate
-
或通过创建 JavaScript 文件手动:
¥or manually by creating a JavaScript file:
-
在
./src/api/[api-name]/controllers/
中用于 API 控制器(此位置很重要,因为 Strapi 从那里自动加载控制器)¥in
./src/api/[api-name]/controllers/
for API controllers (this location matters as controllers are auto-loaded by Strapi from there) -
或者在插件控制器的
./src/plugins/[plugin-name]/server/controllers/
这样的文件夹中,尽管只要插件接口在strapi-server.js
文件中正确导出(请参阅 插件服务器 API 文档),它们就可以在其他地方创建¥or in a folder like
./src/plugins/[plugin-name]/server/controllers/
for plugin controllers, though they can be created elsewhere as long as the plugin interface is properly exported in thestrapi-server.js
file (see Server API for Plugins documentation)
-
- 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.
Example: GET /hello route calling a basic controller
定义了具体的 GET /hello
route,路由文件的名称(即 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: '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: 'hello.index',
}
]
}
export default {
async index(ctx, next) { // called by GET /hello
ctx.body = 'Hello World!'; // we could also send a JSON
},
};
当创建新的 content-type 时,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.
控制器中的清理和验证
¥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:
-
用户无权创建的关系
¥relations the user do not have permission to create
-
模式中不存在的无法识别的值
¥unrecognized values that are not present on a schema
-
不可写字段和内部时间戳(如
createdAt
和createdBy
字段)标识¥non-writable fields and internal timestamps like
createdAt
andcreatedBy
fields -
设置或更新
id
字段(连接关系除外)¥setting or updating an
id
field (except for connecting relations)
使用控制器工厂时的消毒
¥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 | (EXPERIMENTAL)验证输入数据(无效数据引发错误) |
这些函数自动从模型继承清理设置,并根据内容类型架构和任何内容 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.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 tools 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
命名空间公开,可用于清理和验证:
¥Within custom controllers, there are several primary functions exposed via the global strapi
namespace that can be used for sanitization and validation:
函数名称 | 参数 | 描述 |
---|---|---|
strapi.contentAPI.sanitize.input | data 、schema 、auth | 清理请求输入,包括不可写字段、删除受限关系以及插件添加的其他嵌套 "visitors" |
strapi.contentAPI.sanitize.output | data 、schema 、auth | 清理响应输出,包括受限关系、私有字段、密码和插件添加的其他嵌套 "visitors" |
strapi.contentAPI.sanitize.query | ctx.query 、schema 、auth | 清理请求查询,包括过滤器、排序、字段和填充 |
strapi.contentAPI.validate.query | ctx.query 、schema 、auth | 验证请求查询,包括过滤器、排序、字段(当前未填充) |
strapi.contentAPI.validate.input | data 、schema 、auth | (EXPERIMENTAL)验证请求输入,包括不可写字段、删除受限关系以及插件添加的其他嵌套 "visitors" |
根据自定义控制器的复杂性,你可能需要 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
const { sanitize, validate } = strapi.contentApi;
module.exports = {
async findCustom(ctx) {
const contentType = strapi.contentType('api::test.test');
await validate.query(ctx.query, contentType, { auth: ctx.state.auth });
const sanitizedQueryParams = await sanitize.query(ctx.query, contentType, { auth: ctx.state.auth });
const documents = await strapi.documents(contentType.uid).findMany(sanitizedQueryParams);
return await sanitize.output(documents, contentType, { auth: ctx.state.auth });
}
}
const { sanitize, validate } = strapi.contentApi;
export default {
async findCustom(ctx) {
const contentType = strapi.contentType('api::test.test');
await validate.query(ctx.query, contentType, { auth: ctx.state.auth });
const sanitizedQueryParams = await sanitize.query(ctx.query, contentType, { auth: ctx.state.auth });
const documents = await strapi.documents(contentType.uid).findMany(sanitizedQueryParams);
return await sanitize.output(documents, contentType, { auth: ctx.state.auth });
}
}
扩展核心控制器
¥Extending core controllers
为每个内容类型创建默认控制器和操作。这些默认控制器用于返回对 API 请求的响应(例如,当访问 GET /api/articles/3
时,将调用 "文章" 内容类型的默认控制器的 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.
Collection type examples
后端定制示例秘诀 展示了如何覆盖默认控制器操作,例如 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;
}
Single type examples
- 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
控制器被声明并附加到路由。调用路由时会自动调用控制器,因此通常不需要显式调用控制器。但是,services 可以调用控制器,在这种情况下,应使用以下语法:
¥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
.