NestJS 构建下一代 Node.js 应用

NestJS 构建下一代 Node.js 应用

2023.11月的某一天,我在学习小满哥B站的视频,无意中发现Nestjs这个东西,然后就跑进知乎,在乎uu的介绍和描述,我了解到了这个新奇的东西——像极了Spring

我在此前学习过ExpressKoaNodejs框架,他们都是用前端的语言来写服务端,缺少了后端的如IOC控制反转DI依赖注入AOP面向切片编程等等思想的实现(大神可以手写模拟实现,我是菜鸡我不行,以后会试试,毕竟是非常优秀的思想),而我暂时又不想学Java的像SpringBoot的框架系列的技术,这时候我想Nestjs是一个不错的选择。

以下是我学习小满哥的Nestjs视频的文档记录,当然也会有一些我的见解。

介绍

Nestjs是一个基于ExpressFastify做的框架,集成了TypeScript,实现了各种特性,继承了两大框架的优点和生态。特别是nest/cli的一系列命令,能够让你快速搭建服务。

  • 基于 Express Fastify框架

NestJS构建在Express Fastify 之上,这两者都是 Node.js 中非常流行的 HTTP 框架。这使得 NestJS 具有广泛的社区支持和成熟的中间件生态系统。你可以选择使用其中一个或者根据需要在它们之间切换。

  • 集成 TypeScript

NestJS是一个使用 TypeScript编写的框架,这意味着你可以利用 TypeScript的强类型系统、面向对象的编程范式以及其他高级语言特性。TypeScript让你在编写代码时能够更早地捕捉潜在的错误,提高了代码的可读性和可维护性。

  • 依赖注入模块化

​ 通过依赖注入,你可以更灵活地组织你的代码,使得组件更加可测试和可维护。NestJS使用模块的概念,允许你将应用程序拆分为一系列模块,每个模块都负责一个特定的功能。模块化的设计使得应用程序的组织结构更加清晰,也方便团队协作。

初始化项目

  • 脚手架安装

    npm install -g @nestjs/cli
    
  • 创建项目

    nest new <project-name>
    

拦截器(Interceptor)

通过以下代码的学习,不难看出拦截器和管道校验都有Express中间件的影子。

响应拦截器实现

  • src目录下新建common文件夹,文件夹下创建response.ts文件

    import { CallHandler, Injectable, NestInterceptor } from '@nestjs/common';
    import { Observable } from 'rxjs';
    import { map } from 'rxjs/operators';
    
    interface Data<T> {
      data: T;
    }
    
    // 响应拦截器根据模板返回数据
    @Injectable()
    export class Response<T> implements NestInterceptor {
      intercept(context: any, next: CallHandler): Observable<Data<T>> {
        return next.handle().pipe(
          map((data) => {
            return {
              code: 200,
              data,
              message: '冲就完事了',
              success: true,
            };
          })
        );
      }
    }
    
    
  • 使用全局响应拦截器

    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { Response } from './common/response';
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
    
      // 全局注册响应拦截器
      app.useGlobalInterceptors(new Response());
      await app.listen(3000);
    }
    bootstrap();
    

异常拦截器实现

  • common文件夹下创建filter.ts文件

    import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
    import { Request, Response } from 'express';
    
    // 异常拦截器
    @Catch(HttpException)
    export class HttpFilter implements ExceptionFilter {
      catch(exception: HttpException, host: ArgumentsHost) {
        const { getResponse, getRequest } = host.switchToHttp();
        const response = getResponse<Response>();
        const request = getRequest<Request>();
    
        const status = exception.getStatus();
    	
        // 优先取全局的异常管道符校验失败的响应信息,前提是使用了全局的校验管道
        const message = exception.getResponse() || exception.message;
    
        response.status(status).json({
          status,
          data: message,
          success: false,
          time: new Date(),
          path: request.url,
        });
      }
    }
    
  • 使用全局异常拦截器

    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { Response } from './common/response';
    import { HttpFilter } from './common/filter';
    import { ValidationPipe } from '@nestjs/common';
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
    
      // 全局注册异常拦截器
      app.useGlobalFilters(new HttpFilter());
    
      // 全局注册响应拦截器
      app.useGlobalInterceptors(new Response());
    
      await app.listen(3000);
    }
    bootstrap();
    

