Skip to main content

服务器 API:控制器和服务

🌐 Server API: Controllers & services

Page summary:

就像 Strapi 核心一样,插件也可以有控制器和服务。插件控制器处理 HTTP 层:它们接收 ctx、调用服务并返回响应。插件服务包含可重用的业务逻辑,并通过文档服务 API 与内容类型交互。保持控制器简洁,将字段逻辑放在服务中。

控制器和服务是插件服务器中处理请求和业务逻辑的两个构建模块。它们在职责分离上协同工作:控制器负责 HTTP 层,服务负责字段层:

🌐 Controllers and services are the 2 building blocks that handle request processing and business logic in a plugin server. They work together in a clear separation of concerns: controllers own the HTTP layer, services own the domain layer: | 目标 | 使用 || --- | --- || 接收 ctx,读取请求,设置响应 | 控制器 || 查询数据库或应用业务规则 | 服务 || 在多个控制器或生命周期钩子中重用逻辑 | 服务 || 作为请求的一部分调用外部 API | 服务 |

Prerequisites

在深入了解本页的概念之前,请确保你已经:

🌐 Before diving deeper into the concepts on this page, please ensure you have:

控制器

🌐 Controllers

控制器是一个包含动作方法的对象,每个方法对应一个路由处理程序。控制器接收包含请求和响应的 Koa 上下文对象(ctx),调用相应的服务,并为响应设置 ctx.bodyctx.status

🌐 A controller is an object of action methods, each corresponding to a route handler. Controllers receive a Koa context object (ctx) containing the request and response, call the appropriate service, and set ctx.body or ctx.status for the response.

声明

🌐 Declaration

控制器可以作为接收 { strapi } 的工厂函数导出,也可以作为普通对象导出。工厂函数模式是推荐的依赖注入方式,并且与大多数文档示例保持一致。

🌐 Controllers can be exported either as a factory function receiving { strapi } or as a plain object. The factory function pattern is the recommended approach for dependency injection and consistency with most documentation examples.

在运行时,Strapi 支持导出,并通过使用 { strapi } 调用它们来解析函数导出。

🌐 At runtime, Strapi supports both exports and resolves function exports by calling them with { strapi }.

controllers/index.js|ts 中使用的导出键必须与路由定义中使用的处理程序名称匹配。

🌐 The export key used in controllers/index.js|ts must match the handler name used in route definitions.

/src/plugins/my-plugin/server/src/controllers/index.js
'use strict';

const article = require('./article');

module.exports = {
article,
};
/src/plugins/my-plugin/server/src/controllers/article.js
'use strict';

module.exports = ({ strapi }) => ({
async find(ctx) {
const articles = await strapi
.plugin('my-plugin')
.service('article')
.findAll();

ctx.body = articles;
},

async findOne(ctx) {
const { documentId } = ctx.params;
const article = await strapi
.plugin('my-plugin')
.service('article')
.findOne(documentId);

if (!article) {
return ctx.notFound('Article not found');
}

ctx.body = article;
},

async create(ctx) {
const article = await strapi
.plugin('my-plugin')
.service('article')
.create(ctx.request.body);

ctx.status = 201;
ctx.body = article;
},
});

消毒

🌐 Sanitization

当你的插件公开内容 API 路由时,在返回查询参数和输出数据之前,请对其进行清理。这可以防止泄露私有字段或绕过访问规则。

🌐 When your plugin exposes Content API routes, sanitize query parameters and output data before returning them. This prevents leaking private fields or bypassing access rules.

插件控制器是普通的工厂函数,并不像 Strapi 核心中那样继承 createCoreController(详情请参见 后端自定义)。这意味着 this.sanitizeQuerythis.sanitizeOutput 简写不可用。请直接使用 strapi.contentAPI.sanitize,并显式传入内容类型的架构:

🌐 Plugin controllers are plain factory functions and do not extend createCoreController like in the Strapi core (see backend customization for details). This means the this.sanitizeQuery and this.sanitizeOutput shorthands are not available. Use strapi.contentAPI.sanitize directly instead, passing the content-type schema explicitly:

/src/plugins/my-plugin/server/src/controllers/article.js
module.exports = ({ strapi }) => ({
async find(ctx) {
const schema = strapi.contentType('plugin::my-plugin.article');

const sanitizedQuery = await strapi.contentAPI.sanitize.query(
ctx.query, schema, { auth: ctx.state.auth }
);
const articles = await strapi.plugin('my-plugin').service('article').findAll(sanitizedQuery);
ctx.body = await strapi.contentAPI.sanitize.output(articles, schema, { auth: ctx.state.auth });
},
});
Backend customization

