NestJS是构建服务器端应用程序的最佳Node框架之一。在本教程中,我们将探讨如何建立一个简单的NestJS电子商务应用程序,并在此过程中展示Nest的许多主要功能。我们将涵盖。
开始使用我们的NestJS电子商务应用程序
默认情况下,NestJS在后台使用Express,尽管你可以选择使用Fastify代替。Nest提供了一个坚实的应用程序架构,而Express和Fastify是强大的HTTP服务器框架,具有无数的应用程序开发功能。
拥有强大的架构使你有能力建立高度可扩展、可测试、松散耦合和易于维护的应用程序。使用Nest可以让你的Node.js后端更上一层楼。
Nest在很大程度上受到Angular的启发,并借用了它的许多概念。如果你已经使用Angular,Nest可能是一个完美的匹配。
要遵循本教程,你至少需要对Node、MongoDB、TypeScript和Nest有基本了解和经验。请确保你的机器上安装了Node和MongoDB。
你应该知道的Nest功能
让我们花点时间来回顾一下Nest的主要功能:模块、控制器和服务。
模块是组织和构建Nest应用程序的主要策略。必须至少有一个根模块来创建一个应用程序。每个模块可以包含控制器和服务,甚至是其他模块。
Nest使用依赖性注入模式,将模块与它们的依赖关系连接起来。为了使一个类可以注入,Nest使用了一个@Injectable 装饰器。然后,为了在模块或控制器中提供该类,它使用基于构造器的依赖注入。
控制器处理传入的HTTP请求,验证参数,并向客户端返回响应。控制器应该保持干净和简单,这也是下一个Nest功能发挥作用的地方。
服务为你的Nest项目提供大部分的业务逻辑和应用功能。任何复杂的逻辑都应该通过服务来提供。事实上,服务属于一种主要类型的类,称为提供者。
提供者只是一个作为依赖关系注入的类。可能使用的其他类型的提供者包括像存储库、工厂、帮助者等类。
为我们的电子商务应用程序创建一个新的Nest项目
当你准备好了,让我们初始化一个新的Nest项目。首先,我们将安装Nest CLI。然后,我们将创建一个新的项目。
npm install -g @nestjs/cli
nest new nestjs-ecommerce
安装完成后,导航到该项目并启动它。
cd nestjs-ecommerce
npm run start:dev
然后你可以通过访问http://localhost:3000/,在你的浏览器中启动该应用程序。你应该看到一个漂亮的 "Hello World!"信息。
在你做出任何改变后,该应用程序将自动重新加载。如果你想手动重启该应用程序,请使用npm run start 命令代替。
现在我们准备开始创建商店的功能。
创建NestJS电子商务商店的产品功能
在本节中,我们将专注于产品管理。商店产品功能将允许我们检索商店产品,添加新的产品,以及编辑或删除它们。
创建我们的产品资源
让我们从创建所需的资源开始。要创建它们,请运行以下命令。
nest g module product
nest g service product --no-spec
nest g controller product --no-spec
第一条命令生成一个产品模块,并把它放在自己的同名目录中。
接下来的两个命令生成服务和控制器文件,并在product 模块中自动导入它们。--no-spec 参数告诉Nest,我们不希望生成额外的测试文件。
运行上述命令后,我们将得到一个新的product 目录,包含以下文件。product.module.ts,product.service.ts, 和product.controller.ts 。
现在我们有一个NestJS电子商务商店产品功能的基本结构。在我们继续前进之前,我们需要设置我们的数据库。
配置MongoDB数据库
由于我们使用MongoDB作为数据库,我们需要安装mongoose 和@nestjs/mongoose 包。
npm install --save @nestjs/mongoose mongoose
安装完成后,打开app.module.ts ,将其内容替换为以下内容。
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose'; // 1.1 Import the mongoose module
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ProductModule } from './product/product.module'; // 2.1 Import the product module
@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/store'), // 1.2 Setup the database
ProductModule, // 2.2 Add the product module
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
以下是我们在上面的代码中所做的。使用我的编号说明跟着做。
- 首先,我们导入了
MongooseModule(1.1),并使用它来建立一个新的store数据库(1.2)。 - 其次,我们导入了
ProductModule(2.1)并将其添加到imports数组中(2.2)。
我们的下一步是为我们的产品模型创建一个数据库模式。
创建一个产品模型模式
在product 目录中,创建一个新的schemas 目录。在新目录中放一个product.schema.ts ,内容如下。
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type ProductDocument = Product & Document;
@Schema()
export class Product {
@Prop()
name: string;
@Prop()
description: string;
@Prop()
price: number;
@Prop()
category: string;
}
export const ProductSchema = SchemaFactory.createForClass(Product);
上面的代码为我们的产品创建了一个带有name,description,price, 和category 属性的模式。
现在以如下方式编辑product.module.ts 。
import { Module } from '@nestjs/common';
import { ProductController } from './product.controller';
import { ProductService } from './product.service';
import { MongooseModule } from '@nestjs/mongoose'; // 1. Import mongoose module
import { ProductSchema } from './schemas/product.schema'; // 2. Import product schema
@Module({
imports: [
MongooseModule.forFeature([{ name: 'Product', schema: ProductSchema }]) // 3. Setup the mongoose module to use the product schema
],
controllers: [ProductController],
providers: [ProductService]
})
export class ProductModule {}
从我的编号说明中可以看出,在上面的代码中,我们导入了MongooseModule (1)和ProductModule (2),然后设置ProductSchema ,用于我们的产品模型(3)。
创建产品DTO文件
除了产品模式,我们还需要两个数据传输对象(DTO)文件用于我们的NestJS电子商务应用程序。一个DTO文件定义了将从表单提交、搜索查询等接收的数据。
我们需要一个DTO用于产品创建,另一个用于产品过滤。现在让我们来创建它们。
在product 目录中,创建一个新的dtos 目录。在这个新目录中放一个create-product.dto.ts 文件,内容如下。
export class CreateProductDTO {
name: string;
description: string;
price: number;
category: string;
}
上面的DTO定义了一个产品对象,具有创建新产品的必要属性。
然后,在同一目录下,创建一个filter-product.dto.ts 文件,内容如下。
export class FilterProductDTO {
search: string;
category: string;
}
这第二个DTO定义了一个过滤器对象,我们将用它来按搜索查询、类别或两者来过滤商店的产品。
创建产品服务方法
本节的所有准备工作已经完成。现在让我们来创建产品管理的实际代码。
打开product.service.ts 文件,将其内容替换为以下内容。
import { Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { Product, ProductDocument } from './schemas/product.schema';
import { CreateProductDTO } from './dtos/create-product.dto';
import { FilterProductDTO } from './dtos/filter-product.dto';
@Injectable()
export class ProductService {
constructor(@InjectModel('Product') private readonly productModel: Model<ProductDocument>) { }
async getFilteredProducts(filterProductDTO: FilterProductDTO): Promise<Product[]> {
const { category, search } = filterProductDTO;
let products = await this.getAllProducts();
if (search) {
products = products.filter(product =>
product.name.includes(search) ||
product.description.includes(search)
);
}
if (category) {
products = products.filter(product => product.category === category)
}
return products;
}
async getAllProducts(): Promise<Product[]> {
const products = await this.productModel.find().exec();
return products;
}
async getProduct(id: string): Promise<Product> {
const product = await this.productModel.findById(id).exec();
return product;
}
async addProduct(createProductDTO: CreateProductDTO): Promise<Product> {
const newProduct = await this.productModel.create(createProductDTO);
return newProduct.save();
}
async updateProduct(id: string, createProductDTO: CreateProductDTO): Promise<Product> {
const updatedProduct = await this.productModel
.findByIdAndUpdate(id, createProductDTO, { new: true });
return updatedProduct;
}
async deleteProduct(id: string): Promise<any> {
const deletedProduct = await this.productModel.findByIdAndRemove(id);
return deletedProduct;
}
}
让我们逐条检查上面的代码块。
首先,让我们看一下下面复制的部分。
@Injectable()
export class ProductService {
constructor(@InjectModel('Product') private readonly productModel: Model<ProductDocument>) { }
}
这段代码通过使用@InjectModel 装饰器注入了所需的依赖关系(产品模型)。
在接下来的部分,我们有两个方法。
async getAllProducts(): Promise<Product[]> {
const products = await this.productModel.find().exec();
return products;
}
async getProduct(id: string): Promise<Product> {
const product = await this.productModel.findById(id).exec();
return product;
}
第一个方法getAllProducts ,用于获取所有产品。第二个方法getProduct ,用于获取单一产品。我们使用标准的Mongoose方法来实现这些操作。
下面的方法getFilteredProducts ,返回过滤后的产品。
async getFilteredProducts(filterProductDTO: FilterProductDTO): Promise<Product[]> {
const { category, search } = filterProductDTO;
let products = await this.getAllProducts();
if (search) {
products = products.filter(product =>
product.name.includes(search) ||
product.description.includes(search)
);
}
if (category) {
products = products.filter(product => product.category === category)
}
return products;
}
产品可以通过搜索查询、类别或两者进行过滤。
下面的方法addProduct ,创建一个新的产品。
async addProduct(createProductDTO: CreateProductDTO): Promise<Product> {
const newProduct = await this.productModel.create(createProductDTO);
return newProduct.save();
}
addProduct 通过使用 文件中的类并将其保存到数据库中来实现这一目的。create-product.dto.ts
最后两个方法是updateProduct 和deleteProduct 。
async updateProduct(id: string, createProductDTO: CreateProductDTO): Promise<Product> {
const updatedProduct = await this.productModel
.findByIdAndUpdate(id, createProductDTO, { new: true });
return updatedProduct;
}
async deleteProduct(id: string): Promise<any> {
const deletedProduct = await this.productModel.findByIdAndRemove(id);
return deletedProduct;
}
使用这些方法,你可以通过ID找到一个产品,并更新它或从数据库中删除它。
创建产品控制器方法
产品模块的最后一步是创建API端点。
我们将创建以下API端点。
- POST
store/products/- 添加新产品 - GET
store/products/- 获得所有产品 - GET
store/products/:id- 获取单个产品 - PUT
store/products/:id- 编辑单个产品 - DELETE
store/products/:id- 删除单个产品
打开product.controller.ts 文件,将其内容替换为以下内容。
import { Controller, Post, Get, Put, Delete, Body, Param, Query, NotFoundException } from '@nestjs/common';
import { ProductService } from './product.service';
import { CreateProductDTO } from './dtos/create-product.dto';
import { FilterProductDTO } from './dtos/filter-product.dto';
@Controller('store/products')
export class ProductController {
constructor(private productService: ProductService) { }
@Get('/')
async getProducts(@Query() filterProductDTO: FilterProductDTO) {
if (Object.keys(filterProductDTO).length) {
const filteredProducts = await this.productService.getFilteredProducts(filterProductDTO);
return filteredProducts;
} else {
const allProducts = await this.productService.getAllProducts();
return allProducts;
}
}
@Get('/:id')
async getProduct(@Param('id') id: string) {
const product = await this.productService.getProduct(id);
if (!product) throw new NotFoundException('Product does not exist!');
return product;
}
@Post('/')
async addProduct(@Body() createProductDTO: CreateProductDTO) {
const product = await this.productService.addProduct(createProductDTO);
return product;
}
@Put('/:id')
async updateProduct(@Param('id') id: string, @Body() createProductDTO: CreateProductDTO) {
const product = await this.productService.updateProduct(id, createProductDTO);
if (!product) throw new NotFoundException('Product does not exist!');
return product;
}
@Delete('/:id')
async deleteProduct(@Param('id') id: string) {
const product = await this.productService.deleteProduct(id);
if (!product) throw new NotFoundException('Product does not exist');
return product;
}
}
NestJS提供了一整套的JavaScript装饰器来处理HTTP请求和响应(Get,Put,Body,Param 等),处理错误(NotFoundException ),定义控制器(Controller ),等等。
我们在文件的开头从@nestjs/common ,导入了我们需要的那些。我们还导入了所有我们已经创建的、我们需要的其他文件。ProductService,CreateProductDTO, 和FilterProductDTO 。
从现在开始,我不会再详细解释导入。它们中的大多数都是非常直接和不言自明的。关于某个特定类或组件的使用的更多信息,你可以查阅文档。
让我们把其余的代码分成小块。
首先,我们使用@Controller 装饰器来设置所有端点共享的URL部分。
@Controller('store/products')
export class ProductController {
constructor(private productService: ProductService) { }
}
我们还在上面的代码中的类构造函数中注入产品服务。
接下来,我们通过使用@Get 装饰器定义以下端点。
@Get('/')
async getProducts(@Query() filterProductDTO: FilterProductDTO) {
if (Object.keys(filterProductDTO).length) {
const filteredProducts = await this.productService.getFilteredProducts(filterProductDTO);
return filteredProducts;
} else {
const allProducts = await this.productService.getAllProducts();
return allProducts;
}
}
定义完端点后,我们在getProducts() 方法中使用@Query 装饰器,并使用filter-product.dto.ts 对象来获取请求的查询参数。
如果请求中的查询参数存在,我们使用产品服务中的getFilteredProduct() 方法。如果没有这样的参数,我们就使用常规的getAllProducts() 方法来代替。
在下面的端点中,我们使用@Body 装饰器来从请求体中获取所需的数据,然后将其传递给addProduct() 方法。
@Post('/')
async addProduct(@Body() createProductDTO: CreateProductDTO) {
const product = await this.productService.addProduct(createProductDTO);
return product;
}
在接下来的端点中,我们使用@Param 装饰器从URL中获取产品ID。
@Get('/:id')
async getProduct(@Param('id') id: string) {
const product = await this.productService.getProduct(id);
if (!product) throw new NotFoundException('Product does not exist!');
return product;
}
@Put('/:id')
async updateProduct(@Param('id') id: string, @Body() createProductDTO: CreateProductDTO) {
const product = await this.productService.updateProduct(id, createProductDTO);
if (!product) throw new NotFoundException('Product does not exist!');
return product;
}
@Delete('/:id')
async deleteProduct(@Param('id') id: string) {
const product = await this.productService.deleteProduct(id);
if (!product) throw new NotFoundException('Product does not exist');
return product;
}
然后我们使用产品服务中适当的方法来获取、编辑或删除产品。如果没有找到产品,我们使用NotFoundException ,抛出一个错误信息。
创建用户管理功能
我们需要为我们的NestJS电子商务应用程序创建的下一个功能是用户管理功能。
生成我们的用户管理资源
对于用户管理功能,我们只需要一个模块和一个服务。要创建它们,请运行以下程序。
nest g module user
nest g service user --no-spec
与之前的功能一样,我们将需要一个模式和DTO。
创建一个用户模式和DTO
在 Nest 生成的user 目录中,创建一个新的schemas 文件夹。在这个新文件夹中添加一个user.schema.ts 文件,内容如下。
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
// import { Role } from 'src/auth/enums/role.enum';
export type UserDocument = User & Document;
@Schema()
export class User {
@Prop()
username: string;
@Prop()
email: string;
@Prop()
password: string;
/*
@Prop()
roles: Role[];
*/
}
export const UserSchema = SchemaFactory.createForClass(User);
当我们实施用户授权时,该块末尾的注释代码将被使用。我将在本教程的后面告诉你何时取消注释。
接下来,在user 目录中,创建一个新的dtos 文件夹。在这个新文件夹中添加一个create-user-dto.ts 文件,内容如下。
export class CreateUserDTO {
username: string;
email: string;
password: string;
roles: string[];
}
配置资源
打开user.module.ts ,按照我们在产品功能中的相同方式设置模式。
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UserSchema } from './schemas/user.schema';
import { UserService } from './user.service';
@Module({
imports: [
MongooseModule.forFeature([{ name: 'User', schema: UserSchema }])
],
providers: [UserService],
exports: [UserService]
})
export class UserModule {}
在上面的代码中,我们也在导出UserService ,这样我们就可以在以后的认证服务中使用它。
我们还需要安装两个额外的包:bcrypt 和@types/bcrypt 。
npm install bcrypt
npm install -D @types/bcrypt
这些包使我们能够保存密码,我们将在下一节进行这方面的工作。
创建用户服务方法
现在让我们来添加用户管理的逻辑。打开user.service.ts 文件,将其内容替换为以下内容。
import { Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { User, UserDocument } from './schemas/user.schema';
import { CreateUserDTO } from './dtos/create-user.dto';
import * as bcrypt from 'bcrypt';
@Injectable()
export class UserService {
constructor(@InjectModel('User') private readonly userModel: Model<UserDocument>) { }
async addUser(createUserDTO: CreateUserDTO): Promise<User> {
const newUser = await this.userModel.create(createUserDTO);
newUser.password = await bcrypt.hash(newUser.password, 10);
return newUser.save();
}
async findUser(username: string): Promise<User | undefined> {
const user = await this.userModel.findOne({username: username});
return user;
}
}
我们在上面的代码中添加了两个方法。addUser() 方法创建一个新的用户,通过使用bcrypt.hash() ,对新用户的密码进行加密,然后将用户保存到数据库中。
findUser() 方法通过username 找到一个特定的用户。
创建用户认证和授权
在本节中,我们将通过添加用户认证和用户授权来扩展NestJS电子商务应用程序中的用户管理功能,前者可以验证用户的身份,后者可以定义用户被允许做什么。
我们将使用著名的Passport库,它提供了大量的认证策略。让我们来安装必要的软件包。
npm install --save @nestjs/passport passport passport-local
npm install --save-dev @types/passport-local
在上面的代码中,我们安装了主passport 包、passport-local 策略(实现了一个简单的用户名和密码认证机制)和Nest passport 适配器。我们还安装了passport-local 的类型。
我们还需要安装用于管理环境变量的dotenv 包。
npm install dotenv
在根目录下创建一个.env 文件,并将以下代码放在里面。
JWT_SECRET="topsecret"
我们以后会用到这个变量。
生成我们的用户认证和授权资源
像往常一样,让我们开始为我们的认证功能创建所需的资源。
nest g module auth
nest g service auth --no-spec
nest g controller auth --no-spec
创建用户服务方法
打开auth.service.ts 文件,将其内容替换为以下内容。
import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(private readonly userService: UserService) {}
async validateUser(username: string, password: string): Promise<any> {
const user = await this.userService.findUser(username);
const isPasswordMatch = await bcrypt.compare(
password,
user.password
);
if (user && isPasswordMatch) {
return user;
}
return null;
}
}
上面的代码给了我们一个用户验证方法,它检索用户并验证用户的密码。
创建一个本地验证策略
在auth 目录下,创建一个新的strategies 文件夹。在这个新文件夹中添加一个local.strategy.ts 文件,内容如下。
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from '../auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
这段代码做了两件事。
首先,它在构造函数中调用super() 方法。如果需要,我们可以在这里传递一个选项对象。我们稍后将通过一个例子。
第二,我们添加了一个validate() 方法,它使用auth服务的validateUser() 来验证用户。
用JWT创建一个认证策略
现在我们将使用JSON Web Tokens(JWT)创建一个护照认证策略。这将为登录的用户返回一个JWT,以便在随后调用受保护的API端点时使用。
让我们来安装必要的软件包。
npm install --save @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt
接下来,在strategies 目录中,创建一个jwt.strategy.ts 文件,内容如下。
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import 'dotenv/config'
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET,
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username, roles: payload.roles };
}
}
在上面的代码中,我们设置一个具有以下属性的options 对象。
jwtFromRequest告诉Passport模块如何从请求中提取JWT(在这种情况下,作为一个承载令牌)ignoreExpiration设置为 ,意味着确保JWT没有过期的责任被委托给了Passport模块falsesecretOrKey是用来签署令牌的
validate() 方法返回一个payload ,这是 JWT 被解码为 JSON。然后我们使用这个有效载荷来返回一个具有必要属性的用户对象。
现在让我们修改一下auth.service.ts 文件。
import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt'; // 1
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(private readonly userService: UserService, private readonly jwtService: JwtService) {} // 2
async validateUser(username: string, password: string): Promise<any> {
const user = await this.userService.findUser(username);
const isPasswordMatch = await bcrypt.compare(
password,
user.password
);
if (user && isPasswordMatch) {
return user;
}
return null;
}
async login(user: any) {
const payload = { username: user.username, sub: user._id, roles: user.roles };
return {
access_token: this.jwtService.sign(payload),
};
}
}
上面的代码是有标签的,所以你可以跟着我们做什么。
- 导入了
JwtService(见//1) - 将
JwtService添加到构造函数中(见//2)。
然后我们使用login() 方法来签署一个JWT。
在我们做了所有的改变之后,我们需要以如下方式更新auth.module.ts 。
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UserModule } from 'src/user/user.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './strategies/local.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';
import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';
import 'dotenv/config'
@Module({
imports: [
UserModule,
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '3600s' },
}),
],
providers: [
AuthService,
LocalStrategy,
JwtStrategy
],
controllers: [AuthController],
})
export class AuthModule {}
在上面的代码中,我们在imports 数组中添加了UserModule,PassportModule, 和JwtModule 。
我们还使用了register() 方法来提供必要的选项:secret 密钥和signOptions 对象,该对象将令牌到期时间设置为3600s ,或1小时。
最后,我们在providers 数组中添加了LocalStrategy 和JwtStrategy 。
创建本地和JWT卫士
为了使用我们刚刚创建的策略,我们需要创建Guards。
在auth 目录中,创建一个新的guards 文件夹。在这个新文件夹中添加一个local.guard.ts 文件,内容如下。
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
同样在guards 文件夹中,创建一个jwt.guard.ts 文件,内容如下。
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
我们将在一分钟内看到如何使用这些防护措施。但首先,让我们创建用户授权功能。
创建用户角色管理
为了在我们的NestJS电子商务应用程序中实现这一功能,我们将使用基于角色的访问控制。
对于这个功能,我们需要三个文件。role.enum.ts,roles.decorator.ts, 和roles.guard.ts 。让我们从role.enum.ts 文件开始。
在auth 目录中,创建一个新的enums 文件夹。在这个新文件夹中添加一个role.enum.ts 文件,内容如下。
export enum Role {
User = 'user',
Admin = 'admin',
}
这代表注册用户的可用角色。
现在你可以回到我们之前创建的 [user.schema.ts](#creating-user-schema-dto) 文件,并取消注释的代码。
接下来,在auth 目录中,创建一个新的decorators 文件夹。在这个新文件夹中添加一个roles.decorator.ts 文件,内容如下。
import { SetMetadata } from '@nestjs/common';
import { Role } from '../enums/role.enum';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
在上面的代码中,我们用SetMetadata() 来创建装饰器。
最后,在guards 目录中,创建一个roles.guard.ts 文件,内容如下。
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from '../enums/role.enum';
import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
在上面的代码中,我们用 [Reflector](https://docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata) 助手类来访问路由的角色。我们还用switchToHttp() ,将执行上下文切换为HTTP,用getRequest() ,获得user 的详细信息。最后,我们返回了用户的角色。
控制器方法
我们在本节的最后一步是创建控制器方法。打开auth.controller.ts 文件,将其内容替换为以下内容。
import { Controller, Request, Get, Post, Body, UseGuards } from '@nestjs/common';
import { CreateUserDTO } from 'src/user/dtos/create-user.dto';
import { UserService } from 'src/user/user.service';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { Roles } from './decorators/roles.decorator';
import { Role } from './enums/role.enum';
import { RolesGuard } from './guards/roles.guard';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService, private userService: UserService) {}
@Post('/register')
async register(@Body() createUserDTO: CreateUserDTO) {
const user = await this.userService.addUser(createUserDTO);
return user;
}
@UseGuards(LocalAuthGuard)
@Post('/login')
async login(@Request() req) {
return this.authService.login(req.user);
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.User)
@Get('/user')
getProfile(@Request() req) {
return req.user;
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.Admin)
@Get('/admin')
getDashboard(@Request() req) {
return req.user;
}
}
在上面的代码中,我们有四个端点。
- POST
auth/register用来创建一个新的用户 - POST
auth/login是用来登录注册用户的- 为了验证该用户,我们使用
LocalAuthGuard
- 为了验证该用户,我们使用
- GET
auth/user用于访问用户的个人资料- 我们使用
JwtGuard来验证用户 - 我们使用
RolesGuard加上@Roles装饰器,根据用户的角色提供适当的授权
- 我们使用
- GET
auth/admin被用来访问管理仪表板 -
- 我们还使用了
JwtGuard和RolesGuard,就像在前面的端点中做的那样
- 我们还使用了
为我们的NestJS电商应用创建商店购物车功能
我们将为我们的项目添加的最后一个功能是一个基本的购物车功能。
创建我们的商店购物车资源
让我们来创建下一节所需的资源。
nest g module cart
nest g service cart --no-spec
nest g controller cart --no-spec
创建模式和DTO
对于商店购物车功能,我们需要两个模式:一个描述购物车中的产品,另一个描述购物车本身。
像往常一样,在cart 目录下,创建一个新的schemas 文件夹。在这个新文件夹中添加一个item.schema.ts 文件,内容如下。
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, SchemaTypes } from 'mongoose';
export type ItemDocument = Item & Document;
@Schema()
export class Item {
@Prop({ type: SchemaTypes.ObjectId, ref: 'Product' })
productId: string;
@Prop()
name: string;
@Prop()
quantity: number;
@Prop()
price: number;
@Prop()
subTotalPrice: number;
}
export const ItemSchema = SchemaFactory.createForClass(Item);
在上面的代码中,在productId 属性的@Prop 装饰器中,我们定义了一个对象id模式类型,并添加了一个对产品的引用。这意味着,我们将使用产品的id作为productId 的值。
下一个模式是针对购物车的。在schemas 目录中,创建一个cart.schema.ts 文件,内容如下。
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, SchemaTypes } from 'mongoose';
import { Item } from './item.schema';
export type CartDocument = Cart & Document;
@Schema()
export class Cart {
@Prop({ type: SchemaTypes.ObjectId, ref: 'User' })
userId: string;
@Prop()
items: Item[];
@Prop()
totalPrice: number;
}
export const CartSchema = SchemaFactory.createForClass(Cart);
在这里,我们对userId 属性使用同样的技术,它将获得用户的id作为值。对于items 属性,我们使用我们的Item 模式来定义一个类型为Item 的项目数组。
最后,让我们来创建项目的DTO。在user 目录中,创建一个新的dtos 文件夹,并添加一个item.dto.ts 文件,内容如下。
export class ItemDTO {
productId: string;
name: string;
quantity: number;
price: number;
}
配置购物车模块
在我们进入业务逻辑之前,我们需要将购物车模式添加到购物车模块中。打开cart.module.ts 文件,配置它以使用购物车模式,如下所示。
import { Module } from '@nestjs/common';
import { CartController } from './cart.controller';
import { CartService } from './cart.service';
import { MongooseModule } from '@nestjs/mongoose';
import { CartSchema } from './schemas/cart.schema';
@Module({
imports: [
MongooseModule.forFeature([{ name: 'Cart', schema: CartSchema }])
],
controllers: [CartController],
providers: [CartService]
})
export class CartModule {}
创建购物车服务方法
现在我们来创建购物车管理逻辑。打开cart.service.ts 文件,将其内容替换为以下内容。
import { Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { Cart, CartDocument } from './schemas/cart.schema';
import { ItemDTO } from './dtos/item.dto';
@Injectable()
export class CartService {
constructor(@InjectModel('Cart') private readonly cartModel: Model<CartDocument>) { }
async createCart(userId: string, itemDTO: ItemDTO, subTotalPrice: number, totalPrice: number): Promise<Cart> {
const newCart = await this.cartModel.create({
userId,
items: [{ ...itemDTO, subTotalPrice }],
totalPrice
});
return newCart;
}
async getCart(userId: string): Promise<CartDocument> {
const cart = await this.cartModel.findOne({ userId });
return cart;
}
async deleteCart(userId: string): Promise<Cart> {
const deletedCart = await this.cartModel.findOneAndRemove({ userId });
return deletedCart;
}
private recalculateCart(cart: CartDocument) {
cart.totalPrice = 0;
cart.items.forEach(item => {
cart.totalPrice += (item.quantity * item.price);
})
}
async addItemToCart(userId: string, itemDTO: ItemDTO): Promise<Cart> {
const { productId, quantity, price } = itemDTO;
const subTotalPrice = quantity * price;
const cart = await this.getCart(userId);
if (cart) {
const itemIndex = cart.items.findIndex((item) => item.productId == productId);
if (itemIndex > -1) {
let item = cart.items[itemIndex];
item.quantity = Number(item.quantity) + Number(quantity);
item.subTotalPrice = item.quantity * item.price;
cart.items[itemIndex] = item;
this.recalculateCart(cart);
return cart.save();
} else {
cart.items.push({ ...itemDTO, subTotalPrice });
this.recalculateCart(cart);
return cart.save();
}
} else {
const newCart = await this.createCart(userId, itemDTO, subTotalPrice, price);
return newCart;
}
}
async removeItemFromCart(userId: string, productId: string): Promise<any> {
const cart = await this.getCart(userId);
const itemIndex = cart.items.findIndex((item) => item.productId == productId);
if (itemIndex > -1) {
cart.items.splice(itemIndex, 1);
return cart.save();
}
}
}
这里有很多方法。让我们一个一个地检查它们。
第一个是为当前用户创建一个新的购物车。
async createCart(userId: string, itemDTO: ItemDTO, subTotalPrice: number, totalPrice: number): Promise<Cart> {
const newCart = await this.cartModel.create({
userId,
items: [{ ...itemDTO, subTotalPrice }],
totalPrice
});
return newCart;
}
接下来的两个方法是为了获取或删除某个用户的购物车。
async getCart(userId: string): Promise<CartDocument> {
const cart = await this.cartModel.findOne({ userId });
return cart;
}
async deleteCart(userId: string): Promise<Cart> {
const deletedCart = await this.cartModel.findOneAndRemove({ userId });
return deletedCart;
}
下一个方法是在添加或删除物品,或改变物品的数量时,重新计算购物车的总数。
private recalculateCart(cart: CartDocument) {
cart.totalPrice = 0;
cart.items.forEach(item => {
cart.totalPrice += (item.quantity * item.price);
})
}
下一个方法是向购物车中添加物品。
async addItemToCart(userId: string, itemDTO: ItemDTO): Promise<Cart> {
const { productId, quantity, price } = itemDTO;
const subTotalPrice = quantity * price;
const cart = await this.getCart(userId);
if (cart) {
const itemIndex = cart.items.findIndex((item) => item.productId == productId);
if (itemIndex > -1) {
let item = cart.items[itemIndex];
item.quantity = Number(item.quantity) + Number(quantity);
item.subTotalPrice = item.quantity * item.price;
cart.items[itemIndex] = item;
this.recalculateCart(cart);
return cart.save();
} else {
cart.items.push({ ...itemDTO, subTotalPrice });
this.recalculateCart(cart);
return cart.save();
}
} else {
const newCart = await this.createCart(userId, itemDTO, subTotalPrice, price);
return newCart;
}
}
在上面的方法中,如果购物车存在,有两个选项。
- 产品存在,所以我们需要更新其数量和小计价格
- 该产品不存在,所以我们需要添加它。
无论哪种情况,我们都需要运行recalculateCart() 方法来适当地更新购物车。如果购物车不存在,我们需要创建一个新的购物车。
最后一个方法是用于从购物车中删除一个物品。
async removeItemFromCart(userId: string, productId: string): Promise<any> {
const cart = await this.getCart(userId);
const itemIndex = cart.items.findIndex((item) => item.productId == productId);
if (itemIndex > -1) {
cart.items.splice(itemIndex, 1);
this.recalculateCart(cart);
return cart.save();
}
}
与前面的方法类似,在上面的方法中,我们运行recalculateCart() ,在删除一个物品后正确地更新购物车。
创建购物车控制器方法
我们完成这个NestJS电子商务应用项目的最后一步是添加购物车控制器方法。
打开cart.controller.ts 文件,用以下内容替换它。
import { Controller, Post, Body, Request, UseGuards, Delete, NotFoundException, Param } from '@nestjs/common';
import { Roles } from 'src/auth/decorators/roles.decorator';
import { Role } from 'src/auth/enums/role.enum';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
import { RolesGuard } from 'src/auth/guards/roles.guard';
import { CartService } from './cart.service';
import { ItemDTO } from './dtos/item.dto';
@Controller('cart')
export class CartController {
constructor(private cartService: CartService) { }
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.User)
@Post('/')
async addItemToCart(@Request() req, @Body() itemDTO: ItemDTO) {
const userId = req.user.userId;
const cart = await this.cartService.addItemToCart(userId, itemDTO);
return cart;
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.User)
@Delete('/')
async removeItemFromCart(@Request() req, @Body() { productId }) {
const userId = req.user.userId;
const cart = await this.cartService.removeItemFromCart(userId, productId);
if (!cart) throw new NotFoundException('Item does not exist');
return cart;
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.User)
@Delete('/:id')
async deleteCart(@Param('id') userId: string) {
const cart = await this.cartService.deleteCart(userId);
if (!cart) throw new NotFoundException('Cart does not exist');
return cart;
}
}
在上面的代码中,我们为三个方法使用了@UseGuards 和@Roles 装饰器。这指示应用程序,客户必须登录,并且必须有一个user 角色被分配来添加或删除产品。
这就是了。如果你的操作正确,你应该有一个基本但功能齐全的NestJS eccomerce应用程序。
总结
吁!这真是一段漫长的旅程。这是一个相当长的旅程。我希望你能喜欢并学到一些关于NestJS的新知识。
尽管需要详细解释构建这个NestJS电子商务应用实例的每一步,但它是相当基本的,可以扩展到包括更多的功能。以下是你可以尝试的一些想法。
- 为产品添加分页功能
- 为收到的数据添加验证功能
- 创建一个订单模块,你可以在其中存储和管理一个特定用户的各种订单。
正如你所看到的,NestJS是一个强大而灵活的服务器端框架,可以为你的下一个项目提供一个强大和可扩展的结构。如果你想了解更多,请深入了解Nest的官方文档,并开始构建伟大的应用程序。
The postHow to build an ecommerce app with NestJSappeared first onLogRocket Blog.