参考资料

了解NestJS

NestJS 是一个用于构建高效可扩展的一个基于 NodeJS 服务端应用程序开发框架。并且完全支持 TypeScript 结合了 AOP 面向切面的编程方式。

设计模式

IOC

Inversion of Control字面意思是控制反转,具体定义是高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。

DI

依赖注入(Dependency Injection)其实和IoC是同根生,这两个原本就是一个东西,只不过由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”。 类A依赖类B的常规表现是在A中使用B的instance。

装饰器

  • 类装饰器
    • 他会自动把class的构造函数传入到装饰器的第一个参数 target
  • 属性装饰器
    • 原形对象
    • 属性名称
  • 方法装饰器
    • 原形对象
    • 方法的名称
    • 属性描述符。可写对应writable,可枚举对应enumerable,可配置对应configurable
  • 参数装饰器
    • 原形对象
    • 方法的名称
    • 参数的位置从0开始

装饰器demo

import axios from 'axios'
 
const Get = (url: string): MethodDecorator => {
    return (target, key, descriptor: PropertyDescriptor) => {
        const fnc = descriptor.value;
        axios.get(url).then(res => {
            fnc(res, {
                status: 200,
            })
        }).catch(e => {
            fnc(e, {
                status: 500,
            })
        })
    }
}
 
//定义控制器
class Controller {
    constructor() {
 
    }
    @Get('https://api.apiopen.top/api/getHaoKanVideo?page=0&size=10')
    getList (res: any, status: any) {
        console.log(res.data.result.list, status)
    }
  
}

初始化项目

$ npm i -g @nestjs/cli@9
$ nest new nest-demo

将会创建 nest-demo 目录, 安装 node_modules 和一些其他样板文件,并将创建一个 src 目录,目录中包含几个核心文件。

src
 ├── app.controller.spec.ts   对于基本控制器的单元测试样例
 ├── app.controller.ts        带有单个路由的基本控制器示例
 ├── app.module.ts            应用程序的根模块
 ├── app.service.ts           带有单个方法的基本服务
 └── main.ts                  应用程序入口文件

启动项目

$ cd nest-demo
$ pnpm run start:dev

运行流程的理解

  1. 程序入口
/* src/main.ts */
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
    const app = await NestFactory.create(AppModule); // 创建AppModule实例
    await app.listen(9099); // 监听9099端口,默认是3000
}
bootstrap()
  1. 模块
    每个 Nest 应用程序至少有一个模块,即根模块。
/* src/app.module.ts */
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [], // 导入模块的列表,这些模块导出了此模块中所需提供者
  exports: [], // 由本模块提供并应在其他模块中可用的提供者的子集
  controllers: [AppController], // 必须创建的一组控制器
  providers: [AppService], // 由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享
})
export class AppModule {}

基础命令

nest --help 可以查看nestjs所有的命令

name alias description
application app Generate a new application workspace
class cl Generate a new class
configuration config Generate a CLI configuration file
controller co Generate a controller declaration
decorator d Generate a custom decorator
filter f Generate a filter declaration
gateway ga Generate a gateway declaration
guard gu Generate a guard declaration
interceptor itc Generate an interceptor declaration
interface itf Generate an interface
library lib Generate a new library within a monorepo
middleware mi Generate a middleware declaration
module mo Generate a module declaration
pipe pi Generate a pipe declaration
provider pr Generate a provider declaration
resolver r Generate a GraphQL resolver declaration
resource res Generate a new CRUD resource
service s Generate a service declaration
sub-app app Generate a new application within a monorepo

RESTful API

RESTful 风格一个接口就会完成 增删改差 他是通过不同的请求方式来区分的

  • 查询 GET
  • 提交 POST
  • 更新 PUT PATCH
  • 删除 DELETE

RESTful 版本控制

一共有三种我们一般用第一种 更加语义化

  • URI Versioning 版本将在请求的 URI 中传递(默认)
  • Header Versioning 自定义请求标头将指定版本
  • Media Type Versioning 请求的Accept标头将指定版本