管道(Pipe)

类型转换管道

import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
import { AppService } from './app.service';

// pipe系列
// ValidationPipe  // 验证
// ParseIntPipe //  int 整型类型 -- number
// ParseFloatPipe // float 浮点类型
// ParseBoolPipe // boolean 布尔类型
// ParseArrayPipe // array 数组类型
// ParseUUIDPipe // UUID 类型
// ParseEnumPipe // enum 枚举类型
// DefaultValuePipe // any 默认值类型
// ...还有什么文件类型等等,可以进行转换

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get(':id')
  getHello(@Param('id', ParseIntPipe) id: number): any {
    console.log('id type ==> ' + typeof id); // out: id type ==> number
    return this.appService.getHello();
  }
}

类型校验管道

有两种实现方式:

1、利用两个库来实现颗粒度更细的校验。

2、使用全局的管道校验。

实现方式一
  • 安装class-transformerclass-validator

    pnpm i class-transformer class-validator --save
    
  • 快速生成CRUD

    nest g res login
    
  • 快速生成pipe

    nest g pi login
    
  • login.controller.ts

    import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
    import { LoginService } from './login.service';
    import { CreateLoginDto } from './dto/create-login.dto';
    import { UpdateLoginDto } from './dto/update-login.dto';
    import { LoginPipe } from './login.pipe';
    
    @Controller('login')
    export class LoginController {
      constructor(private readonly loginService: LoginService) {}
    
      // 对接受的dto的对象进行管道处理
      @Post('create')
      create(@Body(LoginPipe) createLoginDto: CreateLoginDto) {
         console.log(createLoginDto);
         return this.loginService.create(createLoginDto);
      }
    }
    
  • create-login.dto.ts

    import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
    
    export class CreateLoginDto {
      @IsNotEmpty()
      @IsString()
      name: string;
    
      @IsNumber()
      age: number;
    }
    
  • login.pip.ts

    import { ArgumentMetadata, HttpException, HttpStatus, Injectable, PipeTransform } from '@nestjs/common';
    import { plainToInstance } from 'class-transformer';
    import { validate } from 'class-validator';
    
    @Injectable()
    export class LoginPipe implements PipeTransform {
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      async transform(value: any, metadata: ArgumentMetadata) {
        // 将接收到的请求数据转换为指定的 DTO 实例
        const DTO = plainToInstance(metadata.metatype, value);
          
        // 对实例中字段标注的装饰器进行校验,返回一个错误信息的数组
        const errors = await validate(DTO);
    
         // 如果errors数组中存在元素,说明有字段类型校验失败
        if (errors.length) {
          throw new HttpException(errors, HttpStatus.BAD_REQUEST);
        }
        return value;
      }
    }
    
实现方式二

全局的方法,则不需要再写方法一里的第一个和第二个文件了。

  • create-login.dto.ts

    import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
    
    export class CreateLoginDto {
      @IsNotEmpty()
      @IsString()
      name: string;
    
      @IsNumber()
      age: number;
    }
    
  • main.ts

    这里使用全局的管道进行类型的校验,只要传入的与标注的类型对不上,就会响应错误的消息。

    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { Response } from './common/response';
    import { HttpFilter } from './common/filter';
    import { ValidationPipe } from '@nestjs/common';
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
    
      // 全局注册异常拦截器
      app.useGlobalFilters(new HttpFilter());
    
      // 全局注册响应拦截器
      app.useGlobalInterceptors(new Response());
    
      // 全局注册类型校验管道  搭配class-validator库使用
      app.useGlobalPipes(new ValidationPipe());
    
      await app.listen(3000);
    }
    bootstrap();
    

