NestJS 是用于构建服务器端应用程序的最佳 Node 框架之一。在本教程中,我们将探索如何构建一个简单的 NestJS 电子商务应用程序,并在此过程中展示 Nest 的许多主要功能。我们将介绍:
开始使用我们的 NestJS 电子商务应用程序
创建 NestJS 电子商务商店产品功能
创建用户管理功能
创建用户认证和授权
为我们的 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 使用了@Injectable装饰器。然后,为了在模块或控制器中提供类,它使用基于构造函数的依赖注入。
控制器处理传入的 HTTP 请求、验证参数并将响应返回给客户端。控制器应该保持干净和简单,这是下一个 Nest 功能发挥作用的地方。
服务包含 Nest 项目的大部分业务逻辑和应用程序功能。任何复杂的逻辑都应该通过服务提供。事实上,服务属于一种称为提供者的主要类别。
超过 20 万开发人员使用 LogRocket 来创造更好的数字体验了解更多 →
提供者只是一个作为依赖注入的类。可能使用的其他类型的提供程序包括存储库、工厂、助手等类。
准备好后,让我们初始化一个新的 Nest 项目。首先,我们将安装 Nest CLI。然后,我们将创建一个新项目:
npm install -g @nestjs/cli 嵌套新的nestjs-电子商务
安装完成后,导航到项目并启动它:
cd nestjs-电子商务 npm 运行开始:开发
然后,您可以通过访问http://localhost:3000/在浏览器中启动该应用程序。您应该会看到一个不错的“Hello World!” 信息。
在您进行任何更改后,该应用程序将自动重新加载。如果要手动重新启动应用程序,请改用npm run start命令。
现在我们准备开始创建商店功能。
在本节中,我们将专注于产品管理。商店产品功能将允许我们检索商店产品、添加新产品以及编辑或删除它们。
让我们从创建所需的资源开始。要创建它们,请运行以下命令:
巢g模块产品 巢g服务产品--no-spec 巢g控制器产品--no-spec
第一个命令生成一个产品模块并将其放在自己的同名目录中。
接下来的两个命令生成服务和控制器文件,并在product模块中自动导入它们。该--no-spec参数告诉 Nest 我们不想生成额外的测试文件。
运行上述命令后,我们将获得一个product包含以下文件的新目录:product.module.ts、product.service.ts和product.controller.ts.
不要错过来自 LogRocket 的精选时事通讯The Replay
使用 React 的 useEffect优化应用程序的性能
在多个 Node 版本之间切换
了解如何使用 AnimXYZ 为您的 React 应用程序制作动画
探索 Tauri,一个用于构建二进制文件的新框架
比较NestJS 与 Express.js
发现TypeScript 领域中使用的流行 ORM
现在我们有了 NestJS 电子商务商店产品功能的基本结构。在我们继续之前,我们需要设置我们的数据库。
由于我们使用 MongoDB 作为数据库,我们需要安装mongoose和@nestjs/mongoose打包。
npm install --save @nestjs/mongoose mongoose
安装完成后,打开app.module.ts并将其内容替换为以下内容:
从'@nestjs/common'导入{模块}; 从'@nestjs/mongoose'导入 { MongooseModule };// 1.1 导入猫鼬模块 从'./app.controller'导入{AppController}; 从'./app.service'导入{AppService}; 从'./product/product.module'导入{ ProductModule };// 2.1 导入产品模块 @模块({ 进口:[ MongooseModule.forRoot('mongodb://localhost/store'), // 1.2 设置数据库 ProductModule, // 2.2 添加产品模块 ], 控制器:[AppController], 提供者:[AppService], }) 导出类 AppModule {}
这是我们在上面的代码中所做的。跟随我的编号笔记:
首先,我们导入MongooseModule(1.1)并用它来建立一个新的store数据库(1.2)
其次,我们导入ProductModule(2.1) 并将其添加到imports数组 (2.2)
我们的下一步是为我们的产品模型创建一个数据库模式。
在product目录中,创建一个新schemas目录。product.schema.ts在新目录中放置一个文件,内容如下:
从“@nestjs/mongoose”导入 { Prop、Schema、SchemaFactory }; 从“猫鼬”导入{文档}; 导出类型 ProductDocument = 产品和文档; @Schema() 出口类产品{ @支柱() 名称:字符串; @支柱() 描述:字符串; @支柱() 价格:数量; @支柱() 类别:字符串; } 出口 const ProductSchema = SchemaFactory.createForClass(Product);
上面的代码为我们的产品创建了一个带有name、description、price和category属性的模式。
现在product.module.ts按以下方式编辑:
从'@nestjs/common'导入{模块}; 从'./product.controller'导入{ProductController}; 从'./product.service'导入{ ProductService }; 从'@nestjs/mongoose'导入 { MongooseModule };// 1. 导入猫鼬模块 从'./schemas/product.schema'导入{ ProductSchema };// 2. 导入产品架构 @模块({ 进口:[ MongooseModule.forFeature([{ name: 'Product', schema: ProductSchema }]) // 3. 设置 mongoose 模块以使用产品模式 ], 控制器:[ProductController], 提供者:[产品服务] }) 导出类 ProductModule {}
从我的编号注释中可以看出,在上面的代码中,我们导入了MongooseModule(1) 和ProductModule(2),然后将其设置ProductSchema为用于我们的产品模型 (3)。
除了产品架构之外,我们还需要两个用于 NestJS 电子商务应用程序的数据传输对象 (DTO) 文件。DTO 文件定义将从表单提交、搜索查询等接收的数据。
我们需要一个 DTO 用于产品创建,另一个用于产品过滤。现在让我们创建它们。
在product目录中,创建一个新dtos目录。create-product.dto.ts在这个新目录中放置一个文件,内容如下:
导出类 CreateProductDTO { 名称:字符串; 描述:字符串; 价格:数量; 类别:字符串; }
上面的 DTO 定义了一个产品对象,它具有创建新产品所需的属性。
然后,在同一目录中,创建一个filter-product.dto.ts包含以下内容的文件:
导出类 FilterProductDTO { 搜索:字符串; 类别:字符串; }
第二个 DTO 定义了一个过滤器对象,我们将使用它来按搜索查询、类别或两者过滤商店产品。
本节的所有准备工作都已完成。现在让我们为产品管理创建实际代码。
打开product.service.ts文件并将其内容替换为以下内容:
从'@nestjs/common'导入{可注射}; 从“猫鼬”导入{模型}; 从“@nestjs/mongoose”导入 { InjectModel }; 从'./schemas/product.schema'导入{产品,产品文档}; 从'./dtos/create-product.dto'导入{ CreateProductDTO }; 从'./dtos/filter-product.dto'导入{FilterProductDTO}; @Injectable() 出口类产品服务 { 构造函数(@InjectModel('Product') 私有只读 productModel: Model<ProductDocument>) { } 异步 getFilteredProducts(filterProductDTO: FilterProductDTO): Promise<Product[]> { 常量 { 类别,搜索 } = filterProductDTO; 让产品 = 等待 this.getAllProducts(); 如果(搜索){ 产品 = products.filter(产品 => product.name.includes(搜索) || product.description.includes(搜索) ); } 如果(类别){ products = products.filter(product => product.category === category) } 退货; } 异步 getAllProducts(): Promise<Product[]> { 常量产品 = 等待 this.productModel.find().exec(); 退货; } 异步 getProduct(id: string): Promise<Product> { 常量产品 = 等待 this.productModel.findById(id).exec(); 退货; } 异步 addProduct(createProductDTO: CreateProductDTO): Promise<Product> { const newProduct = await this.productModel.create(createProductDTO); 返回 newProduct.save(); } async updateProduct(id: string, createProductDTO: CreateProductDTO): Promise<Product> { const updatedProduct = 等待 this.productModel .findByIdAndUpdate(id, createProductDTO, { new: true }); 返回更新的产品; } async deleteProduct(id: string): Promise<any> { const deletedProduct = 等待 this.productModel.findByIdAndRemove(id); 返回已删除的产品; } }
让我们逐个检查上面的代码块。
首先,让我们看一下下面复制的部分:
@Injectable() 出口类产品服务 { 构造函数(@InjectModel('Product') 私有只读 productModel: Model<ProductDocument>) { } }
此代码使用@InjectModel装饰器注入所需的依赖项(产品模型)。
在下一节中,我们有两种方法:
异步 getAllProducts(): Promise<Product[]> { 常量产品 = 等待 this.productModel.find().exec(); 退货; } 异步 getProduct(id: string): Promise<Product> { 常量产品 = 等待 this.productModel.findById(id).exec(); 退货; }
第一种方法getAllProducts是获取所有产品。第二种方法getProduct是获取单一产品。我们使用标准的 Mongoose 方法来实现这些操作。
以下方法getFilteredProducts返回过滤后的产品:
异步 getFilteredProducts(filterProductDTO: FilterProductDTO): Promise<Product[]> { 常量 { 类别,搜索 } = filterProductDTO; 让产品 = 等待 this.getAllProducts(); 如果(搜索){ 产品 = products.filter(产品 => product.name.includes(搜索) || product.description.includes(搜索) ); } 如果(类别){ products = products.filter(product => product.category === category) } 退货; }
产品可以按搜索查询、类别或两者过滤。
下面的下一个方法addProduct创建一个新产品:
异步 addProduct(createProductDTO: CreateProductDTO): Promise<Product> { const newProduct = await this.productModel.create(createProductDTO); 返回 newProduct.save(); }
addProduct通过使用create-product.dto.ts文件中的类并将其保存到数据库来实现这一点。
最后两种方法是updateProduct和deleteProduct:
async updateProduct(id: string, createProductDTO: CreateProductDTO): Promise<Product> { const updatedProduct = 等待 this.productModel .findByIdAndUpdate(id, createProductDTO, { new: true }); 返回更新的产品; } async deleteProduct(id: string): Promise<any> { const deletedProduct = 等待 this.productModel.findByIdAndRemove(id); 返回已删除的产品; }
使用这些方法,您可以按 ID 查找产品,然后更新它或从数据库中删除它。
产品模块的最后一步是创建 API 端点。
我们将创建以下 API 端点:
POST store/products/— 添加新产品
GET store/products/— 获取所有产品
GET store/products/:id— 获取单个产品
PUT store/products/:id— 编辑单个产品
DELETE store/products/:id— 删除单个产品
打开product.controller.ts文件并将其内容替换为以下内容:
从“@nestjs/common”导入{控制器、发布、获取、放置、删除、正文、参数、查询、NotFoundException}; 从'./product.service'导入{ ProductService }; 从'./dtos/create-product.dto'导入{ CreateProductDTO }; 从'./dtos/filter-product.dto'导入{FilterProductDTO}; @Controller('商店/产品') 出口类产品控制器 { 构造函数(私有产品服务:产品服务){} @得到('/') 异步 getProducts(@Query() filterProductDTO: FilterProductDTO) { if (Object.keys(filterProductDTO).length) { 常量过滤产品 = 等待 this.productService.getFilteredProducts(filterProductDTO); 返回过滤的产品; } 别的 { const allProducts = 等待 this.productService.getAllProducts(); 返回所有产品; } } @Get('/:id') 异步 getProduct(@Param('id') id: string) { 常量产品 = 等待 this.productService.getProduct(id); if (!product) throw new NotFoundException('产品不存在!'); 退货; } @邮政('/') 异步 addProduct(@Body() createProductDTO: CreateProductDTO) { 常量产品 = 等待 this.productService.addProduct(createProductDTO); 退货; } @Put('/:id') async updateProduct(@Param('id') id: string, @Body() createProductDTO: CreateProductDTO) { 常量产品 = 等待 this.productService.updateProduct(id, createProductDTO); if (!product) throw new NotFoundException('产品不存在!'); 退货; } @Delete('/:id') 异步 deleteProduct(@Param('id') id: string) { 常量产品 = 等待 this.productService.deleteProduct(id); if (!product) throw new NotFoundException('产品不存在'); 退货; } }
NestJS 提供了一套完整的 JavaScript 装饰器来处理 HTTP 请求和响应(Get、Put、Body、Param等)、处理错误(NotFoundException)、定义控制器(Controller)等等。
我们从@nestjs/common文件的开头导入了我们需要的那些。我们还导入了我们已经创建的所有其他文件,我们需要:ProductService、CreateProductDTO和FilterProductDTO.
从现在开始,我不会详细解释导入。它们中的大多数都非常简单明了且不言自明。有关特定类或组件使用的更多信息,您可以查阅文档。
让我们将其余的代码分成更小的块。
首先,我们使用@Controller装饰器来设置所有端点共享的 URL 部分:
@Controller('商店/产品') 出口类产品控制器 { 构造函数(私有产品服务:产品服务){} }
我们还在上面代码的类构造函数中注入了产品服务。
接下来,我们使用@Get装饰器定义以下端点:
@得到('/') 异步 getProducts(@Query() filterProductDTO: FilterProductDTO) { if (Object.keys(filterProductDTO).length) { 常量过滤产品 = 等待 this.productService.getFilteredProducts(filterProductDTO); 返回过滤的产品; } 别的 { const allProducts = 等待 this.productService.getAllProducts(); 返回所有产品; } }
定义端点后,我们@Query在方法中使用装饰器getProducts()和对象 fromfilter-product.dto.ts从请求中获取查询参数。
如果请求中的查询参数存在,我们使用getFilteredProduct()来自产品服务的方法。如果没有这样的参数,我们使用常规getAllProducts()方法代替。
在以下端点中,我们使用@Body装饰器从请求正文中获取所需的数据,然后将其传递给addProduct()方法:
@邮政('/') 异步 addProduct(@Body() createProductDTO: CreateProductDTO) { 常量产品 = 等待 this.productService.addProduct(createProductDTO); 退货; }
在接下来的端点中,我们使用@Param装饰器从 URL 中获取产品 ID:
@Get('/:id') 异步 getProduct(@Param('id') id: string) { 常量产品 = 等待 this.productService.getProduct(id); if (!product) throw new NotFoundException('产品不存在!'); 退货; } @Put('/:id') async updateProduct(@Param('id') id: string, @Body() createProductDTO: CreateProductDTO) { 常量产品 = 等待 this.productService.updateProduct(id, createProductDTO); if (!product) throw new NotFoundException('产品不存在!'); 退货; } @Delete('/:id') 异步 deleteProduct(@Param('id') id: string) { 常量产品 = 等待 this.productService.deleteProduct(id); if (!product) throw new NotFoundException('产品不存在'); 退货; }
然后,我们使用产品服务中的适当方法来获取、编辑或删除产品。如果未找到产品,我们使用NotFoundException抛出错误消息。
我们需要为 NestJS 电子商务应用创建的下一个功能是用户管理功能。
对于用户管理功能,我们只需要一个模块和一个服务。要创建它们,请运行以下命令:
嵌套 g 模块用户 嵌套 g 服务用户 --no-spec
与之前的功能一样,我们需要一个模式和 DTO。
在userNest 生成的目录中,新建一个schemas文件夹。user.schema.ts向这个新文件夹添加一个文件,内容如下:
从“@nestjs/mongoose”导入 { Prop、Schema、SchemaFactory }; 从“猫鼬”导入{文档}; // import { Role } from 'src/auth/enums/role.enum'; 导出类型 UserDocument = 用户和文档; @Schema() 导出类用户 { @支柱() 用户名:字符串; @支柱() 电子邮件:字符串; @支柱() 密码:字符串; /* @支柱() 角色:角色[]; */ } 导出 const UserSchema = SchemaFactory.createForClass(User);
当我们实现用户授权时,将使用块末尾的注释代码。我会在本教程后面告诉你何时取消注释。
接下来,在user目录中,创建一个新dtos文件夹。在这个新文件夹中添加一个create-user-dto.ts包含以下内容的文件:
导出类 CreateUserDTO { 用户名:字符串; 电子邮件:字符串; 密码:字符串; 角色:字符串[]; }
以与产品功能相同的方式打开user.module.ts并设置架构:
从'@nestjs/common'导入{模块}; 从'@nestjs/mongoose'导入 { MongooseModule }; 从'./schemas/user.schema'导入{ UserSchema }; 从'./user.service'导入{用户服务}; @模块({ 进口:[ MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]) ], 提供者:[用户服务], 出口:[用户服务] }) 导出类 UserModule {}
在上面的代码中,我们还进行了导出UserService,以便稍后在身份验证服务中使用它。
我们还需要安装两个额外的包:bcrypt和@types/bcrypt:
npm 安装 bcrypt npm install -D @types/bcrypt
这些包使我们能够保存密码,我们将在下一节中进行处理。
现在让我们添加用户管理的逻辑。打开user.service.ts文件并将其内容替换为以下内容:
从'@nestjs/common'导入{可注射}; 从“猫鼬”导入{模型}; 从“@nestjs/mongoose”导入 { InjectModel }; 从'./schemas/user.schema'导入{用户,用户文档}; 从'./dtos/create-user.dto'导入{ CreateUserDTO }; 从'bcrypt'导入*作为bcrypt; @Injectable() 导出类用户服务 { 构造函数(@InjectModel('User') 私有只读 userModel: Model<UserDocument>) { } 异步 addUser(createUserDTO: CreateUserDTO): Promise<User> { const newUser = await this.userModel.create(createUserDTO); newUser.password = 等待 bcrypt.hash(newUser.password, 10); 返回 newUser.save(); } async findUser(username: string): Promise<User | 未定义> { const user = await this.userModel.findOne({username: username}); 返回用户; } }
我们在上面的代码中添加了两个方法。该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-local.
我们还需要安装dotenv用于管理环境变量的包:
npm 安装 dotenv
.env在根目录下创建一个文件,在里面放入如下代码:
JWT_SECRET="绝密"
稍后我们将使用这个变量。
像往常一样,让我们首先为我们的身份验证功能创建所需的资源:
嵌套 g 模块身份验证 嵌套 g 服务身份验证 --no-spec 嵌套 g 控制器身份验证 --no-spec
打开auth.service.ts文件并将其内容替换为以下内容:
从'@nestjs/common'导入{可注射}; 从'../user/user.service'导入{用户服务}; 从'bcrypt'导入*作为bcrypt; @Injectable() 导出类 AuthService { 构造函数(私有只读用户服务:用户服务){} 异步验证用户(用户名:字符串,密码:字符串):Promise<any> { const user = await this.userService.findUser(username); 常量 isPasswordMatch = 等待 bcrypt.compare( 密码, 用户密码 ); 如果(用户 && isPasswordMatch){ 返回用户; } 返回空值; } }
上面的代码给了我们一个用户验证方法,它检索用户并验证用户的密码。
在auth目录中,创建一个新strategies文件夹。在这个新文件夹中添加一个local.strategy.ts包含以下内容的文件:
从'passport-local'导入{策略}; 从“@nestjs/passport”导入 { PassportStrategy }; 从 '@nestjs/common' 导入 { Injectable, UnauthorizedException }; 从'../auth.service'导入{AuthService}; @Injectable() 导出类 LocalStrategy 扩展 PassportStrategy(Strategy) { 构造函数(私有 authService:AuthService){ 极好的(); } 异步验证(用户名:字符串,密码:字符串):Promise<any> { const user = await this.authService.validateUser(用户名,密码); 如果(!用户){ 抛出新的未授权异常(); } 返回用户; } }
这段代码做了两件事。
首先,它调用super()构造函数中的方法。如果需要,我们可以在这里传递一个选项对象。稍后我们将通过一个示例。
其次,我们添加了一个validate()方法,该方法使用validateUser()来自 auth 服务来验证用户。
现在我们将使用 JSON Web Tokens (JWT) 创建一个护照身份验证策略。这将为登录的用户返回一个 JWT,以便在后续调用受保护的 API 端点时使用。
让我们安装必要的软件包:
npm install --save @nestjs/jwt passport-jwt npm install --save-dev @types/passport-jwt
接下来,在strategies目录中,创建一个jwt.strategy.ts包含以下内容的文件:
从'passport-jwt'导入{ExtractJwt,策略}; 从“@nestjs/passport”导入 { PassportStrategy }; 从'@nestjs/common'导入{可注射}; 导入'dotenv/config' @Injectable() 导出类 JwtStrategy 扩展 PassportStrategy(Strategy) { 构造函数(){ 极好的({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 忽略过期:假, secretOrKey:process.env.JWT_SECRET, }); } 异步验证(有效负载:任何){ return { userId:payload.sub,用户名:payload.username,角色:payload.roles }; } }
在上面的代码中,我们设置了一个options具有以下属性的对象:
jwtFromRequest告诉 Passport 模块如何从请求中提取 JWT(在这种情况下,作为不记名令牌)
ignoreExpiration设置为false意味着将确保 JWT 未过期的责任委托给 Passport 模块
secretOrKey用于签署令牌
该validate()方法返回 a payload,它是解码为 JSON 的 JWT。然后,我们使用此有效负载返回具有必要属性的用户对象。
现在让我们修改auth.service.ts文件:
从'@nestjs/common'导入{可注射}; 从'../user/user.service'导入{用户服务}; 从'@nestjs/jwt'导入{ JwtService };// 1 从'bcrypt'导入*作为bcrypt; @Injectable() 导出类 AuthService { 构造函数(私有只读用户服务:用户服务,私有只读 jwtService:JwtService){} // 2 异步验证用户(用户名:字符串,密码:字符串):Promise<any> { const user = await this.userService.findUser(username); 常量 isPasswordMatch = 等待 bcrypt.compare( 密码, 用户密码 ); 如果(用户 && isPasswordMatch){ 返回用户; } 返回空值; } 异步登录(用户:任何){ const payload = { 用户名:user.username,子:user._id,角色:user.roles }; 返回 { access_token: this.jwtService.sign(payload), }; } }
上面的代码已标记,因此您可以按照我们所做的操作:
进口JwtService(见//1)
添加JwtService到构造函数中(参见 参考资料//2)。
然后我们使用该login()方法对 JWT 进行签名。
完成所有更改后,我们需要auth.module.ts按以下方式更新:
从'@nestjs/common'导入{模块}; 从'./auth.service'导入{AuthService}; 从 'src/user/user.module' 导入 { UserModule }; 从“@nestjs/passport”导入 { PassportModule }; 从'./strategies/local.strategy'导入{LocalStrategy}; 从'./strategies/jwt.strategy'导入{ JwtStrategy }; 从'./auth.controller'导入{AuthController}; 从'@nestjs/jwt'导入{ JwtModule }; 导入'dotenv/config' @模块({ 进口:[ 用户模块, 护照模块, JwtModule.register({ 秘密:process.env.JWT_SECRET, signOptions: { expiresIn: '3600s' }, }), ], 提供者:[ 身份验证服务, 本地策略, JwtStrategy ], 控制器:[AuthController], }) 导出类 AuthModule {}
在上面的代码中,我们在数组中添加了UserModule、PassportModule和。JwtModule``imports
我们还使用该register()方法提供了必要的选项:secret键和signOptions对象,它们将令牌到期时间设置为3600s或 1 小时。
最后,我们在数组中添加了LocalStrategy和。JwtStrategy``providers
要使用我们刚刚创建的策略,我们需要创建Guards。
在auth目录中,创建一个新guards文件夹。local.guard.ts向这个新文件夹添加一个文件,内容如下:
从'@nestjs/common'导入{可注射}; 从“@nestjs/passport”导入{ AuthGuard}; @Injectable() 导出类 LocalAuthGuard 扩展 AuthGuard('local') {}
同样在guards文件夹中,创建一个jwt.guard.ts包含以下内容的文件:
从'@nestjs/common'导入{可注射}; 从“@nestjs/passport”导入{ AuthGuard}; @Injectable() 导出类 JwtAuthGuard 扩展 AuthGuard('jwt') {}
我们将在一分钟内了解如何使用这些守卫。但首先,让我们创建用户授权功能。
为了在我们的 NestJS 电子商务应用程序中实现此功能,我们将使用基于角色的访问控制。
对于此功能,我们需要三个文件:role.enum.ts、roles.decorator.ts和roles.guard.ts. 让我们从role.enum.ts文件开始。
在auth目录中,创建一个新enums文件夹。在这个新文件夹中添加一个role.enum.ts包含以下内容的文件:
导出枚举角色 { 用户 = '用户', 管理员 = '管理员', }
这表示注册用户的可用角色。
现在您可以返回到user.schema.ts我们之前创建的文件并取消注释代码。
接下来,在auth目录中,创建一个新decorators文件夹。在这个新文件夹中添加一个roles.decorator.ts包含以下内容的文件:
从“@nestjs/common”导入 { SetMetadata }; 从'../enums/role.enum'导入{角色}; 出口 const ROLES_KEY = '角色'; export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
在上面的代码中,我们用来SetMetadata()创建装饰器。
最后,在guards目录中,创建一个roles.guard.ts文件,内容如下:
从“@nestjs/common”导入 { Injectable, CanActivate, ExecutionContext }; 从'@nestjs/core'导入{反射器}; 从'../enums/role.enum'导入{角色}; 从'../decorators/roles.decorator'导入{ ROLES_KEY }; @Injectable() 导出类 RolesGuard 实现 CanActivate { 构造函数(私有反射器:反射器){} canActivate(上下文:ExecutionContext):布尔{ const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [ context.getHandler(), context.getClass(), ]); 如果(!requiredRoles){ 返回真; } 常量 { 用户 } = context.switchToHttp().getRequest(); return requiredRoles.some((role) => user.roles?.includes(role)); } }
在上面的代码中,我们使用了Reflector帮助类来访问路由的角色。我们还将执行上下文切换到 HTTPswitchToHttp()以user使用getRequest(). 最后,我们返回了用户的角色。
我们在本节中的最后一步是创建控制器方法。打开auth.controller.ts文件并将其内容替换为以下内容:
从“@nestjs/common”导入{控制器、请求、获取、发布、正文、UseGuards}; 从 'src/user/dtos/create-user.dto' 导入 { CreateUserDTO }; 从'src/user/user.service'导入{用户服务}; 从'./auth.service'导入{AuthService}; 从'./guards/local-auth.guard'导入{LocalAuthGuard}; 从'./guards/jwt-auth.guard'导入{JwtAuthGuard}; 从'./decorators/roles.decorator'导入{角色}; 从'./enums/role.enum'导入{角色}; 从'./guards/roles.guard'导入{ RolesGuard }; @Controller('auth') 导出类 AuthController { 构造函数(私有 authService:AuthService,私有 userService:UserService){} @Post('/注册') 异步寄存器(@Body() createUserDTO: CreateUserDTO) { 常量用户 = 等待 this.userService.addUser(createUserDTO); 返回用户; } @UseGuards(LocalAuthGuard) @Post('/登录') 异步登录(@Request()请求){ 返回 this.authService.login(req.user); } @UseGuards(JwtAuthGuard,RolesGuard) @角色(角色.用户) @Get('/用户') getProfile(@Request() 请求) { 返回请求用户; } @UseGuards(JwtAuthGuard,RolesGuard) @角色(角色.管理员) @Get('/admin') getDashboard(@Request() 请求) { 返回请求用户; } }
我们在上面的代码中有四个端点:
POSTauth/register用于创建新用户
POST
auth/login
用于登录注册用户
为了验证用户,我们使用LocalAuthGuard
GET
auth/user
用于访问用户的个人资料
我们用来JwtGuard验证用户
我们使用RolesGuardplus@Roles装饰器根据用户的角色提供适当的授权
GETauth/admin用于访问管理仪表板
我们还使用了JwtGuard和RolesGuard在前面的端点中所做的那样
我们将添加到项目中的最后一个功能是基本的购物车功能。
让我们为下一部分创建我们需要的资源:
巢g模块推车 嵌套 g 服务车--no-spec 嵌套 g 控制器购物车 --no-spec
对于商店购物车功能,我们需要两种模式:一种描述购物车中的产品,另一种描述购物车本身。
像往常一样,在cart目录中创建一个新schemas文件夹。在这个新文件夹中添加一个item.schema.ts包含以下内容的文件:
从“@nestjs/mongoose”导入 { Prop、Schema、SchemaFactory }; 从'猫鼬'导入{文档,SchemaTypes}; 导出类型 ItemDocument = Item & Document; @Schema() 出口类项目{ @Prop({ type: SchemaTypes.ObjectId, ref: 'Product' }) 产品ID:字符串; @支柱() 名称:字符串; @支柱() 数量:数量; @支柱() 价格:数量; @支柱() subTotalPrice:数字; } 出口 const ItemSchema = SchemaFactory.createForClass(Item);
在上面的代码中,在属性的@Prop装饰器中productId,我们定义了一个对象 id 模式类型并添加了对产品的引用。这意味着我们将使用产品的 id 作为productId值。
下一个模式用于购物车。在schemas目录中,创建一个cart.schema.ts包含以下内容的文件:
从“@nestjs/mongoose”导入 { Prop、Schema、SchemaFactory }; 从'猫鼬'导入{文档,SchemaTypes}; 从'./item.schema'导入{项目}; 导出类型 CartDocument = 购物车 & 文档; @Schema() 出口类购物车{ @Prop({ type: SchemaTypes.ObjectId, ref: 'User' }) 用户ID:字符串; @支柱() 项目:项目[]; @支柱() 总价格:数字; } 出口 const CartSchema = SchemaFactory.createForClass(Cart);
在这里,我们对属性使用相同的技术,该userId属性将作为用户 id 的值。对于items属性,我们使用我们的Item模式来定义类型为 的项目数组Item。
最后,让我们创建项目 DTO。在user目录中,创建一个新dtos文件夹并添加一个item.dto.ts包含以下内容的文件:
导出类 ItemDTO { 产品ID:字符串; 名称:字符串; 数量:数量; 价格:数量; }
在我们进入业务逻辑之前,我们需要将购物车模式添加到购物车模块。打开cart.module.ts文件并将其配置为使用购物车模式,如下所示:
从'@nestjs/common'导入{模块}; 从'./cart.controller'导入{ CartController}; 从'./cart.service'导入{ CartService }; 从'@nestjs/mongoose'导入 { MongooseModule }; 从'./schemas/cart.schema'导入{ CartSchema }; @模块({ 进口:[ MongooseModule.forFeature([{ name: 'Cart', schema: CartSchema }]) ], 控制器:[CartController], 提供者:[CartService] }) 导出类 CartModule {}
现在让我们创建购物车管理逻辑。打开cart.service.ts文件并将其内容替换为以下内容:
从'@nestjs/common'导入{可注射}; 从“猫鼬”导入{模型}; 从“@nestjs/mongoose”导入 { InjectModel }; 从 './schemas/cart.schema' 导入 { Cart, CartDocument }; 从'./dtos/item.dto'导入{ItemDTO}; @Injectable() 出口类购物车服务 { 构造函数(@InjectModel('Cart') 私有只读 cartModel: Model<CartDocument>) { } 异步 createCart(userId: string, itemDTO: ItemDTO, subTotalPrice: number, totalPrice: number): Promise<Cart> { const newCart = 等待 this.cartModel.create({ 用户身份, 项目:[{ ...itemDTO,subTotalPrice }], 总价 }); 返回新购物车; } 异步 getCart(userId: string): Promise<CartDocument> { const cart = await this.cartModel.findOne({ userId }); 退货车; } async deleteCart(userId: string): Promise<Cart> { const deletedCart = await this.cartModel.findOneAndRemove({ userId }); 返回已删除的购物车; } 私人重新计算购物车(购物车:购物车文档){ 购物车.totalPrice = 0; cart.items.forEach(item => { cart.totalPrice += (item.quantity * item.price); }) } 异步 addItemToCart(userId: string, itemDTO: ItemDTO): Promise<Cart> { const { productId, 数量, 价格 } = itemDTO; 常量 subTotalPrice = 数量 * 价格; const cart = await this.getCart(userId); 如果(购物车){ 常量 itemIndex = cart.items.findIndex((item) => item.productId == productId); if (itemIndex > -1) { 让 item = cart.items[itemIndex]; item.quantity = Number(item.quantity) + Number(quantity); item.subTotalPrice = item.quantity * item.price; cart.items[itemIndex] = item; this.recalculateCart(cart); 返回购物车.save(); } 别的 { cart.items.push({ ...itemDTO, subTotalPrice }); this.recalculateCart(cart); 返回购物车.save(); } } 别的 { const newCart = await this.createCart(userId, itemDTO, subTotalPrice, price); 返回新购物车; } } 异步 removeItemFromCart(userId: string, productId: string): Promise<any> { const cart = await this.getCart(userId); 常量 itemIndex = cart.items.findIndex((item) => item.productId == productId); if (itemIndex > -1) { 购物车.items.splice(itemIndex, 1); 返回购物车.save(); } } }
这里有很多方法。让我们一一检查。
第一个是为当前用户创建一个新的购物车:
异步 createCart(userId: string, itemDTO: ItemDTO, subTotalPrice: number, totalPrice: number): Promise<Cart> { const newCart = 等待 this.cartModel.create({ 用户身份, 项目:[{ ...itemDTO,subTotalPrice }], 总价 }); 返回新购物车; }
接下来的两种方法用于获取或删除特定用户的购物车:
异步 getCart(userId: string): Promise<CartDocument> { const cart = await this.cartModel.findOne({ userId }); 退货车; } async deleteCart(userId: string): Promise<Cart> { const deletedCart = await this.cartModel.findOneAndRemove({ userId }); 返回已删除的购物车; }
下一个方法是在添加或删除商品或更改商品数量时重新计算购物车总数:
私人重新计算购物车(购物车:购物车文档){ 购物车.totalPrice = 0; cart.items.forEach(item => { cart.totalPrice += (item.quantity * item.price); }) }
下一个方法是将商品添加到购物车:
异步 addItemToCart(userId: string, itemDTO: ItemDTO): Promise<Cart> { const { productId, 数量, 价格 } = itemDTO; 常量 subTotalPrice = 数量 * 价格; const cart = await this.getCart(userId); 如果(购物车){ 常量 itemIndex = cart.items.findIndex((item) => item.productId == productId); if (itemIndex > -1) { 让 item = cart.items[itemIndex]; item.quantity = Number(item.quantity) + Number(quantity); item.subTotalPrice = item.quantity * item.price; cart.items[itemIndex] = item; this.recalculateCart(cart); 返回购物车.save(); } 别的 { cart.items.push({ ...itemDTO, subTotalPrice }); this.recalculateCart(cart); 返回购物车.save(); } } 别的 { const newCart = await this.createCart(userId, itemDTO, subTotalPrice, price); 返回新购物车; } }
在上面的方法中,如果购物车存在,有两种选择:
产品存在,所以我们需要更新它的数量和小计价格
该产品不存在,所以我们需要添加它
无论哪种方式,我们都需要运行该recalculateCart()方法来适当地更新购物车。如果购物车不存在,我们需要创建一个新的。
最后一种方法是从购物车中删除商品:
异步 removeItemFromCart(userId: string, productId: string): Promise<any> { const cart = await this.getCart(userId); 常量 itemIndex = cart.items.findIndex((item) => item.productId == productId); if (itemIndex > -1) { 购物车.items.splice(itemIndex, 1); this.recalculateCart(cart); 返回购物车.save(); } }
与前面的方法类似,在上面的方法中,我们运行recalculateCart()以在删除商品后正确更新购物车。
我们完成这个 NestJS 电子商务应用程序项目的最后一步是添加购物车控制器方法。
打开cart.controller.ts文件并将其内容替换为以下内容:
从“@nestjs/common”导入{控制器、帖子、正文、请求、UseGuards、删除、NotFoundException、参数}; 从'src/auth/decorators/roles.decorator'导入{角色}; 从'src/auth/enums/role.enum'导入{角色}; 从'src/auth/guards/jwt-auth.guard'导入{JwtAuthGuard}; 从'src/auth/guards/roles.guard'导入{ RolesGuard }; 从'./cart.service'导入{ CartService }; 从'./dtos/item.dto'导入{ItemDTO}; @Controller('购物车') 导出类 CartController { 构造函数(私人购物车服务:购物车服务){} @UseGuards(JwtAuthGuard,RolesGuard) @角色(角色.用户) @邮政('/') 异步 addItemToCart(@Request() req, @Body() itemDTO: ItemDTO) { 常量 userId = req.user.userId; const cart = await this.cartService.addItemToCart(userId, itemDTO); 退货车; } @UseGuards(JwtAuthGuard,RolesGuard) @角色(角色.用户) @删除('/') 异步 removeItemFromCart(@Request() req, @Body() { productId }) { 常量 userId = req.user.userId; const cart = await this.cartService.removeItemFromCart(userId, productId); if (!cart) throw new NotFoundException('Item不存在'); 退货车; } @UseGuards(JwtAuthGuard,RolesGuard) @角色(角色.用户) @Delete('/:id') 异步 deleteCart(@Param('id') userId: string) { 常量购物车 = 等待 this.cartService.deleteCart(userId); if (!cart) throw new NotFoundException('Cart does not exist'); 退货车; } }
在上面的代码中,我们为这三个方法使用了@UseGuards和@Roles装饰器。这指示应用程序客户必须登录并且必须user分配一个角色才能添加或删除产品。
而已。如果您正确地遵循,您应该拥有一个基本但功能齐全的 NestJS eccomerce 应用程序。
呸!这是一段相当长的旅程。我希望你喜欢并学到了一些关于 NestJS 的新东西。
尽管需要详细解释构建此 NestJS 电子商务应用程序示例的每个步骤,但它非常基本,可以扩展以包含更多功能。以下是您可以尝试的一些想法:
为产品添加分页
为接收到的数据添加验证
创建一个订单模块,您可以在其中存储和管理特定用户的各种订单
如您所见,NestJS 是一个强大且灵活的服务器端框架,可以为您的下一个项目提供强大且可扩展的结构。如果您想了解更多信息,请深入了解Nest 官方文档并开始构建出色的应用程序。
LogRocket就像一个用于网络和移动应用程序和网站的 DVR,几乎可以记录您的电子商务应用程序上发生的所有事情。LogRocket 不会猜测用户不转化的原因,而是主动揭示阻碍您的渠道转化的问题的根本原因,例如 JavaScript 错误或死点击。LogRocket 还监控您的应用程序的性能,报告客户端 CPU 负载、客户端内存使用情况等指标。