有关完整的清理和验证参考,包括 sanitizeInputvalidateQueryvalidateInput,请参见 Controllers

🌐 For the full sanitization and validation reference, including sanitizeInput, validateQuery, and validateInput, see Controllers.

服务

🌐 Services

服务是一个工厂函数,它接收 { strapi } 并返回一个具有命名方法的对象,或一个普通对象;像 controllers 一样,Strapi 在运行时解析两者。服务包含由控制器、生命周期钩子或其他服务调用的业务逻辑。

🌐 A service is a factory function that receives { strapi } and returns an object of named methods, or a plain object; like controllers, Strapi resolves both at runtime. Services hold business logic called from controllers, lifecycle hooks, or other services.

声明

🌐 Declaration

/src/plugins/my-plugin/server/src/services/index.js
'use strict';

const article = require('./article');

module.exports = {
article,
};
/src/plugins/my-plugin/server/src/services/article.js
'use strict';

module.exports = ({ strapi }) => ({
async findAll(params = {}) {
return strapi.documents('plugin::my-plugin.article').findMany(params);
},

async findOne(documentId) {
return strapi.documents('plugin::my-plugin.article').findOne({
documentId,
});
},

async create(data) {
return strapi.documents('plugin::my-plugin.article').create({ data });
},

async update(documentId, data) {
return strapi.documents('plugin::my-plugin.article').update({
documentId,
data,
});
},

async delete(documentId) {
return strapi.documents('plugin::my-plugin.article').delete({
documentId,
});
},
});
TypeScript service typing

services 在当前的 ServerObject TypeScript 接口 (@strapi/types) 中被类型化为 unknown。这意味着 strapi.plugin('my-plugin').service('article') 返回 unknown,并且调用带有类型安全的方法时需要进行类型转换。对于完全类型化的服务调用,请显式定义并导出服务类型,并在调用处进行类型转换。

Document Service API

服务通过文档服务 API与内容类型进行交互,该 API 记录了可用方法和参数的完整列表。

🌐 Services interact with content-types through the Document Service API, which documents the full list of available methods and parameters.

端到端示例

🌐 End-to-end example

以下示例展示了针对一个简单文章资源,跨路由、控制器和服务的完整请求流程。

🌐 The following example shows the complete request flow across routes, a controller, and a service for a simple article resource.

/src/plugins/my-plugin/server/src/routes/index.js
'use strict';

module.exports = {
'content-api': {
type: 'content-api',
routes: [
{
method: 'GET',
path: '/articles',
handler: 'article.find', // maps to controllers/article.js → find()
config: { auth: false },
},
{
method: 'POST',
path: '/articles',
handler: 'article.create', // maps to controllers/article.js → create()
config: { auth: false },
},
],
},
};
/src/plugins/my-plugin/server/src/controllers/article.js
'use strict';

module.exports = ({ strapi }) => ({
async find(ctx) {
ctx.body = await strapi.plugin('my-plugin').service('article').findAll();
// Note: sanitize query and output in production — see the Sanitization section above
},

async create(ctx) {
const article = await strapi
.plugin('my-plugin')
.service('article')
.create(ctx.request.body);
ctx.status = 201;
ctx.body = article;
},
});
/src/plugins/my-plugin/server/src/services/article.js
'use strict';

module.exports = ({ strapi }) => ({
findAll() {
return strapi.documents('plugin::my-plugin.article').findMany();
},

create(data) {
return strapi.documents('plugin::my-plugin.article').create({ data });
},
});

最佳实践

🌐 Best practices

  • 保持控制器简洁。 控制器操作应该做三件事:接收 ctx,委托给服务,并设置响应。业务逻辑、数据库调用以及条件分支都应该放在服务中。
  • 每个资源一个服务。 按它们管理的资源组织服务(例如 articlecommentsettings),而不是按操作类型组织。这可以让每个文件集中并且易于测试。
  • 在服务中使用文档服务 API,而不是在控制器中。 在控制器中直接调用 strapi.documents(...) 会绕过服务层,使逻辑难以复用。将所有文档服务调用放在服务中。
  • 清理内容 API 响应。 在公开内容 API 路由时,返回数据之前使用 strapi.contentAPI.sanitize.output()。跳过清理可能会将私有字段泄露给终端用户。管理员路由不受相同内容类型字段可见性规则的限制,但对它们进行清理也是无害的。
  • 在 TypeScript 中显式地转换服务类型。@strapi/typesservices 还未强类型化之前,在每个调用点将 strapi.plugin('my-plugin').service('my-service') 的返回值转换为服务接口。避免在整个代码库中使用 any