静态目录(Static Public)

这个简单,直接上代码!与Express没啥区别。做了一下TS的类型封装而已。

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Response } from './common/response';
import { HttpFilter } from './common/filter';
import { ValidationPipe } from '@nestjs/common';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';

async function bootstrap() {
  // 添加NestExpressApplication泛型,扩展属性的类型推导。
  // 不然eslint会报错useStaticAssets不存在。
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  // 使用静态目录。src/public下的所有文件都可以直接访问
  app.useStaticAssets(join(__dirname, '..', 'public'));

  // 全局注册异常拦截器
  app.useGlobalFilters(new HttpFilter());

  // 全局注册响应拦截器
  app.useGlobalInterceptors(new Response());

  // 全局注册类型校验管道  搭配class-validator库使用
  app.useGlobalPipes(new ValidationPipe());

  await app.listen(3000);
}
bootstrap();

跨域(Cors)

跨域在Nestjs中有内置的一个方法可以实现,一行代码就解决了跨域,当然你也可以使用第三方库pnpm i cors @types/cors --save

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Response } from './common/response';
import { HttpFilter } from './common/filter';
import { ValidationPipe } from '@nestjs/common';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
// import * as cors from 'cors';

async function bootstrap() {
  // 添加NestExpressApplication泛型,扩展属性的类型推导。
  // 不然eslint会报错useStaticAssets不存在。
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  // 配置允许跨域
  app.enableCors();
  
  // 使用第三方库cors实现跨域
  // app.use(cors());

  // 使用静态目录。src/public下的所有文件都可以直接访问
  app.useStaticAssets(join(__dirname, '..', 'public'));

  // 全局注册异常拦截器
  app.useGlobalFilters(new HttpFilter());

  // 全局注册响应拦截器
  app.useGlobalInterceptors(new Response());

  // 全局注册类型校验管道  搭配class-validator库使用
  app.useGlobalPipes(new ValidationPipe());

  await app.listen(3000);
}
bootstrap();

守卫(Guard)

