Nest快速入门

背景

全栈

全栈其实可以说是一种野心,发展全栈对公司和个人都大有益处,但最重要的还是对个人的发展如何。个人发展全栈主要还是为了应对未来,主要有几方面: 避免被淘汰、 提升个人价值、 走向管理职位、 实现创业梦。
从个人为出发点,后端语言百花齐放,想要到一个比较深入的程度,那对所使用的开发语言、框架就需要稍微深入了。例如同时要深入typescript、Java的领域,那就会谈到精力问题,对个人来说似乎会很累,不太现实。

NestJS

根据官网介绍:

Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用JavaScript 的渐进增强的能力,使用并完全支持 TypeScript (仍然允许开发者使用纯 JavaScript 进行开发),并结合了 OOP (面向对象编程)、FP (函数式编程)和 FRP (函数响应式编程)。

简单来说就是:

  • 原生支持 TypeScript
  • 两个支持开箱即用的 HTTP 平台:expressfastify
  • 可以与任何 node.js 的 HTTP 框架一起工作

优势

NestJS 的一些优势包括:

  • 构建在现代 JavaScript 栈之上,因此使用了最新的 JavaScript 技术。
  • 基于 Angular 的架构和语法,提供了强大的模块化系统和依赖注入功能。
  • 基于 TypeScript,提供了强类型和静态类型检查
  • 提供了丰富的工具和模块,可用于构建各种类型的服务器端应用程序,包括 RESTful APIGraphQL APIWebSocket 服务器等。
  • 提供了一组可扩展的构建块,可用于快速构建应用程序。
  • 提供了与主流数据库和身份验证系统的集成。

对于前端人员来说,“一招Typescript”吃遍前后端,何乐而不为。

项目搭建

项目初始化

确保已经安装了 Node.js(版本 >= 12,v13 除外)

$ npm i -g @nestjs/cli 
$ nest new project-name

运行项目

启动 HTTP 服务监听定义在 src/main.ts 文件中定义的端口号。在浏览器访问[http://localhost:3000](http://localhost:3000/)/,访问成功代表服务已经OK了;

# dev为热更新模式
$ npm run start:dev

框架约束

NestJS文件约束:

  • 每个模块最少有三种文件组合:module、server、controller。
  • NestJS允许命令行创建不生成.spec(nest g controller product --no-spec)。

初始化后项目的主要目录结构:

image.png
image.png

这里涉及到NestJs框架的文件约束:

app.controller.ts 单个路由的基本控制器(Controller)
app.controller.spec.ts 针对控制器的单元测试
app.module.ts 应用程序的根模块(Module)
app.service.ts 具有单一方法的基本服务(Service)
main.ts 应用程序的入口文件,它使用核心函数 NestFactory 来创建 Nest 应用程序的实例。

main.ts中使用工厂函数NestFactory创建一个 AppModule 应用实例,启动 HTTP 监听器,等待 HTTP 请求。

import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';

async function bootstrap() {
  // <NestExpressApplication> | <NestFastifyApplication> | <>
  // 一般来说我们选择 NestExpress,因为网上资源多,官方文档有很多例子都是基于Express
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  // 设置全局路由前缀
  // app.setGlobalPrefix('api');
  await app.listen(3000);
}
bootstrap();

可选平台Express
是一个众所周知的 node.js 简约 Web 框架。 这是一个经过实战考验,适用于生产的库,拥有大量社区资源。 默认情况下使用 @nestjs/platform-express 包。 许多用户都可以使用 Express ,并且无需采取任何操作即可启用它。
Fastify
是一个高性能,低开销的框架,专注于提供最高的效率和速度。 在这里
阅读如何使用它。

核心概念

模块

模块是用 @Module() 装饰的类。 @Module() 装饰器提供了元数据,Nest 用它来组织应用程序结构。

模块主要分4个类别:

  1. 功能模块
  2. 共享模块
  3. 全局模块
  4. 动态模块

main.ts 只引入了app.module.ts文件,此为应用程序的根模块(每个 Nest 应用程序至少有一个模块):

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

对装饰器不了解的,可以看走近MidwayJS:初识TS装饰器与IoC机制

@Module()装饰器接收四个属性:providerscontrollersimportsexports

providers Nest.js注入器实例化的提供者(服务提供者),处理具体的业务逻辑,各个模块之间可以共享;
controllers 处理http请求,包括路由控制,向客户端返回响应,将具体业务逻辑委托给providers处理;
imports 导入模块的列表,如果需要使用其他模块的服务,需要通过这里导入;
exports 导出服务的列表,供其他模块导入使用。如果希望当前模块下的服务可以被其他模块共享,需要在这里配置导出;

下面通过一个产品的CRUD例子来进行演示:

NestJs中,具有同一应用程序域的文件英国放到同一个功能模块下:
首先我们可以通过nest-cli提供的命令生成模块:

  • nest g module products
import { Module } from '@nestjs/common';

@Module({})
export class ProductsModule {}

同时 ProductsModule 也会被自动注册到根模块:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ProductsModule } from './products/products.module';

