秘诀示例:自定义服务和控制器
¥Examples cookbook: Custom services and controllers
此页面是后端定制示例手册的一部分。请确保你已阅读其 introduction。
¥This page is part of the back end customization examples cookbook. Please ensure you've read its introduction.
在 FoodAdvisor 的前端网站上,你可以浏览 localhost:3000/restaurants
可到达的餐厅列表。单击列表中的任何餐厅将使用 /client
文件夹中包含的代码来显示有关该餐厅的其他信息。餐厅页面上显示的内容是在 Strapi 的内容管理器中创建的,并通过查询 Strapi 的 REST API(使用 /api
文件夹中包含的代码)来检索。
¥From the front-end website of FoodAdvisor, you can browse a list of restaurants accessible at localhost:3000/restaurants
. Clicking on any restaurant from the list will use the code included in the /client
folder to display additional information about this restaurant. The content displayed on a restaurant page was created within Strapi's Content Manager and is retrieved by querying Strapi's REST API which uses code included in the /api
folder.
本页将教授以下高级主题:
¥This page will teach about the following advanced topics:
话题 | 部分 |
---|---|
创建一个与 Strapi 后端交互的组件 | 来自前端的 REST API 查询 |
了解服务和控制器如何协同工作 | 控制器与服务 |
创建定制服务 | |
使用控制器中的服务 | 自定义控制器 |
来自前端的 REST API 查询
¥REST API queries from the front end
💭 上下文:
¥💭 Context:
FoodAdvisor 前端网站上的餐厅页面包含只读的评论部分。添加评论需要登录 Strapi 的管理面板,并通过 内容管理者 将内容添加到 "评论" 集合类型。
¥Restaurant pages on the front-end website of FoodAdvisor include a Reviews section that is read-only. Adding reviews requires logging in to Strapi's admin panel and adding content to the "Reviews" collection type through the Content Manager.
让我们向餐厅页面添加一个小型前端组件。该组件将允许用户直接从前端网站撰写评论。
¥Let's add a small front-end component to restaurant pages. This component will allow a user to write a review directly from the front-end website.
🎯 目标:
¥🎯 Goals:
-
添加一个表格来撰写评论。
¥Add a form to write a review.
-
在任何餐厅页面上显示该表格。
¥Display the form on any restaurants page.
-
提交表单时,向 Strapi 的 REST API 发送 POST 请求。
¥Send a POST request to Strapi's REST API when the form is submitted.
-
使用 之前存储的 JWT 来验证请求。
¥Use the previously stored JWT to authenticate the request.
🧑💻 代码示例:
¥🧑💻 Code example:
在 FoodAdvisor 项目的 /client
文件夹中,你可以使用以下代码示例:
¥In the /client
folder of the FoodAdvisor project, you could use the following code examples to:
-
创建一个新的
pages/restaurant/RestaurantContent/Reviews/new-review.js
文件,¥create a new
pages/restaurant/RestaurantContent/Reviews/new-review.js
file, -
并更新现有的
components/pages/restaurant/RestaurantContent/Reviews/reviews.js
。¥and update the existing
components/pages/restaurant/RestaurantContent/Reviews/reviews.js
.
Example front-end code to add a component for writing reviews and display it on restaurants pages:
-
在
/client
文件夹下新建一个文件,添加一个用于撰写评论的新组件,代码如下:¥Create a new file in the
/client
folder to add a new component for writing reviews with the following code:
import { Button, Input, Textarea } from '@nextui-org/react';
import { useFormik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { getStrapiURL } from '../../../../../utils';
const NewReview = () => {
const router = useRouter();
const { handleSubmit, handleChange, values } = useFormik({
initialValues: {
note: '',
content: '',
},
onSubmit: async (values) => {
/**
* Queries Strapi REST API to reach the reviews endpoint
* using the JWT previously stored in localStorage to authenticate
*/
const res = await fetch(getStrapiURL('/reviews'), {
method: 'POST',
body: JSON.stringify({
restaurant: router.query.slug,
...values,
}),
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json',
},
});
},
});
/**
* Renders the form
*/
return (
<div className="my-6">
<h1 className="font-bold text-2xl mb-3">Write your review</h1>
<form onSubmit={handleSubmit} className="flex flex-col gap-y-4">
<Input
onChange={handleChange}
name="note"
type="number"
min={1}
max={5}
label="Stars"
/>
<Textarea
name="content"
onChange={handleChange}
placeholder="What do you think about this restaurant?"
/>
<Button
type="submit"
className="bg-primary text-white rounded-md self-start"
>
Send
</Button>
</form>
</div>
);
};
export default NewReview;
-
通过将高亮的行(7、8 和 13)添加到用于渲染餐厅信息的代码中,在任何餐厅页面上显示新的表单组件:
¥Display the new form component on any restaurants page by adding the highlighted lines (7, 8, and 13) to the code used to render restaurant's information:
import React from 'react';
import delve from 'dlv';
import { formatDistance } from 'date-fns';
import { getStrapiMedia } from '../../../../../utils';
import { Textarea } from '@nextui-org/react';
import NewReview from './new-review';
const Reviews = ({ reviews }) => {
return (
<div className="col-start-2 col-end-2 mt-24">
<NewReview />
{reviews &&
reviews.map((review, index) => (
// …
控制器与服务
¥Controllers vs. Services
控制器可以包含当客户端请求路由时要执行的任何业务逻辑。然而,随着你的代码变得越来越大并且变得更加结构化,最佳实践是将逻辑拆分为只做好一件事的特定服务,然后从控制器调用这些服务。
¥Controllers could contain any business logic to be executed when the client requests a route. However, as your code grows bigger and becomes more structured, it is a best practice to split the logic into specific services that do only one thing well, then call the services from controllers.
为了说明服务的使用,在本文档中,自定义控制器不处理任何职责,并将所有业务逻辑委托给服务。
¥To illustrate the use of services, in this documentation the custom controller does not handle any responsibilities and delegates all the business logic to services.
假设我们想要定制 FoodAdvisor 的后端来实现以下场景:当在前端网站提交 之前添加的审核表 时,Strapi 会在后端创建评论并通过电子邮件通知餐厅老板。将其转化为 Strapi 后端定制意味着执行 3 个操作:
¥Let's say we would like to customize the back end of FoodAdvisor to achieve the following scenario: when submitting the previously added review form on the front-end website, Strapi will create a review in the back end and notify the restaurant owner by email. Translating this to Strapi back end customization means performing 3 actions:
-
创建 创建评论 的自定义服务。
¥Creating a custom service to create the review.
-
创建 发送电子邮件 的自定义服务。
¥Creating a custom service to send an email.
-
自定义默认控制器 由 Strapi 提供,供 Review 内容类型使用 2 项新服务。
¥Customizing the default controller provided by Strapi for the Review content-type to use the 2 new services.
客户服务:创建评论
¥Custom service: Creating a review
💭 上下文:
¥💭 Context:
默认情况下,Strapi 中的服务文件包含使用 createCoreService
工厂函数的基本样板代码。
¥By default, service files in Strapi includes basic boilerplate code that use the createCoreService
factory function.
让我们通过替换其代码来更新 FoodAdvisor 的 "评论" 集合类型的现有 review.js
服务文件以创建评论。
¥Let's update the existing review.js
service file for the "Reviews" collection type of FoodAdvisor by replacing its code to create a review.
🎯 目标:
¥🎯 Goals:
-
声明
create
方法。¥Declare a
create
method. -
从请求中获取上下文。
¥Grab context from the request.
-
使用 EntityService API 中的
findMany()
方法查找餐厅。¥Use the
findMany()
method from the EntityService API to find a restaurant. -
使用 EntityService API 中的
create()
方法将数据附加到餐厅,填充餐厅所有者。¥Use the
create()
method from the EntityService API to append data to the restaurant, populating the restaurant owner. -
返回新的评论数据。
¥Return the new review data.
其他信息可在 请求上下文、services 和 实体服务 API 文档中找到。
¥Additional information can be found in the request context, services and EntityService API documentation.
🧑💻 代码示例:
¥🧑💻 Code example:
要创建这样的服务,在 FoodAdvisor 项目的 /api
文件夹中,将 src/api/review/services/review.js
文件的内容替换为以下代码:
¥To create such a service, in the /api
folder of the FoodAdvisor project, replace the content of the src/api/review/services/review.js
file with the following code:
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::review.review', ({ strapi }) => ({
async create(ctx) {
const user = ctx.state.user;
const { body } = ctx.request;
/**
* Queries the Restaurants collection type
* using the Entity Service API
* to retrieve information about the restaurant.
*/
const restaurants = await strapi.entityService.findMany(
'api::restaurant.restaurant',
{
filters: {
slug: body.restaurant,
},
}
);
/**
* Creates a new entry for the Reviews collection type
* and populates data with information about the restaurant's owner
* using the Entity Service API.
*/
const newReview = await strapi.entityService.create('api::review.review', {
data: {
note: body.note,
content: body.content,
restaurant: restaurants[0].id,
author: user.id,
},
populate: ['restaurant.owner'],
});
return newReview;
},
}));
-
在控制器的代码中,可以使用
strapi.service('api::review.review').create(ctx)
调用此服务中的create
方法,其中ctx
是请求的 context。¥In a controller's code, the
create
method from this service can be called withstrapi.service('api::review.review').create(ctx)
wherectx
is the request's context. -
提供的示例代码不包括错误处理。你应该考虑处理错误,例如当餐厅不存在时。其他信息可在 错误处理 文档中找到。
¥The provided example code does not cover error handling. You should consider handling errors, for instance when the restaurant does not exist. Additional information can be found in the Error handling documentation.
客户服务:向餐厅老板发送电子邮件
¥Custom Service: Sending an email to the restaurant owner
💭 上下文:
¥💭 Context:
开箱即用,FoodAdvisor 不提供任何自动电子邮件服务功能。
¥Out of the box, FoodAdvisor does not provide any automated email service feature.
让我们创建一个 email.js
服务文件来发送电子邮件。我们可以在 定制控制器 中使用它来在前端网站上创建新评论时通知餐厅老板。
¥Let's create an email.js
service file to send an email. We could use it in a custom controller to notify the restaurant owner whenever a new review is created on the front-end website.
该服务是使用 电子邮件 插件的高级代码示例,需要了解 plugins 和 providers 如何与 Strapi 配合使用。如果你不需要电子邮件服务来通知餐厅老板,则可以跳过此部分并跳转到自定义 controller 示例旁边。
¥This service is an advanced code example using the Email plugin and requires understanding how plugins and providers work with Strapi. If you don't need an email service to notify the restaurant's owner, you can skip this part and jump next to the custom controller example.
-
你已经设置了 电子邮件插件的提供者,例如 发送邮件 提供商。
¥You have setup a provider for the Email plugin, for instance the Sendmail provider.
-
在 Strapi 的管理面板中,你有 创建了
Email
单一类型,其中包含from
文本字段来定义发件人电子邮件地址。¥In Strapi's admin panel, you have created an
Email
single type that contains afrom
Text field to define the sender email address.
🎯 目标:
¥🎯 Goals:
-
为 "电子邮件" 单一类型创建一个新的服务文件,
¥Create a new service file for the "Email" single type,
-
为该服务声明一个
send()
方法,¥Declare a
send()
method for this service, -
使用实体服务 API 获取存储在电子邮件单一类型中的发件人地址,
¥Grab the sender address stored in the Email single type using the Entity Service API,
-
使用调用服务的
send()
方法时传递的电子邮件详细信息(收件人地址、主题和电子邮件正文),以使用电子邮件插件和之前配置的提供程序发送电子邮件。¥Use email details (recipient's address, subject, and email body) passed when invoking the service's
send()
method to send an email using the Email plugin and a previously configured provider.
其他信息可在 服务、实体服务 API、电子邮件插件 和 提供者 文档中找到。
¥Additional information can be found in the Services, Entity Service API, Email plugin and Providers documentation.
🧑💻 代码示例:
¥🧑💻 Code example:
要创建这样的服务,请在 FoodAdvisor 项目的 /api
文件夹中,创建一个新的 src/api/email/services/email.js
文件,其中包含以下代码:
¥To create such a service, in the /api
folder of the FoodAdvisor project, create a new src/api/email/services/email.js
file with the following code:
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::email.email', ({ strapi }) => ({
async send({ to, subject, html }) {
/**
* Retrieves email configuration data
* stored in the Email single type
* using the Entity Service API.
*/
const emailConfig = await strapi.entityService.findOne(
'api::email.email',
1
);
/**
* Sends an email using:
* - parameters to pass when invoking the service
* - the 'from' address previously retrieved with the email configuration
*/
await strapi.plugins['email'].services.email.send({
to,
subject,
html,
from: emailConfig.from,
});
},
}));
在控制器的代码中,可以使用 strapi.service('api::email.email).send(parameters)
调用此电子邮件服务中的 send
方法,其中 parameters
是具有电子邮件相关信息(收件人地址、主题和电子邮件正文)的对象。
¥In a controller's code, the send
method from this email service can be called with strapi.service('api::email.email).send(parameters)
where parameters
is an object with the email's related information (recipient's address, subject, and email body).
自定义控制器
¥Custom controller
💭 上下文:
¥💭 Context:
默认情况下,Strapi 中的控制器文件包含使用 createCoreController
工厂函数的基本样板代码。这公开了在到达所请求的端点时创建、检索、更新和删除内容的基本方法。可以自定义控制器的默认代码以执行任何业务逻辑。
¥By default, controllers files in Strapi includes basic boilerplate code that use the createCoreController
factory function. This exposes basic methods to create, retrieve, update, and delete content when reaching the requested endpoint. The default code for the controllers can be customized to perform any business logic.
让我们为 FoodAdvisor 的 "评论" 集合类型自定义默认控制器,场景如下:根据对 /reviews
端点的 POST
请求,控制器将先前创建的 创建评论 和 发送电子邮件 服务调用给餐厅所有者。
¥Let's customize the default controller for the "Reviews" collection type of FoodAdvisor with the following scenario: upon a POST
request to the /reviews
endpoint, the controller calls previously created services to both create a review and send an email to the restaurant's owner.
🎯 目标:
¥🎯 Goals:
-
扩展 "评论" 集合类型的现有控制器。
¥Extend the existing controller for the "Reviews" collection type.
-
声明自定义
create()
方法。¥Declare a custom
create()
method. -
调用之前创建的服务。
¥Call previously created service(s).
-
清理要返回的内容。
¥Sanitize the content to be returned.
其他信息可在 controllers 文档中找到。
¥Additional information can be found in the controllers documentation.
🧑💻 代码示例:
¥🧑💻 Code example:
在 FoodAdvisor 项目的 /api
文件夹中,将 src/api/review/controllers/review.js
文件的内容替换为以下代码示例之一,具体取决于你之前是仅创建 一项定制服务 还是同时创建用于评论创建和 电子邮件通知 的自定义服务:
¥In the /api
folder of the FoodAdvisor project, replace the content of the src/api/review/controllers/review.js
file with one of the following code examples, depending on whether you previously created just one custom service or both custom services for the review creation and the email notification:
- Custom controller without email service
- Custom controller with email service
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::review.review', ({ strapi }) => ({
/**
* As the controller action is named
* exactly like the original `create` action provided by the core controller,
* it overwrites it.
*/
async create(ctx) {
// Creates the new review using a service
const newReview = await strapi.service('api::review.review').create(ctx);
const sanitizedReview = await this.sanitizeOutput(newReview, ctx);
ctx.body = sanitizedReview;
},
}));
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::review.review', ({ strapi }) => ({
/**
* As the controller action is named
* exactly like the original `create` action provided by the core controller,
* it overwrites it.
*/
async create(ctx) {
// Creates the new review using a service
const newReview = await strapi.service('api::review.review').create(ctx);
// Sends an email to the restaurant's owner, using another service
if (newReview.restaurant?.owner) {
await strapi.service('api::email.email').send({
to: newReview.restaurant.owner.email,
subject: 'You have a new review!',
html: `You've received a ${newReview.note} star review: ${newReview.content}`,
});
}
const sanitizedReview = await this.sanitizeOutput(newReview, ctx);
ctx.body = sanitizedReview;
},
}));
详细了解 定制政策 如何帮助你调整基于 Strapi 的应用并根据特定条件限制对某些资源的访问。
¥Learn more about how custom policies can help you tweak a Strapi-based application and restrict access to some resources based on specific conditions.