个人觉得,使用守卫做权限校验是十分不错的,全局的拦截器、管道、守卫,都有中间件的思想,只需要new一个实例过去,在特定时期会调用。当然也提供了局部的使用。

  • 创建guard的测试类,使用命令快速生成CRUD

    nest g res guard
    
  • 创建角色守卫

    nest g gu role
    
  • role.guard.ts

    context:请求守卫的上下文信息。

    其中,context.getHandler()可以获取到调用的Service中的方法,配合Reflector可以找到Controller请求的路由方法的元数据,然后获取一些信息,如装饰器@SetMetaData的值。

    context.switchToHttp()看名称就知道,这是将上下文切换到HTTP请求,在HTTP请求中存在三个老朋友(RequestResponseNext)。

    Request中获取到Get请求的query中的值,接下来就可以判断了。

    import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
    import { Observable } from 'rxjs';
    import type { Request } from 'express';
    import { Reflector } from '@nestjs/core';
    
    type TRoles = 'superAdmin' | 'admin' | 'user' | 'vipUser';
    
    // 定义局部的请求守卫,权限校验
    @Injectable()
    export class RoleGuard implements CanActivate {
      constructor(private ReflectorX: Reflector) {}
      canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
        const roles = this.ReflectorX.get<TRoles[]>('role', context.getHandler());
        const req = context.switchToHttp().getRequest<Request>();
        const { role } = req.query as { role: TRoles };
    
        if (roles && !roles.includes(role)) return false;
        return true;
      }
    }
    
  • 局部守卫实现(guard.controller.ts)

    这里是对整个/guard路由下的使用了权限守卫,你也可以精确到某一个请求路由上,只是装饰器的位置不一样。

    import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, SetMetadata } from '@nestjs/common';
    import { GuardService } from './guard.service';
    import { CreateGuardDto } from './dto/create-guard.dto';
    import { UpdateGuardDto } from './dto/update-guard.dto';
    import { RoleGuard } from 'src/role/role.guard';
    
    @Controller('guard')
    @UseGuards(RoleGuard)
    export class GuardController {
      constructor(private readonly guardService: GuardService) {}
    
      @Get('test')
      @SetMetadata('role', ['superAdmin', 'admin'])
      findAll() {
        return this.guardService.findAll();
      }
    }
    
  • 全局守卫实现(main.ts)

    当注册了全局的守卫,所有的请求的都会走RoleGuard

    这里可能同学们有一个疑问——如果我全局守卫和局部守卫同时使用会怎么样?

    答:局部守卫优先级高于全局守卫,走过了局部守卫,如果为true,还会走全局守卫,这是一个流程。

    import { NestFactory, Reflector } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { Response } from './common/response';
    import { HttpFilter } from './common/filter';
    import { ValidationPipe } from '@nestjs/common';
    import { NestExpressApplication } from '@nestjs/platform-express';
    import { join } from 'path';
    // import * as cors from 'cors';
    import { RoleGuard } from './role/role.guard';
    
    async function bootstrap() {
      // 添加NestExpressApplication泛型,扩展属性的类型推导。
      // 不然eslint会报错useStaticAssets不存在。
      const app = await NestFactory.create<NestExpressApplication>(AppModule);
    
      // 配置允许跨域
      app.enableCors();
    
      // 使用第三方库cors实现跨域
      // app.use(cors());
    
      // 使用静态目录。src/public下的所有文件都可以直接访问
      app.useStaticAssets(join(__dirname, '..', 'public'));
    
      // 全局注册异常拦截器
      app.useGlobalFilters(new HttpFilter());
    
      // 全局注册响应拦截器
      app.useGlobalInterceptors(new Response());
    
      // 全局注册类型校验管道  搭配class-validator库使用
      app.useGlobalPipes(new ValidationPipe());
    
      // 全局注册权限校验守卫,在controller的路由上使用装饰器@SetMetaData,然后在RoleGuard里面取值做判断
      app.useGlobalGuards(new RoleGuard(new Reflector()));
    
      await app.listen(3000);
    }
    bootstrap();
    

接口文档构建(Swagger)

只写接口,不写接口文档的后端程序员,你们认为是一个合格的后端吗?我是忍不了🤬。