@Module({
  imports: [ProductsModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

这样便完成了一个新的功能模块的注册。

共享模块在 Nest 中,默认情况下,模块是单例的,因此我们可以轻松地在多个模块之间共享同一个提供者实例
模块本身存在共享性,一个模块一旦创建便可以被任意模块重复使用。例如:

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  // 导出模块供其他模块倒入,所有模块共享catsService实例
  exports: [CatsService]
})
export class CatsModule {}

全局模块如果想提供一些通用的模块,如:helper,数据库连接等等,可以创建全局模块,也比较简单,增加@Global
即可,全局模块应该只注册一次,最好由根或核心模块注册。

import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Global()
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

动态模块动态模块在 Nest 中也是非常重要的。此功能使您可以轻松创建可自定义的模块,这些模块可以动态注册和配置提供程序。例如:
注册动态模块:DatabaseModule

import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';

@Module({
  providers: [Connection],
})
export class DatabaseModule {
  static forRoot(entities = [], options?): DynamicModule {
    const providers = createDatabaseProviders(options, entities);
    return {
      module: DatabaseModule,
      providers: providers,
      exports: providers,
    };
  }
}

使用该模块

import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';

@Module({
  imports: [DatabaseModule.forRoot([User])],
})
export class AppModule {}

控制器

同样的,通过nest-cli提供的命令生成控制器:

  • nest g controller products

在Nest中,使用装饰器@Controller()定义一个基本的控制器,该装饰器可以传入一个路径参数

import { Controller } from '@nestjs/common';

@Controller('products')
export class ProductsController {}

同时在 products.module.ts文件中会自动注册 ProductsController。

服务

一样的操作,使用命令生成

  • nest g service products

其实可以通过 nest g resource products一次性生成。
更多创建命令可以通过 nest generate --help查看。

import { Injectable } from '@nestjs/common';

@Injectable()
export class ProductsService {}

此时的 products.module.ts 文件

import { Module } from '@nestjs/common';
import { ProductsController } from './products.controller';
import { ProductsService } from './products.service';

@Module({
  controllers: [ProductsController],
  providers: [ProductsService],
})
export class ProductsModule {}

到目前为止,已经完成了 Products 功能模块的搭建,在增加业务逻辑之前,先来了解一下 NestJS 如何接入数据库。

数据库

这里以TypeORM框架为例与MySQL数据库进行交互。

ORM技术(Object-Relational Mapping),把关系数据库的变结构映射到对象上。

安装依赖:

npm install @nestjs/typeorm typeorm mysql2 -S

在 app.module.ts 中注册:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ProductsModule } from './products/products.module';

