如何用NestJS建立一个电子商务应用程序

857 阅读24分钟

NestJS是构建服务器端应用程序的最佳Node框架之一。在本教程中,我们将探讨如何建立一个简单的NestJS电子商务应用程序,并在此过程中展示Nest的许多主要功能。我们将涵盖。

开始使用我们的NestJS电子商务应用程序

默认情况下,NestJS在后台使用Express,尽管你可以选择使用Fastify代替。Nest提供了一个坚实的应用程序架构,而Express和Fastify是强大的HTTP服务器框架,具有无数的应用程序开发功能。

拥有强大的架构使你有能力建立高度可扩展、可测试、松散耦合和易于维护的应用程序。使用Nest可以让你的Node.js后端更上一层楼。

Nest在很大程度上受到Angular的启发,并借用了它的许多概念。如果你已经使用Angular,Nest可能是一个完美的匹配。

要遵循本教程,你至少需要对Node、MongoDB、TypeScript和Nest有基本了解和经验。请确保你的机器上安装了NodeMongoDB

你应该知道的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

最后两个方法是updateProductdeleteProduct

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端点。

  • POSTstore/products/ - 添加新产品
  • GETstore/products/ - 获得所有产品
  • GETstore/products/:id - 获取单个产品
  • PUTstore/products/:id - 编辑单个产品
  • DELETEstore/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模块false
  • secretOrKey 是用来签署令牌的

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 数组中添加了LocalStrategyJwtStrategy

创建本地和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;
  }
}

在上面的代码中,我们有四个端点。

  • POSTauth/register 用来创建一个新的用户
  • POSTauth/login 是用来登录注册用户的
    • 为了验证该用户,我们使用LocalAuthGuard
  • GETauth/user 用于访问用户的个人资料
    • 我们使用JwtGuard 来验证用户
    • 我们使用RolesGuard 加上@Roles 装饰器,根据用户的角色提供适当的授权
  • GETauth/admin 被用来访问管理仪表板
    • 我们还使用了JwtGuardRolesGuard ,就像在前面的端点中做的那样

为我们的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;
  }
}

在上面的方法中,如果购物车存在,有两个选项。

  1. 产品存在,所以我们需要更新其数量和小计价格
  2. 该产品不存在,所以我们需要添加它。

无论哪种情况,我们都需要运行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.