nest.js入门

介绍

nest.js是一个高效,可扩展的node.js服务器端web框架,node.js提供了一个灵活的运行时环境,而nest.js提供了更高层次的组织架构。

特点:
支持原生typescript的框架;
可以基于express也可以选择fastify(快,更高效),也可以用express直接访问其api

项目入门

  • 项目创建(CLI)

npm i -g nestjs/cli
nest new pro-name

// 运行:
npm run start:dev

  • 各文件解释:

// main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule); // 创建nestjs app
  app.setGlobalPrefix('api'); //设置全局路由前缀: 127.0.0.0:8002/api/...
  await app.listen(8002); //开启服务器,开始监听
}
bootstrap();
// app.moudle.ts  
// 应用程序的根模块

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

/* 装饰器Module接收四个属性:
    providers: 服务提供者,srevices
    controllers: 处理http,将请求委托为providers
    imports: 其他模块的服务导入
    exports: 导出服务的列表,供其他模块导入使用
*/
@Module({
  imports: [PostsModule],
  controllers: [AppController],
  providers: [AppService],
})

export class AppModule {}
// app.controller.ts

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

/* 
@Controller: 定义控制器
参数:传入'app',则处理路径/app/下的请求。路由控制,表示该控制器的主路径
*/
@Controller('app')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Post('list')
  create() {}

  @Get('user_*')
  getUser() {
    return 'getUser';
  }

  @Put('list/:id')
  update() {
    return 'update';
  }

  @Put('list/user')
  updateUser() {
    return 'updateUser';
  }
  // 若访问127.0.0.0.1:8002/api/app/list/user不会被调用,路径‘list/:id’已经满足,不会接着向下匹配,直接调用update()
}

/* 
  http方法处理装饰器:
    @Get, @Post, @Put,由这些装饰器处理,方法可以响应对应的http请求。
    参数接收一个字符串,字符串数组(字符串:路径/通配符): 路由装饰器
*/
// app.service.ts

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

// 使用Injectable()装饰器后,直接引用,无需实例化
@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}
  • 添加新模块

// 命令: nest g [文件类型] [文件名] [文件目录] 目录不写,默认创建和文件名一样的文件夹
// 在项目根目录下打开终端依次运行以下命令

nest g mo posts
nest g co posts
nest g service posts

注意: 先创建Module, 再创建ControllerService, 这样创建出来的文件在Module中自动注册,反之,后创建Module, ControllerService,会被注册到外层的app.module.ts

当前目录结构:

  • 连接mysql

1. (安装mysql+navicat premium) → 新建连接:连接名115 → 新建数据库:数据库名blog

2. TypeORM连接数据库

什么是ORM? :(object-relational mapping)对象关系映射 数据库表是一个二维表,如表所示,将它转换为一个js对象: { id: 1, title: "nestjs", content: "文章描述" } (使得读写都是js对象)

使用typeORM操作数据库,首先安装依赖包:
npm install @nestjs/typeorm typeorm mysql2 -S

官方提供的连接数据库地方法有两种,这里仅说明一种

在根目录下新建文件 .env 和 .env.prod:

// .env
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=root
DB_DATABASE=blog

SALT= NestProject
SECRET=test123456

APPID=sssss
APPSECRET= xxxx
// .env.prod
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWD=root
DB_DATABASE=blog

在根目录下创建文件夹config/env.ts

// env.ts
import * as fs from 'fs';
import * as path from 'path';
// const dotenv = require('dotenv');
const isProd = process.env.NODE_ENV === 'production';

function parseEnv() {
  const localEnv = path.resolve('.env');
  const prodEnv = path.resolve('.env.prod');

  if (!fs.existsSync(localEnv) && !fs.existsSync(prodEnv)) {
    throw new Error('缺少环境配置文件');
  }

  const filePath = isProd && fs.existsSync(prodEnv) ? prodEnv : localEnv;

  return { path: filePath };
}

export default parseEnv();

在app.module.js中添加如下内容,即可成功连接数据库

...
...
imports: [
    ConfigModule.forRoot({ isGlobal: true, envFilePath: [envConfig.path] }),
    ConfigModule.forRoot({ isGlobal: true }),
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        type: 'mysql',
        host: configService.get('DB_HOST', '127.0.0.0'),
        port: configService.get('DB_PORT', 3306),
        username: configService.get('DB_USER', 'root'),
        password: configService.get('DB_PASSWORD', 'root'),
        database: configService.get('DB_DATABASE', 'blog'),
        // charset: 'utf8mb4',
        timezone: '+08:00',
        synchronize: false,
        autoLoadEntities: true,
      }),
    }),
    PostsModule,
  ],
  ...
  ..

3. CRUD(create, read, update, delete)增删改查

TypeORM是通过实体映射到数据库表,因此接下来我们先建立一个实体PostsEntity,在posts目录下新建文件posts.entity.ts

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