@Module({
  imports: [
    ProductsModule,
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: '123456',
      database: 'store',
      autoLoadEntities: true, // 自动加载实体
      synchronize: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

这是最简单的配置方式,当然 **TypeORM **也非常方便地支持:

  • 存储库模式:每个实体都有自己的存储库Entity。
  • 实体关系:一对一@OneToOne(),一对多@OneToMany()@ManyToOne(),多对多@ManyToMany()
  • 自动载入实体
  • 事务
  • 多数据库
  • 异步配置
  • 等等

详细可参考官方文档

对了,不忘创建一下 Products 的实体类;

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity('products')
export class ProductsEntity {
  @PrimaryGeneratedColumn()
  id: number; // 标记为主列,值自动生成

  @Column({ length: 50 })
  title: string;

  @Column('text')
  desc: string;

  @Column({ default: '' })
  thumb_url: string;

  @Column('tinyint')
  type: number;

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  create_time: Date;

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  update_time: Date;
}

完成数据库的打通后,开始增加业务逻辑代码。

HTTP装饰器

HTTP 装饰器使用起来同样非常简单;
Nest 为所有标准的 HTTP 方法提供了相应的装饰器:

  • @Get()
  • @Post( )
  • @Put()
  • @Delete()
  • @Patch()
  • @Options()
  • @Head()
  • @All()用于定义一个用于处理所有 HTTP 请求方法的处理程序。

经过它们装饰的方法,可以对相应的HTTP请求进行响应。
同时它们可以接受一个字符串或一个字符串数组作为参数,这里的字符串可以是固定的路径,也可以是通配符

import { Body, Controller, Delete, Get, Param, Post, Put} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreateProductDto } from './dto/create-product.dto';
import { ProductsService } from './products.service';

@ApiTags('产品')
@Controller('products')
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}

  @ApiOperation({ summary: '创建产品' })
  @Post()
  async create(@Body() product: CreateProductDto) {
    return await this.productsService.create(product);
  }

  @Get()
  async findAll(): Promise<ProductsEntity[]> {
    return await this.productsService.findAll();
  }

  @Get(':id')
  async findById(@Param('id') id) {
    return await this.productsService.findById(id);
  }

  @Put(':id')
  async update(@Param('id') id, @Body() product) {
    return await this.productsService.updateById(id, product);
  }

  @Delete('id')
  async remove(@Param('id') id) {
    return await this.productsService.remove(id);
  }
}

@Req()、@Res()

Nest 提供了对底层平台(默认为 Express)的请求对象(request)的访问方式。我们可以在待处理函数的签名中增加@Req()装饰器,Nest 则会自动注入请求对象。

针对HTTP请求,Nest 提供了开箱即用的专有装饰器,如下

装饰器 对应参数
@Request(),@Req() req
@Response(),@Res()* res
@Next() next
@Session() req.session
@Param(key?: string) req.params/req.params[key]
@Body(key?: string) req.body/req.body[key]
@Query(key?: string) req.query/req.query[key]
@Headers(name?: string) req.headers/req.headers[name]
@Ip() req.ip
@HostParam() req.hosts

另外,Nest还支持较多的装饰器,例如

  • @HttpCode(statusCode) 修改响应状态码
  • @Header(key, value) 自定义响应头
  • @Redirect(url, statusCode|302) 设置重定向

详细可参考官方文档查阅。

Repository

上面介绍了 TypeORM 以及 Entity,这里具体化一下 service;

import { HttpException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ProductsEntity } from './products.entity';

@Injectable()
export class ProductsService {
  constructor(
    @InjectRepository(ProductsEntity)
    private readonly productsRepository: Repository<ProductsEntity>,
  ) {}

  // 创建产品
  async create(product: Partial<ProductsEntity>): Promise<ProductsEntity> {
    const { title } = product;
    if (!title) {
      throw new HttpException('缺少产品标题', 401);
    }
    const doc = await this.productsRepository.findOne({ where: { title } });
    if (doc) {
      throw new HttpException('产品已存在', 401);
    }
    return await this.productsRepository.save(product);
  }

  // 获取指定产品
  async findById(id): Promise<ProductsEntity> {
    return await this.productsRepository.findOneBy({ id: id });
  }

  // 更新产品
  async updateById(id, product): Promise<ProductsEntity> {
    const existProduct = await this.productsRepository.findOne(id);
    if (!existProduct) {
      throw new HttpException(`id为${id}的产品不存在`, 401);
    }
    const updateProduct = this.productsRepository.merge(existProduct, product);
    return this.productsRepository.save(updateProduct);
  }

  // 刪除产品
  async remove(id) {
    const existProduct = await this.productsRepository.findOne(id);
    if (!existProduct) {
      throw new HttpException(`id为${id}的产品不存在`, 401);
    }
    return await this.productsRepository.remove(existProduct);
  }

  // 获取产品列表
  async findAll(): Promise<ProductsEntity[]> {
    return this.productsRepository.find();
  }
}

自此一个完成的请求就已经完成了,但为了接口更加规范,还可以再进一步完善;

过滤器

过滤器也是 NestJS 的众多provider之一;这里用过滤器来简单处理错误请求;
nest g filter core/filter/http-exception
代码实现:

import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
} from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp(); // 获取请求上下文
    const response = ctx.getResponse(); // 获取请求上下文中的 response对象
    const status = exception.getStatus(); // 获取异常状态码

    // 设置错误信息
    const message = exception.message
      ? exception.message
      : `${status >= 500 ? 'Service Error' : 'Client Error'}`;
    const errorResponse = {
      data: {},
      message: message,
      code: -1,
    };

    // 设置返回的状态码, 请求头,发送错误信息
    response.status(status);
    response.header('Content-Type', 'application/json; charset=utf-8');
    response.send(errorResponse);
  }
}

在main.js中注册:

....
import { HttpExceptionFilter } from './core/filter/http-exception/http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  // 注册全局错误的过滤器
  app.useGlobalFilters(new HttpExceptionFilter());
    ...
  await app.listen(3000);
}
bootstrap();

接下来对请求成功返回的格式进行统一的处理,可以用Nest.js的拦截器来实现。

拦截器

拦截器可以在调用路由处理程序之前和之后访问响应/请求。比如 logger 或者 统一处理响应结果

老配方,首先使用命令创建一个拦截器:
nest g interceptor core/interceptor/transform

代码实现

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { map, Observable } from 'rxjs';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data) => {
        return {
          data,
          code: 200,
          msg: '请求成功',
        };
      }),
    );
  }
}

注册到main.js

...
import { TransformInterceptor } from './core/interceptor/transform/transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  ...
  // 全局注册拦截器
  app.useGlobalInterceptors(new TransformInterceptor());
  ...
  await app.listen(3000);
}
bootstrap();

接口文档

规范化接口当然少不了接口文档,Nest也是完美支持 swagge,在product
目录下创建一个dto文件夹,再创建一个create-post.dot.ts文件:

import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class CreateProductDto {
  @ApiProperty({ description: '产品标题' })
  readonly title: string;

  @ApiProperty({ description: '产品描述' })
  readonly desc: string;

  @ApiPropertyOptional({ description: '产品缩略图' })
  readonly thumb_url: string;

  @ApiProperty({ description: '产品类型' })
  readonly type: number;
}

完善controller,进行说明:

...
@ApiTags('产品')
@Controller('products')
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}

  @ApiOperation({ summary: '创建产品' })
  @Post()
  async create(@Body() product: CreateProductDto) {
    return await this.productsService.create(product);
  }
...
}

数据验证

在Nest.js中,管道(Pipes)就是专门用来做数据转换的,我们看一下它的定义:

管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口。管道有两个类型:

  • 转换:管道将输入数据转换为所需的数据输出
  • 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常;

管道在异常区域内运行。这意味着当抛出异常时,它们由核心异常处理程序和应用于当前上下文的 异常过滤器 处理。当在 Pipe 中发生异常,controller 不会继续执行任何方法。

通俗来讲就是,对请求接口的入参进行验证和转换的前置操作,验证好了我才会将内容给到路由对应的方法中去,失败了就进入异常过滤器中。

安装依赖:

  • npm install class-validator class-transformer -S

Nest.js自带了三个开箱即用的管道:

  1. ValidationPipe
  2. ParseIntPipe
  3. ParseUUIDPipe,

ValidationPipe 配合 class-validator 就可以完美的实现对参数类型进行验证,验证失败抛出异常

管道验证操作通常用在dto这种传输层的文件中,用作验证操作。安装两个需要的依赖包:class-transformerclass-validator
npm install class-validator class-transformer -S

然后在create-post.dto.ts文件中添加验证, 完善错误信息提示:

import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber } from 'class-validator';

export class CreateProductDto {
  @IsNotEmpty({ message: '产品标题必填' })
  @ApiProperty({ description: '产品标题' })
  readonly title: string;

  @IsNotEmpty({ message: '缺少产品描述' })
  @ApiProperty({ description: '产品描述' })
  readonly desc: string;

  @ApiPropertyOptional({ description: '产品缩略图' })
  readonly thumb_url: string;

  @IsNumber()
  @ApiProperty({ description: '产品类型' })
  readonly type: number;
}

上面只编写了一些常用的验证,class-validator还提供了很多的验证方法, 感兴趣可以看官方文档.

最后我们还有一个重要的步骤, 就是在main.ts中全局注册一下管道ValidationPipe

...
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  ...
  // 注册校验管道
  app.useGlobalPipes(new ValidationPipe());

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

总结

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容