接下来使用将在Nestjs中使用Swagger来快速生成API接口文档。

  • 引入依赖

    pnpm i @nestjs/swagger swagger-ui-express -D
    
  • 配置并创建文档路由站点

    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { NestExpressApplication } from '@nestjs/platform-express';
    import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
    
    async function bootstrap() {
      // 添加NestExpressApplication泛型,扩展属性的类型推导。
      // 不然eslint会报错useStaticAssets不存在。
      const app = await NestFactory.create<NestExpressApplication>(AppModule);
    
      // swagger使用
      const options = new DocumentBuilder()
        // 添加文档查看jwt校验
        .addBearerAuth()
        // 设置文档标题
        .setTitle('小胡的Nestjs-Stduy-Doc-Website')
        // 设置接口文档的描述
        .setDescription('我是接口文档的描述')
        // 设置接口版本
        .setVersion('1')
        // 打包
        .build();
    
      // 创建文档
      const document = SwaggerModule.createDocument(app, options);
    
      // 安装,访问路径:http://localhost:3000/api-docs
      SwaggerModule.setup('api-docs', app, document);
    
      await app.listen(3000);
    }
    bootstrap();
    
  • 接口中装饰器的使用

    在这里可以查看更多用法:👉Swagger

    import { Controller, Get, UseGuards, SetMetadata } from '@nestjs/common';
    import { GuardService } from './guard.service';
    import { RoleGuard } from 'src/role/role.guard';
    import { ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
    
    @Controller('guard')
    @UseGuards(RoleGuard)
    @ApiBearerAuth() // 权限,token校验 文档内的接口请求
    @ApiTags('guard守卫的tag') // 这是/guard下的路由的标题
    export class GuardController {
      constructor(private readonly guardService: GuardService) { }
    
      @Get('test')
      // 学习更多使用:https://docs.nestjs.com/openapi/introduction
      @ApiQuery({
        name: 'role',
        required: true,
        description: '这是一个测试角色权限校验的一个路由值,query参数描述',
      })
      @ApiParam({
        name: 'test',
        description: 'api的param参数描述',
      })
      // 返回的信息文档
      @ApiResponse({
        status: 400,
        description: '小黑子露出鸡脚了吧',
      })
      // api怎么操作的一些描述
      @ApiOperation({
        summary: 'findAll接口',
        description: '描述啦啦啦',
      })
      @SetMetadata('role', ['superAdmin', 'admin'])
      findAll() {
        return this.guardService.findAll();
      }
    }
    
  • 实体类属性字段上的装饰器使用

    import { ApiProperty } from '@nestjs/swagger';
    
    export class CreateGuardDto {
      // 更多可以查看SchemaObject类型定义的一些字段
      @ApiProperty({
        required: false,
        default: '小胡',
      })
      name: string;
    
      @ApiProperty({
        required: false,
        default: 18,
      })
      age: number;
    
      @ApiProperty({
        default: 'JK',
        type: String,
        required: true,
      })
      hobby: string;
    }
    

思考

以下是使用装饰器来帮助快速生成接口文档,其实你这时候就会发现,如果这样写,一个接口或者属性字段得写这样一长串的装饰器,还是有点尴尬的。

这时候你可以使用一种新的方法去生成,在一个路由文件夹下新建一个swagger.json文件,然后在main.ts中读取到所有的swagger.json文件(你也可以手动一个个引入),将它们合并成一个对象,最后生成文档。

这样写有一个弊端,那就是没有了提示,全靠个人熟练度🤭,不要担心,还有别的方法。

1、可以在编辑器里找插件,这样你可以在编辑器里设置一下就可以有字段提示,以及在编辑器内部打开预览(我看了一下VSCode里是有插件的,但是我没试过,理论上来讲可以实现)。

2、再不济能下一个Swagger Editor这个是真量身定做的,写起来肯定嘎嘎快,而且能实时预览,写完粘贴到项目里在用第三方库生成即可。

说了这么多方法,你还不写Api接口文档,我打洗你喔😄。

  • swagger1.json

    // swagger1.json
    {
      "openapi": "3.0.0",
      "info": {
        "title": "API One",
        "version": "1.0.0"
      },
      "paths": {
        "/api/endpoint1": {
          "get": {
            "summary": "Endpoint 1",
            "responses": {
              "200": {
                "description": "Successful response"
              }
            }
          }
        }
      }
    }
    
  • swagger2.json

    // swagger2.json
    {
      "openapi": "3.0.0",
      "info": {
        "title": "API Two",
        "version": "1.0.0"
      },
      "paths": {
        "/api/endpoint2": {
          "get": {
            "summary": "Endpoint 2",
            "responses": {
              "200": {
                "description": "Successful response"
              }
            }
          }
        }
      }
    }
    
  • 集中载入生成

    import { NestFactory } from '@nestjs/core';
    import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
    import { AppModule } from './app.module';
    import * as fs from 'fs';
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
    
      // Load and merge multiple JSON files
      const swaggerJson1 = JSON.parse(fs.readFileSync('swagger1.json', 'utf-8'));
      const swaggerJson2 = JSON.parse(fs.readFileSync('swagger2.json', 'utf-8'));
    
      const combinedSwagger = {
        ...swaggerJson1,
        ...swaggerJson2,
        paths: {
          ...swaggerJson1.paths,
          ...swaggerJson2.paths,
        },
      };
    
      // Setup Swagger options
      const options = new DocumentBuilder()
        .setTitle('Combined API')
        .setDescription('API documentation for multiple services')
        .setVersion('1.0')
        .build();
    
      // Combine Swagger document and serve Swagger UI
      const document = SwaggerModule.createDocument(app, options, combinedSwagger);
      SwaggerModule.setup('api-docs', app, document);
    
      await app.listen(3000);
    }
    bootstrap();
    