/* main.ts */
import { NestFactory } from '@nestjs/core';
import { VersioningType } from '@nestjs/common';
import { AppModule } from './app.module';
 
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableVersioning({  // 开启版本控制
    type: VersioningType.URI,
  })
  await app.listen(3000);
}
bootstrap();
/* controller */
import { Controller, Get, Post, Body, Patch, Param, Delete, Version } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
 
@Controller({
  path:"user",
  version:'1'  // 指定版本
})
export class UserController {
  constructor(private readonly userService: UserService) {}
 
  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.userService.create(createUserDto);
  }
 
  @Get()
  // @Version('1')
  findAll() {
    return this.userService.findAll();
  }
 
  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.userService.findOne(+id);
  }
 
  @Patch(':id')
  update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    return this.userService.update(+id, updateUserDto);
  }

Code码规范

  • 200 OK
  • 304 Not Modified 协商缓存了
  • 400 Bad Request 参数错误
  • 401 Unauthorized token错误
  • 403 Forbidden referer origin 验证失败
  • 404 Not Found 接口不存在
  • 500 Internal Server Error 服务端错误
  • 502 Bad Gateway 上游接口有问题或者服务器问题

nestjs 控制器

装饰器 描述
@Request() 获取请求对象
@Response() 获取响应对象
@Next() 获取下一个处理函数
@Session() 获取会话对象
@Param(key?: string) 获取请求参数或指定参数
@Body(key?: string) 获取请求体或指定参数
@Query(key?: string) 获取查询参数或指定参数
@Headers(name?: string) 获取请求头或指定头部
@HttpCode 设置响应状态码

Session

session 是服务器 为每个用户的浏览器创建的一个会话对象 这个session 会记录到 浏览器的 cookie 用来区分用户

我们使用的是nestjs 默认框架express 他也支持express 的插件 所以我们就可以安装express的session

pnpm i express-session --save

需要智能提示可以装一个声明依赖

pnpm i @types/express-session -D

然后在main.ts 引入 通过app.use 注册session

import * as session from 'express-session'
/* ... */
app.use(session())

参数配置详解

参数 描述
secret 用于生成服务端 session 签名的加盐字符串
name 生成客户端 cookie 的名称,默认为 “connect.sid”
cookie 设置返回到前端的 cookie 属性,默认值为 { path: '/', httpOnly: true, secure: false, maxAge: null }
rolling 在每次请求时强制设置 cookie,这将重置 cookie 过期时间,默认为 false

配置示例

import { NestFactory } from '@nestjs/core';
import { VersioningType } from '@nestjs/common';
import { AppModule } from './app.module';
import * as session from 'express-session'
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableVersioning({
    type: VersioningType.URI
  })
  app.use(session({ secret: "XiaoMan", name: "xm.session", rolling: true, cookie: { maxAge: null } }))
  await app.listen(3000);
}
bootstrap();

后端nestjs 验证码插件 svgCaptcha

npm install svg-captcha -S

Providers

Provider 只是一个用 @Injectable() 装饰器注释的类

自定义名称

/* module.ts */
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service'; // 导入service
import { CatModule } from './cat/cat.module';

@Module({
  imports: [CatModule],
  controllers: [AppController],
  providers: [AppService], // 简写
  providers: [{
    provide: "newName",  // 需要在对应的 controller 中用 Inject 注明
    useClass: AppService
  }], // 全写
})
export class AppModule {}
/* controller.ts */
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(Inject("newName") private readonly appService: AppService) {} // 保持与全写中的provide相同

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

自定义注入值

providers: [{
	provider: "name",
    useValue: [], // 可以是数字、字符串、数组等
}]

工厂模式

providers: [{
	provider: "name",
    useFactory(){
    	// 函数逻辑
    }
}]
providers: [AppService, {
	provider: "name",
    inject: [AppService], // 此处注入的可以作为工厂函数的参数值,需要在provider数组前面有的
    useFactory(appService: AppService){
    	// 函数逻辑,支持异步
    }
}]

共享模块

在共享的模块内部的 exports 中添加对应共享的 service

/* cat.module.ts */
import { Module } from '@nestjs/common';
import { CatService } from './cat.service';
import { CatController } from './cat.controller';

@Module({
  controllers: [CatController],
  providers: [CatService],
  exports: [CatService],
})
export class CatModule {}

