跳到主要内容

NestJS-继续编码

这些集成方案会继续在开始编码的代码中继续编写。

Mysql

NestJS推荐使用@nestjs/typeorm typeorm mysql2的方案来接入mysql

安装

yarn add @nestjs/typeorm typeorm mysql2

配置与准备

这里会将Mysql对应的代码全部放在一个新的模块中

$ nest g res mysql

命令运行完成后会在src下生成一个mysql的文件夹,具体结构如下:

├─mysql.controller.spec.ts
├─mysql.controller.ts
├─mysql.module.ts
├─mysql.service.spec.ts
├─mysql.service.ts
├─entities
| └mysql.entity.ts
├─dto
| ├─create-mysql.dto.ts
| └update-mysql.dto.ts

接着配置,首先将mysql的连接配置写到.env

MYSQL_URL="mysql://nest:这里是密码@localhost:3306/nest-first?allowPublicKeyRetrieval=true"

编码

准备实体类src/mysql/entities/user.entity.ts

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

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

@Column()
firstName: string;

@Column()
lastName: string;

@Column({ default: true })
isActive: boolean;
}

修改app.module.ts,在 imports中加入如下代码

    TypeOrmModule.forRootAsync({
inject: [ConfigService],
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
type: 'mysql',
url: configService.get("MYSQL_URL"),
entities: ["dist/**/*.entity{.ts,.js}"],
synchronize: true,
}
}
}),
MysqlModule,MysqlModule,

接着修改mysql.module.ts

import { Module } from '@nestjs/common';
import { MysqlService } from './mysql.service';
import { MysqlController } from './mysql.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';

@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [MysqlController],
providers: [MysqlService]
})
export class MysqlModule {}

启动项目后数据库中已经自动创建好了user表,继续编写伪业务代码

// @@filename mysql.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateMysqlDto } from './dto/create-mysql.dto';
import { UpdateMysqlDto } from './dto/update-mysql.dto';
import { User } from './entities/user.entity';

@Injectable()
export class MysqlService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}

create(createMysqlDto: CreateMysqlDto) {
const user = this.usersRepository.create(createMysqlDto);
return this.usersRepository.insert(user);
}

findAll() {
return this.usersRepository.find();
}

findOne(id: number) {
return this.usersRepository.findOneBy({ id });
}

update(id: number, updateMysqlDto: UpdateMysqlDto) {
return this.usersRepository.update(id, updateMysqlDto);
}

remove(id: number) {
return this.usersRepository.delete(id);
}
}

现在就可以在http://localhost:3002/api中测试mysql下的api了,如果需要了解更多相关内容可以参考Database | NestJS - A progressive Node.js framework。代码地址:Github

任务调度

在特定时间需要做一些特定的事情或者有一些周期性的任务时就需要用到任务调度,在NestJS中有成熟的方案来处理它。在NestJS中任务调度允许您调度任意代码(方法/函数),以便在固定日期/时间、定期间隔或指定间隔后执行一次。

安装

$ yarn add @nestjs/schedule
$ yarn add -D @types/cron

激活

要激活任务调度需要在app.moduleimports中调用ScheduleModuleforRoot方法

imports: [ScheduleModule.forRoot()],

定义任务

NestJS中我们可以在任何一个provider中将一个方法定义为Job,分别在app.servicemysql.service中加入代码

// @@ src/app.service.ts
@Cron(CronExpression.EVERY_5_SECONDS)
appJob() {
console.log('appJob Running.', new Date());
}
// @@filename src/mysql/mysql.service.ts
@Cron(CronExpression.EVERY_10_SECONDS)
sqlJob() {
console.log('sqlJob Running.', new Date());
}

运行项目后能在控制太周期性的看到日志输出,说明任务已经正常调用。

更多相关内容请参考:Task Scheduling | NestJS - A progressive Node.js framework

源代码:Github

文件上传与下载

上传

为了处理文件上传,Nest 为 Express 提供了一个基于 multer 中间件包的内置模块。Multer 处理以multipart/form-data格式发布的数据,该格式主要用于通过 HTTP POST请求上传文件。该模块是完全可配置的,您可以根据您的应用程序要求做出相应的调整。