自定义装饰器(DIY Decorator)

这个装饰器很有意思啊,这风格,不知道的我还以为我在写Java代码。在Nestjs里面,装饰器我觉得就是一个函数,只不过调用的表现形式不一样。装饰器可以配合Reflector获取元数据,能够有更灵活的开发体验,你值得拥有~😎。实际上装饰器不是Nestjs特有的,而是TS的一个功能,"emitDecoratorMetadata": true, "experimentalDecorators": true

  • 使用命令创建一个自定义指令文件

    nest g d role
    
  • 当然,也可以自己创建一个文件

    下面就写了两个自定义的装饰器。

    一个是Role用来用来判断权限的,和守卫那里一样,不过将SetMetadata进行封装了一下,使用的时候直接@Role(...)输入可通过的白名单字符串即可。

    第二个是ReqUrl,用来在Param参数上使用的

    import { ExecutionContext, SetMetadata, createParamDecorator } from '@nestjs/common';
    import { Request } from 'express';
    
    // 自定义一个Role装饰器
    export const Role = (...args: string[]) => SetMetadata('role', args);
    
    // 获取请求url的装饰器,这里使用的是createParamDecorator,还有其他的,如:applyDecorators能够批量处理多个装饰器。
    export const ReqUrl = createParamDecorator((decorParam?: string, ctx?: ExecutionContext) => {
      console.log('装饰器传入的值:', decorParam);
      const url = ctx.switchToHttp().getRequest<Request>().url;
      console.log('请求的url:', url);
      return {
        url,
        decorParam,
      };
    });
    
  • 使用自定义装饰器

    import { Controller, Get, UseGuards } from '@nestjs/common';
    import { LoginService } from './login.service';
    import { RoleGuard } from 'src/role/role.guard';
    import { ReqUrl, Role } from 'src/common/decorators';
    
    @Controller('login')
    export class LoginController {
      constructor(private readonly loginService: LoginService) {}
    
      @Get('all')
      @UseGuards(RoleGuard) // 使用守卫
      @Role('superAdmin', 'admin') // 定义白名单
      findAll(@ReqUrl('JK') url: string) {  // 请求路径的获取与传值
        console.log(url);  // 输出ReqUrl函数u的返回值
        return this.loginService.findAll();
      }
    }
    

连接数据库(Database)

数据库有:MongoDBMySQLPostgreSQL,这三个是我用过的,其中MongoDBNoSQL的数据库,即非关系型数据库,而MySQLPostgreSQL则是关系型数据库。这里我将使用PostgreSQL

ORM工具:typeormprisma两个,typeormNestjs集成度最高,生态也十分不错,但是我看乎uu们都说这个ORM工具坑很多,而且没有prisma好用,这里我暂时保留意见,因为我没深入了解使用过。