全局模块

创建一个模块,用 @Global 装饰
同样需要在 app.module 中进行导入,需要进行导出

/* config.module.ts */
import { Global, Module } from '@nestjs/common';

const config = {
  provide: 'config',
  useValue: {
    baseUrl: 'http://localhost:3000',
  },
};

@Global()
@Module({
  providers: [config],
  exports: [config],
})
export class ConfigModule {}

/* user.service.ts */
/* ... */
  constructor(
    private readonly catService: CatService,
    @Inject('config') private readonly config: any,
  ) {}
  
  findAll() {
    return this.config.baseUrl;
  }
/* ... */

动态模块(传参)

/* config.module.ts */
import { Global, Module } from '@nestjs/common';

const config = {
  provide: 'config',
  useValue: {
    baseUrl: 'http://localhost:3000',
  },
};

@Global()
@Module({
  providers: [config],
  exports: [config],
})
export class ConfigModule {
  static register(path: string) { // 关键实现
    config.useValue.baseUrl += path;
    return {
      module: ConfigModule,
      providers: [config],
      exports: [config],
    };
  }
}

/* app.module.ts */
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { CatModule } from './cat/cat.module';
import { ConfigModule } from './config/config.module';

@Module({
  imports: [UserModule, CatModule, ConfigModule.register('/cats')], // 注册方式
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

中间件

局部中间件

定义
/* logger.middleware.ts */
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: () => void) {
    console.log('request url:', req.url);
    next();
  }
}

使用:在 module 进行导入
/* app.module.ts */
@Module({
  imports: [UserModule, CatModule, ConfigModule.register('/cats')],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes(''); // 关键实现,forRoutes指定路由,也可以使用对象配置或指定控制器
    // {path: 'user', method: RequestMethod.GET}
    // UserController
  }
}

全局中间件

基本和局部中间件一样

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

// 定义
function LoggerMiddlewareAll(req, res, next) {
  console.log('request url:', req.url);
  next();
}

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(LoggerMiddlewareAll); // 使用
  await app.listen(3000);
}
bootstrap();

文件流(暂无)

RxJS 和 NestJS

概念

RxJs 使用的是观察者模式,用来编写异步队列和事件处理。

  • Observable 可观察的物件
  • Subscription 监听Observable
  • Operators 纯函数可以处理管道的数据 如 map filter concat reduce 等

案例

类似于迭代器 next 发出通知 complete 通知完成
subscribe 订阅 observable 发出的通知 也就是一个观察者

import { Observable } from "rxjs";
 
//类似于迭代器 next 发出通知  complete通知完成
const observable = new Observable(subscriber=>{
    subscriber.next(1)
    subscriber.next(2)
    subscriber.next(3)
 
    setTimeout(()=>{
        subscriber.next(4)
        subscriber.complete()
    },1000)
})
 
observable.subscribe({
    next:(value)=>{
       console.log(value)  // 1 2 3 (1s后) 4
    }
})

实现一个响应拦截器

定义

/* 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, next: CallHandler): Observable<Data<T>> {
    return next.handle().pipe(map((data) => ({ data })));
  }
}

使用


async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(LoggerMiddlewareAll);

  app.useGlobalInterceptors(new Response()); // 关键实现
  await app.listen(3000);
}
bootstrap();

异常拦截器

/* exception.ts */
import { HttpException } from '@nestjs/common';
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)  // 捕捉异常类型
export class HttpExceptionFilter implements ExceptionFilter {
    catch(exception: HttpException, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const request = ctx.getRequest<Request>();
        const response = ctx.getResponse<Response>();
        response.status(exception.getStatus()).json({  // 返回 json 数据
            errMsg: exception.message,
            errCode: exception.getStatus(),
            path: request.url,
            time: new Date().toLocaleString(),
        });
    }
}

/* main.ts */
/* ... */
async function bootstrap() {
    const app = await NestFactory.create(AppModule); // 创建应用
    app.use(LoggerMiddlewareAll); // 日志中间件
    app.useGlobalFilters(new HttpExceptionFilter()); // 全局异常过滤器

    app.useGlobalInterceptors(new Response()); // 全局响应拦截器
    await app.listen(3000);
}
bootstrap();