安装

为了更好的使用multer先安装类型模块@types/multer,安装完之后就可以使用Express.Multer.File

$ yarn add -D @types/multer

简单使用

Nest使用拦截器(Interceptor)先将上传的文件处理好再交给控制器来处理,所以需要在对应的路由上加上FileInterceptor拦截器

@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) {
console.log(file);
}

文件验证

在一些场景下我们需要验证文件的大小或者格式(比如:用户上传头像)。此时我们可以在UploadFile()装饰器调用的方法中传入ParseFilePipe并在构造时传入具体的验证

  @Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(
@UploadedFile(
new ParseFilePipe({
validators: [
new MaxFileSizeValidator({ maxSize: 1000 }),
new FileTypeValidator({ fileType: 'image/jpeg' }),
],
}),
)
file: Express.Multer.File,
) {
console.log('file', file);
}

数组文件

  @Post('uploadarray')
@UseInterceptors(FilesInterceptor('files'))
uploadFileArray(@UploadedFiles() files: Array<Express.Multer.File>) {
console.log(files);
}

多文件上传

  @Post('uploads')
@UseInterceptors(
FileFieldsInterceptor([
{ name: 'avatar', maxCount: 1 },
{ name: 'file', maxCount: 1 },
]),
)
uploadFiles(
@UploadedFiles()
files: {
avatar?: Express.Multer.File[];
file?: Express.Multer.File[];
},
) {
console.log(files);
}

不指定文件名

Nest提供了AnyFilesInterceptor拦截器来解析所有的文件

@Post('uploadany')
@UseInterceptors(AnyFilesInterceptor())
uploadFile(@UploadedFiles() files: Array<Express.Multer.File>) {
console.log(files);
}

配置文件存放目录

先将存放目录配置到.env

FILE_DEST="/codes/upload"

修改app.module,导入模块

    MulterModule.registerAsync({
inject: [ConfigService],
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
dest: configService.get("FILE_DEST")
}),
}),

下载文件

  • 完全读取文件内容再返回
  @Get("download")
@Header("content-type", "html/text")
async download() {
const content = await readFile(__filename);
return content.toString();
}
  • 使用流式下载
  @Get("pipe")
@Header("content-type", "html/text")
pipe(@Res() res: Response) {
const reader = createReadStream(__filename);
reader.pipe(res);
}
  • 一些头

Content-Disposition: attachment; filename="app.ts" 告诉浏览器按附件的方式去下载

"content-type:html/text 指定文件格式

Cookie和Session

虽然现在已经基本上不用这俩,但仍然可以了解一下

HTTP cookie是用户浏览器存储的一小块数据。Cookie被设计为网站记住有状态信息的可靠机制。当用户再次访问网站时,Cookie将随请求自动发送。

安装

$ yarn add cookie-parser
$ yarn add -D @types/cookie-parser

启用

修改app.module.ts中的bootstrap方法

import * as cookieParser from 'cookie-parser'; // 文件

// bootstrap方法中
app.use(cookieParser()); //

使用cookie

  @Get("cookie")
getCookie(@Req() req: Request) {
return req.cookies;
}

@Post("cookie")
setCookie(
@Body('key') key: string,
@Body('value') value: string,
@Res() res: Response,
) {
res.cookie(key, value);
};
}

更多cookie操作请查看:expressjs/cookie-parser: Parse HTTP request cookies (github.com)

Sesseion

Session提供了一种跨多个请求存储用户信息的方法,可以为用户保存一些状态

安装

$ yarn add express-session
$ yarn add -D @types/express-session

启用

// @@filename main.ts
import * as session from 'express-session';

// bootstrap method
app.use(
session({
secret: configService.get("SESSION_SECRET", 'nest-first'),
resave: false,
saveUninitialized: false,
}),
);

使用

  @Get('session')
findAll(@Session() session: Record<string, any>) {
session.visits = session.visits ? session.visits + 1 : 1;
return session;
}

更多Session相关的请参考expressjs/session: Simple session middleware for Express (github.com)

项目源码:Github

输入验证