// 表posts对应的实体:PostEntity
@Entity('posts')
export class PostEntity {
  @PrimaryGeneratedColumn()
  id: number; // 标记为主列,值自动生成

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

  @Column({ length: 20 })
  author: string;

  @Column('text')
  content: string; // 列类型为text

  @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;
}

4. 接下来,创建完实体,在posts.service.ts下实现CRUD操作

import { HttpException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { PostsEntity } from './posts.entity';
import { Repository, getRepository } from 'typeorm';

export interface PostRo {
  list: PostsEntity[];
  count: number;
}

@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(PostsEntity)
    private readonly postsRepository: Repository,
  ) {}

  // 创建文章
  async create(post: Partial): Promise {
    const { title } = post;
    if (!title) {
      throw new HttpException('缺少文章标题', 401);
    }
    const doc = await this.postsRepository.findOne({ where: { title } });
    if (doc) {
      throw new HttpException('文章已存在', 401);
    }
    return await this.postsRepository.save(post);
  }

  // 获取文章列表
  async findAll(query): Promise {
    const qb = await getRepository(PostsEntity).createQueryBuilder('post');
    qb.where('1 = 1'); //没有意义,占位符,允许后续添加更多条件,一般用于动态查询
    qb.orderBy('post.create_time', 'DESC'); //时间降序

    const count = await qb.getCount();
    const { pageNum = 1, pageSize = 10 } = query;
    qb.limit(pageSize);
    qb.offset(pageSize * (pageNum - 1)); // 实现分页

    const posts = await qb.getMany();
    return { list: posts, count: count };
  }

  // 获取指定文章
  async findById(id): Promise {
    return this.postsRepository.findOne({ where: { id } });
  }

  // 更新文章
  async updateById(id, post): Promise {
    const existPost = await this.postsRepository.findOne({ where: { id } });
    if (!existPost) {
      throw new HttpException(`id为${id}的文章不存在`, 401);
    }
    const updatePost = this.postsRepository.merge(existPost, post);
    return this.postsRepository.save(updatePost);
  }

  //删除文章
  async remove(id) {
    const existPost = await this.postsRepository.findOne({ where: { id } });
    if (!existPost) {
      throw new HttpException(`id为${id}的文章不存在`, 401);
    }
    return await this.postsRepository.remove(id);
  }
}
  • 接口格式统一

一般不会根据http状态码来判断接口的成功或失败,而是根据请求返回的数据中的code字段,例如:

{
	"code": 0,
	"message": "OK",
	"data": {}
}

//失败:
{
	"code": -1,
	"message": "error msg",
	"data": {}
}
  • 拦截错误请求

主要步骤是:创建过滤器 → 实现过滤器代码 → 注册

1. 创建过滤器: nest g filter core/filter/http-exception

2. 实现过滤器代码:

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);
  }
}

3. 注册:在main.ts

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix('api'); //设置全局路由前缀: 127.0.0.0:8002/api/...
  app.useGlobalFilters(new HttpExceptionFilter()); //设置全局错误的过滤器
  await app.listen(8002);
}
bootstrap();
  • 拦截成功的返回数据

主要步骤是:创建拦截器 → 实现拦截器代码 → 注册

1. 创建拦截器

nest g interceptor core/interceptor/transform

2. 实现拦截器代码

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

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable {
    // next: 处理程序调用器
    // next.handle() 用来调用下一个程序,返回一个Observable,该应用程序的响应流
    // pipe()是Rxjs中用于组合多个操作符的函数。在这里允许对Observable的输出进行一列转换和操作
    // map() 是Rxjs中的一个操作符,用来对Observable发出的每一个值应用一个函数,并返回一个新的Observable。这里用于将原始响应数据转换为一个新的对象。
    return next.handle().pipe(
      map((data) => {
        return {
          data,
          code: 0,
          msg: '请求成功',
        };
      }),
    );
  }
}

3. 注册:在mian.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
import { HttpExceptionFilter } from './core/filter/http-exception/http-exception.filter';
import { TransformInterceptor } from './core/interceptor/transform/transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix('api'); //设置全局路由前缀: 127.0.0.0:8002/api/...
  app.useGlobalFilters(new HttpExceptionFilter()); //设置全局错误的过滤器
  app.useGlobalInterceptors(new TransformInterceptor()); //设置全局拦截器(作用于所有的请求和响应)
  await app.listen(8002);
}
bootstrap();
  • 配置接口文档Swagger

  • 配置

1. 安装: npm install @nestjs/swagger swagger-ui-express -S
2. 设置:在main.ts中设置Swagger文档信息

...
...
// 设置swagger文档
  const config = new DocumentBuilder()
    .setTitle('管理后台')
    .setDescription('管理后台接口文档')
    .setVersion('1.0')
    .addBearerAuth() // 添加 Bearer 认证的支持,当API 需要进行用户身份验证时,可以使用该方法来确保用户能够在 Swagger UI 中输入他们的 Bearer token,从而访问受保护的资源。
    .build();
  const document = SwaggerModule.createDocument(app, config); //生成swagger文档对象
  SwaggerModule.setup('docs', app, document); // 设置swagger ui为可访问状态,‘docs’指定路径

  await app.listen(8002);

