在大多数情况下,控制器将包含项目的大部分业务逻辑。但随着控制器的逻辑变得越来越复杂,使用 服务 将代码组织成可重复使用的部分是一种很好的做法。
该图表示请求如何通过 Strapi 后端的简化版本,其中突出显示了控制器。后端自定义介绍页面包含一个完整的交互式图表。
在决定自定义核心控制器之前,请考虑创建自定义路由中间件(请参阅路由文档)。
实施
可以手动生成或添加 控制器。 Strapi 提供了一个 createCoreController
工厂函数,可以自动生成核心控制器并允许构建自定义控制器或 扩展或替换生成的控制器。
添加新控制器
可以通过以下方式实现新控制器:
- 使用 交互式 CLI 命令
strapi generate
- 或手动创建 JavaScript 文件:
- 对于 API 控制器,位于
./src/api/[api-name]/controllers/
(此位置很重要,因为 Strapi 会从那里自动加载控制器) - 或对于插件控制器,位于
./src/plugins/[plugin-name]/server/controllers/
等文件夹中,但只要在strapi-server.js
文件中正确导出插件接口,它们就可以在其他地方创建(请参阅 插件服务器 API 文档)
// ./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'
},
})
)
每个控制器操作都可以是 async
或 sync
函数。
每个操作都接收一个上下文对象 (ctx
) 作为参数。ctx
包含 请求上下文 和 响应上下文。
示例:GET /hello
路由调用基本控制器
定义了一个特定的 GET /hello
路由,路由器文件的名称(即 index
)用于调用控制器处理程序(即 index
)。每次将 GET /hello
请求发送到服务器时,Strapi 都会调用 hello.js
控制器中的 index
操作,该操作返回 Hello World!
:
// ./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 中:
- 验证应用于查询参数,
- 并且仅对输入数据(创建和更新正文数据)进行清理。
强烈建议您使用新的 sanitizeQuery
和 validateQuery
函数清理(v4.8.0+)和/或验证(v4.13.0+)传入的请求查询,以防止泄露私人数据。
使用控制器工厂时的清理
在 Strapi 工厂中,公开了以下可用于清理和验证的函数:
函数名称 | 参数 | 说明 |
---|---|---|
sanitizeQuery | ctx | 清理请求查询 |
sanitizeOutput | entity /entities , ctx | 清理输出数据,其中 entity/entities 应该是对象或数据数组 |
sanitizeInput | data , ctx | 清理输入数据 |
validateQuery | ctx | 验证请求查询(无效参数抛出错误) |
validateInput | data , ctx | (实验)验证输入数据(无效数据抛出错误) |
这些函数会自动从模型继承清理设置,并根据内容类型架构和任何内容 API 身份验证策略(例如用户和权限插件或 API 令牌)相应地清理数据。
由于这些方法使用与当前控制器关联的模型,因此如果您查询来自另一个模型的数据(即在“餐厅”控制器方法中查找“菜单”),则必须改用 @strapi/utils
工具,例如 清理自定义控制器 中描述的 sanitize.contentAPI.query
,否则您的查询结果将针对错误的模型进行清理。
// ./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.input | data 、schema 、auth | 清理请求输入,包括不可写字段、删除受限关系和插件添加的其他嵌套“访问者” |
sanitize.contentAPI.output | data 、schema 、auth | 清理响应输出,包括受限关系、私有字段、密码和插件添加的其他嵌套“访问者” |
sanitize.contentAPI.query | ctx.query 、schema 、auth | 清理请求查询,包括过滤器、排序、字段和填充 |
validate.contentAPI.query | ctx.query 、schema 、auth | 验证请求查询,包括过滤器、排序、字段(当前不填充) |
validate.contentAPI.input | data 、schema 、auth | (实验性)验证请求输入,包括不可写字段、删除受限关系和插件添加的其他嵌套“访问者” |
根据自定义控制器的复杂性,您可能需要额外的清理,而 Strapi 目前无法考虑到这一点,尤其是在组合来自多个来源的数据时。
// ./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
操作)。可以自定义默认控制器以实现您自己的逻辑。以下代码示例应该可以帮助您入门。
可以通过创建自定义操作 并将操作命名为与原始操作相同的名称(例如 find
、findOne
、create
、update
或 delete
)来完全替换核心控制器中的操作。
扩展核心控制器时,您无需重新实现任何清理,因为它已经由您正在扩展的核心控制器处理。如果可能,强烈建议扩展核心控制器,而不是创建自定义控制器。
集合类型示例
async find(ctx) {
// some logic here
const { data, meta } = await super.find(ctx);
// some more logic
return { data, meta };
}
Single type examples
async find(ctx) {
// some logic here
const response = await super.find(ctx);
// some more logic
return response;
}
Usage
控制器被声明并附加到路由。当路由被调用时,控制器会自动调用,因此通常不需要显式调用控制器。但是,服务 可以调用控制器,在这种情况下应使用以下语法:
// 访问 API 控制器
strapi.controller('api::api-name.controller-name')
// 访问插件控制器
strapi.controller('plugin::plugin-name.controller-name')
要列出所有可用的控制器,请运行 yarn strapi controllers:list
。