Nest中可以使用管道来处理输入有效性的验证,在Nest中内置了一些管道来处理输入的验证与格式转化

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe

安装依赖

要使用内置的验证管道,需要先安装必要的依赖

$ yarn add class-validator class-transformer

配置与使用

1、定义DTO

// @@filename src/mysql/dto/create-mysql.dto.ts
import { IsBoolean, IsString } from "class-validator"
import { IsNotEmpty } from "class-validator/types/decorator/decorators"

export class CreateMysqlDto {

@IsString()
@IsNotEmpty()
firstName: string

@IsString()
@IsNotEmpty()
lastName: string

@IsBoolean()
isActive?: boolean
}

2、使用

使用管道验证一般有两种方法。其中一种是全局验证

// 修改 main.ts 
app.useGlobalPipes(new ValidationPipe());

另一种是在路由上加上特定注解

@UsePipes(new ValidationPipe({ transform: true }))

3、一些配置参数

Nest中内置了三个可选属性(transform, disableErrorMessages, `exceptionFactory),但是class-validator实际上有更多的属性

属性类型描述
enableDebugMessagesboolean如果设置为true,验证器将在出现错误时向控制台打印额外的警告消息。
skipUndefinedPropertiesboolean如果设置为true,那么验证器将跳过验证对象中未定义的所有属性的验证。
skipNullPropertiesboolean如果设置为true,那么验证器将跳过验证对象中为空的所有属性的验证。
skipMissingPropertiesboolean如果设置为true,那么验证器将跳过验证对象中为空或未定义的所有属性的验证。
whitelistboolean如果设置为true,验证器将从未使用任何验证修饰符的任何属性中剥离已验证(返回)对象。
forbidNonWhitelistedboolean如果设置为true,验证程序将抛出异常,而不是剥离非白名单的属性。
forbidUnknownValuesboolean如果设置为true,验证未知对象的尝试会立即失败。
disableErrorMessagesboolean如果设置为true,验证错误将不会返回给客户端。
errorHttpStatusCodenumber此设置允许您指定在出现错误时将使用的异常类型。默认情况下,它抛出“BadRequestException”。
exceptionFactoryFunction获取验证错误的数组,并返回要引发的异常对象。
groupsstring[]验证对象期间要使用的组。
alwaysboolean如果设置为true,验证将不使用默认消息。如果未显式设置错误消息,则错误消息始终未定义。
strictGroupsboolean如果组未给定或为空,请忽略至少有一个组的修饰符。
dismissDefaultMessagesboolean如果设置为true,验证将不使用默认消息。如果未显式设置错误消息,则错误消息始终未定义。
validationError.targetboolean指示是否应在验证错误中公开目标。
validationError.valueboolean指示验证值是否应在验证错误中公开。
stopAtFirstErrorboolean当设置为true时,给定属性的验证将在遇到第一个错误后停止。默认为false。

想要了解更多class-validator相关的信息可以参考:typestack/class-validator: Decorator-based property validation for classes. (github.com)

Json Web Token

一个完整的项目怎么能没有一套认证和授权,这里使用Json Web Token完成项目的认证和授权。

安装依赖

$ yarn add jsonwebtoken
$ yarn add -D @types/jsonwebtoken

编写Guard代码

@Injectable()
export class AuthenticationGuard implements CanActivate {
constructor(private readonly configService: ConfigService) {}

canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest<Request>();
return this.validateRequest(request);
}

private async validateRequest(request: Request) {
const token = request.headers['authorization'];
if (token) {
const parts = token.split(' ');
if (parts.length === 2 || parts[0] === 'Bearer') {
//Bearer token 是有效的
const secret = this.configService.get<string>('JWT_SECRET');
try {
const data = verify(parts[1], secret) as IUser;
request.user = data;
} catch {

}
}
}
return true;
}

}

@Injectable()
export class AuthorizationGuard implements CanActivate {

constructor(private readonly reflector: Reflector) {
}

canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
const roles = this.reflector.get<string[]>('role', context.getHandler());
const user: User = (request as any).user;
if(roles?.length && !user) throw new UnauthorizedException();
if (roles?.length && !roles.includes(user?.role))
throw new UnauthorizedException();
return true;
}
}

应用Guard

这里全局使用了,也可以在具体的Route或者Controller上使用,具体参看概念

// main.ts bootstrap中
const configService = app.get(ConfigService);
const reflector = app.get(Reflector);
app.useGlobalGuards(new AuthenticationGuard(configService));
app.useGlobalGuards(new AuthorizationGuard(reflector));

配置路由

使用@SetMetadata('role', ['vip', 'student'])来给vipstudent授权

import { Body, Controller, Get, Post, SetMetadata, UnauthorizedException } from "@nestjs/common";
import { hash, LoginDto, users } from "./user.dto";
import jwt from 'jsonwebtoken';
import { ConfigService } from "@nestjs/config";

@Controller('auth')
export class AuthController {

constructor(private readonly configService: ConfigService) {}

@Post("login")
login(@Body() { username, password }: LoginDto) {
const user = users.find(u => u.username === username);
if(!user) throw new UnauthorizedException("invaild username.")
if(hash(password) !== user.password) throw new UnauthorizedException("invaild password.");
const token = jwt.sign(user, this.configService.get("JWT_SECRET", 'JWT_SECRET'));
return {
...user,
token,
}
}

@Get()
all() {
return 'all';
}

@SetMetadata('role', ['admin'])
@Get("admin")
onlyAdmin() {
return "admin";
}

@SetMetadata('role', ['vip', 'student'])
@Get("vs")
vipAndStudent() {
return 'vip and student.'
}

}

代码: nest-first@784c191 · GitHub

日志

Nest有一个内置的基于文本的日志记录器,在应用程序启动和其他一些情况下使用,如显示捕获的异常(即系统日志)。该功能是通过@nestjs/common包中的Logger类提供的。默认的日志输出到控制台即Console

基本配置

// main.ts
// 关闭日志
const app = await NestFactory.create(AppModule, {
logger: false,
});

// 设置日志级别为 `warn` 和 `error`
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn'],
});

自定义日志

我们可以通过实现LoggerService中的方法来自定义一个日志记录器。

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

export class CustomerLogger implements LoggerService {
/** * Write a 'log' level log. */
log(message: any, ...optionalParams: any[]) {}

/** * Write an 'error' level log. */
error(message: any, ...optionalParams: any[]) {}

/** * Write a 'warn' level log. */
warn(message: any, ...optionalParams: any[]) {}

/** * Write a 'debug' level log. */
debug?(message: any, ...optionalParams: any[]) {}

/** * Write a 'verbose' level log. */
verbose?(message: any, ...optionalParams: any[]) {}
}

使用winston

需要了解winston的可以查看:GitHub - winstonjs/winston: A logger for just about everything.

安装依赖

$ yarn add nest-winston winston

配置

需要在注册WindstonModule的时候配置好日志的格式和文件存储的位置等,更多、更具体的配置项可以自行去官网:GitHub - gremo/nest-winston: A Nest module wrapper form winston logger 查看

// app.module.ts 先注册模块
imports: [ WinstonModule.forRoot({ /** options here */ }) ]

// 也可以使用异步方式
WinstonModule.forRootAsync({
// options
inject: [ConfigService],
imports: [ ConfigModule ],
useFactory: (configService: ConfigService) => ({

})
}),

替换Nest默认的日志

Nest默认使用Console记录日志,但是它也允许我们手动替换

// main.ts
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';

app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));

在需要的注入使用

import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';

@Controller('log')
export class LoggerController {
constructor(@Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService) { }

index() {
this.logger.warn('log -> index');
return "log";
}
}

使用文件存放日志

WinstonModule.forRootAsync({
// options
inject: [ConfigService],
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
level: 'info',
transports: [
// error.log中只存放error级别以上的日志
new winston.transports.File({
filename: 'error.log',
level: 'error',
}),
// 所有的日志都会输出到这里
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.ms(),
utilities.format.nestLike('best-first', {}),
),
}),
],
}),
}),

源代码

https://github.com/ChaoweiLuo/nest-first/commit/f1acf566b1a4126a7a35cfc83b3cb254f3c392bb