3. 访问 http://localhost:8002/docs可以看到生成的文档

  • 接口设置

1. 接口标签

接口根据controller来分类,在controller上添加@ApiTags即可

@ApiTags('文章')
@Controller('post')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}
  ...
  ...
}

2. 接口描述

为每一个接口添加文字说明,使用@ApiOperation装饰器

@ApiTags('文章')
@Controller('post')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  @ApiOperation({ summary: '创建文章' })
  @Post()
  async create(@Body() post: CreatePostDto) {
    // @Body()提取请求体数据post
    return await this.postsService.create(post);
  }
  ...
  ...
  }

3. 接口传参

在posts下创建文件夹dto,然后新建文件create-post.dot.ts文件:

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

export class CreatePostDto {
  @ApiProperty({ description: '文章标题' })
  readonly title: string;

  @ApiPropertyOptional({ description: '内容' })
  readonly content: string;

  @ApiPropertyOptional({ description: '文章封面' })
  readonly coverUrl: string;

  @ApiPropertyOptional({ description: '文章状态' })
  readonly status: string;

  @ApiProperty({ description: '文章分类' })
  readonly category: number;

  @ApiPropertyOptional({ description: '是否推荐' })
  readonly isRecommend: boolean;

  @ApiPropertyOptional({ description: '文章标签' })
  readonly tag: string;
}

最终效果如下:

  • 数据验证

数据验证是指对请求接口的入参数数据进行验证和转换的前置操作,通过验证之后才会将请求内容给到对应的路由方法中去,否则进入异常过滤器。

实现:nest.js中的管道就是用来数据转换和验证的,这也是管道的两种类型。

  1. 转换:将输入数据转换为所需的数据输出。(ParseIntPipe和ParseUUIDPipe)
  2. 验证:对输入数据进行验证。若验证通过则继续传递;验证失败则抛出异常,由当前上下文的异常过滤器处理。pipe管道中发生异常,controller不会继续执行任何方法。(ValidationPipe + class-validator)

1. 首先安装依赖

npm install class-validator class-transformer -S

2. 在create-post.dto.ts中添加验证

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

export class CreatePostDto {
  @ApiProperty({ description: '文章标题' })
  @IsNotEmpty({ message: '文章标题必填' }) //数据验证
  readonly title: string;

  @ApiPropertyOptional({ description: '内容' })
  readonly content: string;

  @ApiPropertyOptional({ description: '文章封面' })
  readonly coverUrl: string;

  @ApiPropertyOptional({ description: '文章状态' })
  readonly status: string;

  @IsNumber() //数据验证
  @ApiProperty({ description: '文章分类' })
  readonly category: number;

  @ApiPropertyOptional({ description: '是否推荐' })
  readonly isRecommend: boolean;

  @ApiPropertyOptional({ description: '文章标签' })
  readonly tag: string;
}

3. 注册

app.useGlobalPipes(new ValidationPipe()); //注册数据验证管道

概念理解

  • modules

模块是具有@Module()装饰器的类,该装饰器提供了元数据,nest使用它来组织应用程序架构。每个nest应用程序至少有一个模块,即根模块,它是nest开始构建应用程序树的地方。@Module()装饰器接受一个描述模块属性的对象:

共享模块:若想共享某个servie: 则需要放到export数组中:(服务的进出都需要通过module)

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

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService] //需要共享的模块
})
export class CatsModule {}

全局模块: @Global,只注册一次,一般为根模块或核心模块。全局模块不需要在imports数中导入。但是,将所有内容全局化并不是一个好的策略

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 {}

动态模块:DynamicModule,可以创建自定义模块,例如下面的例子:forRoot静态方法允许在模块导入时传递entities(数据库相关的实体),以及其他配置。这是nest.js一个强大的功能

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])],
  exports: [DatabaseModule],
})
export class AppModule {}

  • controllers:

控制器的目的是接收应用的特定请求。路由机制控制哪个控制器接收哪些请求。通常,每个控制器有多个路由,不同的路由可以执行不同的操作。

  • providers:

直观来看,providers只是一个被@Injectable()装饰器注释的类,许多基本的类都可以被视为nest providers,一般遵循SOLID原则。调用时直接在constructor中注入依赖。

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

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {
	//依赖注入
	constructor(private readonly catsService: CatsService) {} 
}

生命周期一般与应用程序的生命周期同步,启动时,解析每个service,关闭时,销毁每个providers。

可选providers: 注入依赖'HTTP_OPTIONS'到httpClient中,如果不存在则为undefined

contructor(
	@Optional() @Inject('HTTP_OPTIONS') private readonly httpClient: T
) {}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值