关于数据库的本地环境的安装,这里跳过。

  • 引入依赖

    这里的pgPostgreSQL的驱动,配合ORM工具能够实现ts/js代码操作数据库。

    dotenv是本地环境变量的一个第三方库,用于将项目下的.env文件里的键值对注入到本地环境变量中。

    pnpm install --save @nestjs/typeorm typeorm pg dotenv
    
  • 创建环境变量文件,在项目的根目录下创建.env文件

    NODE_ENV=development
    HOST=127.0.0.1
    PORT=5432
    DB_USERNAME=postgres
    DB_PASSWORD=admin
    DB_DATABASE_NAME=postgres
    
  • 使用dotenv(main.ts)

    这里需要在最顶部执行这个dotenv.config(),这样本地的.env文件能服务启动前就注入完毕,通过process.env就可以拿到值了。

    import * as dotenv from 'dotenv';
    dotenv.config();
    
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { NestExpressApplication } from '@nestjs/platform-express';
    
    async function bootstrap() {
      const app = await NestFactory.create<NestExpressApplication>(AppModule);
      await app.listen(3000);
    }
    bootstrap();
    
    
  • 在项目中导入TypeOrm

    个人认为,这个TypeOrmModule.forRoot的里面的配置项,可以单独提取出来,创建一个config文件,然后进行导出使用,这样会更友好一点。

    import { Module } from '@nestjs/common';
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    import { LoginModule } from './login/login.module';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { resolve } from 'path';
    
    @Module({
      imports: [LoginModule, TypeOrmModule.forRoot({
        type: 'postgres',  // 连接的数据库
        host: process.env.HOST,  // 地址
        port: process.env.PORT,  // 端口
        username: process.env.DB_USERNAME, // 数据库使用者名称 
        password: process.env.DB_PASSWORD, // 数据库密码
        database: process.env.DB_DATABASE_NAME, // 数据库名称
        entities: [resolve(__dirname, '/**/*.entity{.ts,.js}')], // 将所有的实体类注册生成表
        synchronize: true, // 是否自动将实体类同步到数据库,生成环境下不建议使用
        retryDelay: 500, // 重试连接数据库的间隔
        retryAttempts: 1, // 重连的次数
        autoLoadEntities: true, // 自动加载实体,forFeature()方法注册的每个实体都将自动添加到配置对象的实体类中
      })],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {}
    
  • Login实体类(login.entity.ts)

    相关的装饰器使用就不多赘述了,请移步查看文档了解更多。

    import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
    import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
    
    @Entity()  // 使用这个装饰器,表示这是一个实体类,需要被生成一个数据表
    export class Login {
      @PrimaryGeneratedColumn('uuid') 
      id: number;
    
      @IsEmail()
      @IsNotEmpty()
      @Column()
      email: string;
    
      @IsString()
      @Column()
      password: string;
    }
    
  • 导入实体类(login.module.ts)

    完成这一步后,数据库会同步出一个login表。

    import { Module } from '@nestjs/common';
    import { LoginService } from './login.service';
    import { LoginController } from './login.controller';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { Login } from './entities/login.entity';
    
    @Module({
      imports: [TypeOrmModule.forFeature([Login])], // 导入实体类
      controllers: [LoginController],
      providers: [LoginService],
    })
    export class LoginModule {}
    

数据库的事务(Transcation)

为什么需要用到事务处理?

比如:张三要给李四转500刀,张三先要扣除500,李四才能增加500。注意这里是有一个绑定关系的,张三要是因为服务器不稳定导致扣款失败,李四是不能增加500的,同理,李四没能增加500,张三也不能扣除500。

简述:成功都成功,失败都失败。

如果处理数据库的多条行为之间没有强关联性,也可不用开启事务。这里只介绍事务的简单使用。话不多说,直接上代码!

  • 快速创建一个user的CRUD

    nest g res user
    
  • 定义创建用户的字段类型(create-user.dto.ts)

    这里使用到了第三方库class-validator,这个第三方库里定义了大量的装饰器用于类型判断,之前有讲到过。这个文件用来定义创建用户时需要哪些参数和参数类型。

    import { IsNotEmpty, IsUUID, IsString } from 'class-validator';
    
    export class CreateUserDto {
      @IsUUID()
      @IsNotEmpty()
      id: string;
    
      @IsString()
      username: string;
    
      @IsString()
      password: string;
    }
    
  • 定义数据库映射实体(user.entity.ts)

    实体文件是ORM工具通过数据库驱动对本地数据库生成表的,也就是说它里面的一些字段、类型等等定义都将和数据库表有一致性的。

    import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
    
    @Entity('User')  // 创建一个表,表名为User,如果不填名称,默认就是user
    export class User {
      @PrimaryGeneratedColumn('uuid')  // 主键列,uuid类型,主键不能重复,测试需注意
      id: string;
    
      @Column({
        comment: '用户名',  // 创建一个名为username的列,列的描述为:用户名
      })
      username: string;
    
      @Column() 
      password: string;
    }
    
  • controller中去调用service的方法(user.contriller.ts)

    controller是相当于路由层,用于转发数据的。而service则就是操作数据的,为controller提供服务。

    import { Controller, Post, Body } from '@nestjs/common';
    import { UserService } from './user.service';
    import { CreateUserDto } from './dto/create-user.dto';
    
    @Controller('user')
    export class UserController {
      constructor(private readonly userService: UserService) {}
    
      @Post()
      async create(@Body() createUserDto: CreateUserDto) {
        // 在这里调用创建用户的方法,接下来就需要去service中去实现具体的操作
        this.userService.create(createUserDto);
      }
    }
    
  • 具体的事务实现(user.service.ts)

    你可以使用ApiFox进行发请求,测试开启了事务和未开启事务它们会有什么不同的效果。

    我既然已经测试过了,肯定不会让学习这么麻烦,直接上测试请求的代码!

    以下代码你可以:

    1. 打开浏览器
    2. F12打开控制台
    3. 将以下发请求的代码粘贴到控制台中
    4. 回车(前提是你的服务端允许跨域

    如果删除主动报错的那行代码,那么User表中的username字段列的所有ikun都将会变成GGBond

    var myHeaders = new Headers();
    myHeaders.append("User-Agent", "Apifox/1.0.0 (https://apifox.com)");
    myHeaders.append("Accept", "*/*");
    myHeaders.append("Host", "localhost:3000");
    myHeaders.append("Connection", "keep-alive");
    myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
    
    var urlencoded = new URLSearchParams();
    urlencoded.append("username", "ikun");
    urlencoded.append("password", "I like JK");
    urlencoded.append("id", "5a597c26-5238-45ac-9d89-d96ebe99158a");
    
    var requestOptions = {
       method: 'POST',
       headers: myHeaders,
       body: urlencoded,
       redirect: 'follow'
    };
    
    fetch("http://localhost:3000/user", requestOptions)
       .then(response => response.text())
       .then(result => console.log(result))
       .catch(error => console.log('error', error));
    
    import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
    import { CreateUserDto } from './dto/create-user.dto';
    import { InjectRepository } from '@nestjs/typeorm';
    import { User } from './entities/user.entity';
    import { EntityManager, Repository } from 'typeorm';
    
    @Injectable()
    export class UserService {
      constructor(@InjectRepository(User) private readonly userManager: Repository<User>) {}
    
      async create(createUserDto: CreateUserDto) {
          
        // 未开启事务的代码
        try{
            await this.userManager.save(createUserDto);
            throw new HttpException('事务失败测试', HttpStatus.INTERNAL_SERVER_ERROR);
            await this.userManager.update(
              {
                username: 'ikun',
              },
              {
                username: 'GGBond',
              }
            );
        }catch(e){}
    	
        // 开启事务的代码
        // try{
        //   await this.userManager.manager.transaction(async (manager: EntityManager) => {
        //   	await manager.save(User, createUserDto);
        //   	throw new HttpException('事务失败测试', HttpStatus.INTERNAL_SERVER_ERROR);
        //   	const res = await manager.update(
        //     	User,
        //     	{
        //      	 username: 'ikun',
        //     	},
        //     	{
        //       	username: 'GGBond',
        //     	}
        //   	);
        //   	console.log(res, 'res');
        // 	});
      	// }catch(e){}
      }
    }
    
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值