管道转换

管道 可以做两件事:

  1. 转换,可以将前端传入的数据转成成我们需要的数据
  2. 验证 类似于前端的rules 配置验证规则

Nestjs 提供了八个内置转换 API

  • ValidationPipe
  • ParseIntPipe
  • ParseFloatPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • ParseEnumPipe
  • DefaultValuePipe

使用:转换异常会抛出异常

/* cat.controller.ts */
import {
    Controller,
    Get,
    Post,
    Body,
    Patch,
    Param,
    Delete,
    ParseIntPipe, // 引入
} from '@nestjs/common';
    /* ... */

@Controller('cat')
export class CatController {
    /* ... */
    @Get(':id')
    findOne(@Param('id', ParseIntPipe) id: number) { // 直接使用ParseIntPipe
        console.log('======>', typeof id);
        return this.catService.findOne(id);
    }
    /* ... */

管道验证 dto

创建一个管道文件

$ nest g pi login 

将会生成一个 login.pipe.ts 文件

在 dto 文件中定义并导出 class

安装常用的 class 验证模块

$ pnpm i --save class-validator class-transformer
/* login.dto.ts */
import { IsNotEmpty } from 'class-validator';  // 引入验证库

export class CreateLoginDto {
    @IsNotEmpty({ message: 'username is required' }) // 装饰器
    username: string;
}
/* login.pipe.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 {
    async transform(value: any, metadata: ArgumentMetadata) {
        const dto = plainToInstance(metadata.metatype, value); // 实例化函数
        const errors = await validate(dto); // 验证函数
        if (errors.length > 0) {
            const errMsg = errors.reduce(
                (acc, curr) =>
                    `${acc}${Object.values(curr.constraints).join('; ')}`,
                '',
            );
            throw new HttpException(errMsg, HttpStatus.BAD_REQUEST);
        }
        return value;
    }
}

/* login.controller.ts */
/* ... */
    @Post()
    create(@Body(LoginPipe) createLoginDto: CreateLoginDto) {
        return this.loginService.create(createLoginDto);
    }
/* ... */

守卫

定义

/* guard.ts */
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class GuardGuard implements CanActivate {
    canActivate(
        context: ExecutionContext,
    ): boolean | Promise<boolean> | Observable<boolean> {
        console.log(context);
        return true;
    }
}

局部引入

/* controller.ts */
/* ... */
import { GuardGuard } from './guard.guard';

@Controller('guard')
@UseGuards(GuardGuard)
export class GuardController {
    constructor(private readonly guardService: GuardService) {}
/* ... */

全局引入

/* main.ts */
/* ... */
import { GuardGuard } from './guard/guard.guard'; // 引入

function LoggerMiddlewareAll(req, res, next) {
    console.log('request url:', req.url);
    next();
}

async function bootstrap() {
    const app = await NestFactory.create(AppModule); // 创建应用
	/* ... */
    app.useGlobalGuards(new GuardGuard()); // 全局守卫
    await app.listen(3000);
/* ... */

控制权限(路由守卫)

/* guard.ts */
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
import type { Request } from 'express';

@Injectable()
export class GuardGuard implements CanActivate {
    constructor(private readonly reflector: Reflector) {}
    canActivate(
        context: ExecutionContext,
    ): boolean | Promise<boolean> | Observable<boolean> {
        const admin = this.reflector.get<string[]>(
            'role',
            context.getHandler(),
        ); // 获取路由前定义的@SetMetadata
        const request = context.switchToHttp().getRequest<Request>(); // 转换到http响应模式
        if (admin.includes(request.query.role as string)) {
            return true;
        } else {
            return false;
        }
    }
}

自定义装饰器

定义
/* decorator.ts */
import { SetMetadata } from '@nestjs/common';

export const Guard = (args: string[]) => SetMetadata('role', args);
应用
/* ... */
import { Guard } from './guard.decorator'; // 导入

@Controller('guard')
@UseGuards(GuardGuard)
export class GuardController {
/* ... */
    @Get()
    @Guard(['admin']) // 用于代替 SetMetadata('role', ['admin'])
    findAll() {
        return this.guardService.findAll();
    }
/* ... */

参数装饰器

定义
/* decorator.ts */
import {
    SetMetadata,
    createParamDecorator,
    ExecutionContext,
} from '@nestjs/common';

export const Guard = (args: string[]) => SetMetadata('role', args);

export const getUrl = createParamDecorator(  // 定义参数装饰器
    (data: string, ctx: ExecutionContext) => { // data:传入,ctx:聚合上下文
        const req = ctx.switchToHttp().getRequest();
        return req.url; // 返回
    },
);
应用
/* controller.ts */
/* ... */
import { Guard, getUrl } from './guard.decorator'; // 引入

/* ... */
    @Get()
    findAll(@getUrl('urlParams') url: string) { // 传入、传出
        console.log('url-return:', url);
        return this.guardService.findAll();
    }
/* ... */

聚合装饰器

swagger接口文档

TypeORM链接数据库

安装TypeORM

pnpm install --save @nestjs/typeorm typeorm mysql2

初始化

/* app.module.ts */
/* ... */
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
    imports: [
    /* ... */
        TypeOrmModule.forRoot({
            type: 'mysql', //数据库类型
            username: '****', //账号
            password: '****', //密码
            host: 'localhost', //host
            port: 3306, //
            database: 'nest', //库名
            entities: [__dirname + '/**/*.entity{.ts,.js}'], //实体文件
            synchronize: true, //synchronize字段代表是否自动将实体类同步到数据库
            retryDelay: 500, //重试连接数据库间隔
            retryAttempts: 10, //重试连接数据库的次数
            autoLoadEntities: true, //如果为true,将自动加载实体 forFeature()方法注册的每个实体都将自动添加到配置对象的实体数组中
        }),
    ],
    controllers: [AppController],
    providers: [AppService],
})
/* ... */

使用

在 entities 中定义数据库表信息,TypeORM将会自动创建并关联表

/* dbtest.entity.ts */
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Dbtest {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @Column()
    email: string;

    @Column()
    phone: string;
}
/* ... */
import { TypeOrmModule } from '@nestjs/typeorm';
import { Dbtest } from './entities/dbtest.entity';

@Module({
    imports: [TypeOrmModule.forFeature([Dbtest])], // 初始化
    controllers: [DbtestController],
    providers: [DbtestService],
})
export class DbtestModule {}

定义实体

http://t.csdn.cn/WtcNo

CURD

/* controller.ts */
/* ... */
import { InjectRepository } from '@nestjs/typeorm';
import { Dbtest } from './entities/dbtest.entity';
import { Repository } from 'typeorm';

@Controller('dbtest')
export class DbtestController {
    constructor(
        @InjectRepository(Dbtest)
        private readonly dbtest: Repository<Dbtest>, // typrorm
        private readonly dbtestService: DbtestService,
    ) {}
/* ... */
    @Get()
    addOne(@Query('name') name: string) {
        return this.dbtest.save({ // 新增
            name,
        });
    }

    @Get(':id')
    findOne(@Param('id') id: string) {
        return this.dbtest.find({ // 查询
            where: { id: +id },
        });
    }
/* ... */

联表

事务

事务具有4个基本特征,分别是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Duration),简称ACID

① 原子性
事务的原子性是指事务必须是一个原子的操作序列单元。事务中包含的各项操作在一次执行过程中,只允许出现两种状态之一,要么都成功,要么都失败

任何一项操作都会导致整个事务的失败,同时其它已经被执行的操作都将被撤销并回滚,只有所有的操作全部成功,整个事务才算是成功完成

② 一致性(Consistency)
事务的一致性是指事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处以一致性状态。

③ 隔离性
事务的隔离性是指在并发环境中,并发的事务是互相隔离的,一个事务的执行不能被其它事务干扰。也就是说,不同的事务并发操作相同的数据时,每个事务都有各自完整的数据空间。

一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务是不能互相干扰的

④ 持久性(Duration)
事务的持久性是指事务一旦提交后,数据库中的数据必须被永久的保存下来。即使服务器系统崩溃或服务器宕机等故障。只要数据库重新启动,那么一定能够将其恢复到事务成功结束后的状态