1、nest.js中session的使用
介绍:session 是另一种记录客户状态的机制,不同的是 Cookie 保存在客户端浏览器中,而 session 保存 在服务器上
工作流:当浏览器访问服务器并发送第一次请求时,服务器端会创建一个 session 对象,生成一个类似于 key,value 的键值对,然后将 key(cookie)返回到浏览器(客户)端,浏览器下次再访问时,携带 key(cookie), 找到对应的 session(value)。 客户的信息都保存在 session 中
具体文档:https://docs.nestjs.com/techniques/session
安装
npm i express-session --save
npm i -D @types/express-session --save
引入和配置(main.ts)
import * as session from 'express-session'
app.use(session({
secret: 'this is secret key',
cookie: {
maxAge: 6000,
httpOnly: true
},
rolling: true //表示在每次请求时强行设置 cookie,这将重置 cookie 过期时间
}))
使用方式
import { Controller, Get, Req, Session } from '@nestjs/common';
import { YfService } from './yf.service';
@Controller('yf')
export class YfController {
public constructor(private yfservice: YfService) {}
@Get()
public index(@Session() session: Record<string, any>) {
session.name = 'this is session test'
return 'this is test'
}
@Get('cookie')
public getCookie(@Session() session: Record<string, any>): string {
return session.name;
}
}
注意:也可以通过request.session进行存储与读取
session配置的常用方法
app.use(session({
resave: false,
saveUninitialized: true,
secret: '12345',
name: 'name',
cookie: {maxAge: 60000},
resave: false,
saveUninitialized: true
}));
参数说明
secret: 一个 String 类型的字符串,作为服务器端生成 session 的签名。
name: 返回客户端的 key 的名称,默认为 connect.sid,也可以自己设置。
resave:强制保存 session 即使它并没有变化,。默认为 true。建议设置成 false。 don't save session if unmodified
saveUninitialized:强制将未初始化的 session 存储。当新建了一个 session 且未设定属性或值时,它就处于 未初始化状态。在设定一个 cookie 前,这对于登陆验证,减轻服务端存储压力,权限控制是有帮助的。(默 认:true)。建议手动添加。
cookie: 设置返回到前端 key 的属性,默认值为{ path: ‘/’, httpOnly: true, secure: false, maxAge: null }。
rolling: 在每次请求时强行设置 cookie,这将重置 cookie 过期时间(默认:false)
常用方法
req.session.destroy(function(err) { /*销毁 session*/
})
req.session.username='张三'; //设置 session
req.session.username //获取 session
req.session.cookie.maxAge=0; //重新设置 cookie 的过期时
也可以使用装饰器删除
2、nest.js文件上传
单文件上传
import { Controller, Get, Post, UseInterceptors, UploadedFile } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { createWriteStream } from 'fs'; import { join } from 'path'; @Controller('upload') export class UploadController { @Get() public index() { return 'this is upload test'; } @Post('addfile') @UseInterceptors(FileInterceptor('file')) //这里面的file是表示<input type='file' name='file'>里的name用axios上传默认是file public addFile(@UploadedFile() file) { // 获取参数的UploadedFile装饰器 const writeImage = createWriteStream( join('public/upload', `${file.originalname}`) //注意这里没有/的路径是指定根目录下的public目录 ); writeImage.write(file.buffer); return { data: 'ok' }; } }
注意:如果进行表单文件上传,那么一定要添加属性 enctype="multipart/form-data"
前端代码
<template> <div class="upload-demo"> <input type="file" ref="fileInt" @change="changeHandle" /> </div> </template> <script> import { getInfo } from '@/api/login' import { setToken, getToken } from '@/utils/auth' export default { name: 'login', methods: { changeHandle() { const file = this.$refs.fileInt.files[0] console.log(file) const data = new FormData() data.append('file', file) //注意:这里相当于指定了name getInfo(data) .then((res) => { console.log(res) }) .catch((err) => { console.log(err) }) }, }, } </script> <style scoped></style>
请求
import request from '@/utils/request' export const getInfo = (data) => { return request({ url: '/upload/addfile', method: 'post', data, Headers: { 'Content-Type': 'multipart/form-data', }, onUploadProgress: function(progressEvent) { const complete = parseInt( ((progressEvent.loaded / progressEvent.total) * 100) | 0 ) // 这里为上传的进度 console.log(complete) }, // onDownloadProgress //下载 }) }
附带请求流数据的写法(axios案例)
const downloadResource = async (url: string, progress?: (progressEvent: ProgressEvent) => void) => {
let {data} = await httpClient.request<ArrayBuffer>({
method: 'get',
url: encodeURI(url),
responseType: 'arraybuffer',
onDownloadProgress(progressEvent) {
console.log(progressEvent);
// 下载进度数据
if (progress) progress(progressEvent);
},
});
fs.writeFileSync(join(getUploadCachePath(), basename(url)), Buffer.from(data));
};
注意:上面写法是在文件数据全部请求完整后再进行写入,这期间会占用浏览器的内存,如果文件的空间占用不大则问题不大,如果文件比较大建议使用以下的方式进行下载
axios
.get(
'https://xxx.zip',
{
responseType: 'stream',
onDownloadProgress(ev) {
console.log(ev);
},
},
)
.then((res: Stream) => {
res.pipe(fs.createWriteStream('dist/test.zip'));
res.on('error', () => {// 执行错误的操作})
res.on('end', () => {
// 执行下载完成的操作
})
});
注意:如果没有用input file进行选择文件,那么就需要用fs文件系统进行读取文件
private async uploadComponent(path: string) {
const file = new FormData(); // 可以用new File来模拟file文件
let buffer = readFileSync(path); // 这个是从fs-extra中获取的方法
file.append('file', new Blob([buffer.buffer])); // 或者使用new File进行转换上传
//通过发送ajax请求进行上传
。。。
}
多文件上传
import { Controller, Get, Post, UseInterceptors, UploadedFiles } from '@nestjs/common'; import { FilesInterceptor, AnyFilesInterceptor, FileFieldsInterceptor } from '@nestjs/platform-express'; import { createWriteStream } from 'fs'; import { join } from 'path'; @Controller('upload') export class UploadController { @Get() public index() { return 'this is upload test'; } @Post('addfile') @UseInterceptors(FilesInterceptor('file')) //如果多文件使用的是一个名称 // @UseInterceptors(AnyFilesInterceptor()) //如果多文件使用的是一个名称 // @UseInterceptors(FileFieldsInterceptor([ // 如果指定的是多个名称的,那么需要按如下配置 // { name: 'avatar', maxCount: 1 }, // { name: 'background', maxCount: 1 }, // ])) public addFile(@UploadedFiles() file) { // 获取参数的UploadedFile装饰器 file.forEach(val => { const writeImage = createWriteStream( join('public/upload', `${val.originalname}`) //注意这里没有/的路径是指定根目录下的public目录 ); writeImage.write(val.buffer, err => { if (err) console.log(err); }); writeImage.end(() => { console.log('end'); }); }); return { data: 'ok' }; } }
前端代码
<template>
<div class="upload-demo">
<input
type="file"
ref="fileInt"
@change="changeHandle"
multiple="multiple"
/>
</div>
</template>
<script>
import { getInfo } from '@/api/login'
import { setToken, getToken } from '@/utils/auth'
export default {
name: 'login',
methods: {
changeHandle() {
const file = this.$refs.fileInt.files
console.log(file)
const data = new FormData()
file.forEach((val) => {
data.append('file', val)
})
// data.append('file', file)
getInfo(data)
.then((res) => {
console.log(res)
})
.catch((err) => {
console.log(err)
})
},
},
}
</script>
request请求的代码不变
如果前端得到后端的数据是{type: Buffer, data: [...]}那么用如下方法进行转换成文件
const len = value.data.length;
const buf = new ArrayBuffer(len);
const view = new Uint8Array(buf);
value.data.forEach((value, key) => {
view[key] = value;
});
let contentUrl = window.URL.createObjectURL(new Blob([buf]));
let link = document.createElement('a');
link.href = contentUrl;
link.setAttribute('download', 'text.txt'); // 文件名称,这里很重要,如果没有完整的文件名(包括扩展名),下载下来还需要自行添加
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(contentUrl);
3、nest.js的中间件
在nest中, 中间件就是匹配路由之前或者匹配路由完成做的一系列的操作。中间件中如果想往下 匹配的话,那么需要写 next()
中间件的创建命令
nest g middleware middleware/init // 表示在middleware的文件夹下创建initMiddleware中间件
类的中间件
import { Injectable, NestMiddleware } from '@nestjs/common'; import { Response } from 'express'; @Injectable() export class InitMiddleware implements NestMiddleware { //在类的中间件中需要实现NestMiddleware接口,并且实现里面的use方法 use(req: any, res: Response, next: () => void) { // 接收三个参数,req, res, next,如果需要向下执行,需要调nest()方法,否则请求会被挂起 console.log('this is test'); next(); } }
函数中间件
// 函数式中间件接收三个参数 export const logger = (req: Request, res: Response, next: () => void) => { console.log('this is logger'); next(); };
中间件的注册(需要注册到model里,model需要实现NestModule的接口)
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware) //应用LoggerMiddleware中间件
.forRoutes('cats/:catName'); //指定中间件的应用路径/cats/:catName
//.forRoutes('cats');
//也可以通过如下方式指定包含中间件的请求方法
// .forRoutes({ path: 'cats', method: RequestMethod.GET });
//也可以使用通配符来匹配路径,如以下示例
//forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });
// 通过以下方法来针对不同控制器使用中间件,也可以传递一个由逗号分隔的控制器列表
//.forRoutes(CatsController);
// 通过exclude和路径方法来排除特定路径
//.exclude(
//{ path: 'cats', method: RequestMethod.GET },
//{ path: 'cats', method: RequestMethod.POST })
}
}
调用多个中间件
consumer.apply(InitMiddleware, logger).forRoutes('*');
注册全局中间件
const app = await NestFactory.create(AppModule);
app.use(logger); // 目前测试这个只支持函数式中间件
await app.listen(3000);
4、nest.js中管道(pipe)的使用
管道(Pipes)是一个用 @Injectable() 装饰过的类,它必须实现 PipeTransform 接口。
从官方的示意图中我们可以看出来管道 pipe 和过滤器 filter 之间的关系:管道偏向于服务端控制器逻辑,过滤器则更适合用客户端逻辑。
过滤器在客户端发送请求后处理,管道则在控制器接收请求前处理。
管道通常有两种作用:
-
转换/变形:转换输入数据为目标格式
-
验证:对输入数据时行验证,如果合法让数据通过管道,否则抛出异常。
目前nest.js中内置了两个验证的类ParseIntPipe 与 ParseUUIDPipe具体使用如下
@Get(':id') public getParamData( @Query('age', ParseIntPipe) query, @Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.GONE })) param ) { console.log(query, param); return 'this is getParamData'; }
自定义管道
创建自定义管道
nest g pipe pipe/parseTestPipe
使用joi库进行验证
$ npm install --save joi
$ npm install --save-dev @types/joi
新建管道
import {
ArgumentMetadata,
Injectable,
PipeTransform,
BadRequestException
} from '@nestjs/common';
@Injectable()
export class ParseTestPipe implements PipeTransform {
public constructor(private readonly schema) {} // schema是外面传入的joi对象
transform(value: any, metadata: ArgumentMetadata) { // 可以从metadata文件中获取验证的数据类型,是body, query, param, custom
console.log(metadata);
if (metadata.type === 'query') { //也可以把类型通过传参的形式传递
const { error } = this.schema.validate(value);
console.log(error);
if (error) {
throw new BadRequestException('Validation failed');
}
}
return value; //如果数据没有问题,那么返回,验证处程序可以正常往下跑
}
}
例如下面的写法
import {
ArgumentMetadata,
Injectable,
PipeTransform,
BadRequestException
} from '@nestjs/common';
@Injectable()
export class ParseTestPipe implements PipeTransform {
public constructor(private readonly schema, private readonly type?) {} // schema是外面传入的joi对象
transform(value: any, metadata: ArgumentMetadata) {
// 可以从metadata文件中获取验证的数据类型,是body, query, param, custom
console.log(this.type);
if (!this.type || metadata.type === this.type) {
const { error } = this.schema.validate(value);
console.log(error);
if (error) {
throw new BadRequestException('Validation failed');
}
}
return value;
}
}
控制器调用的写法
import { Controller, Get, Param, Query, UsePipes } from '@nestjs/common'; import { ParseTestPipe } from 'src/pipe/parse-test-pipe.pipe'; import * as Joi from 'joi'; const schema = Joi.object({ //joi的验证对象 name: Joi.string().required(), age: Joi.number().required() }); @Controller('test') export class TestController { @Get() public index() { return 'this is test'; } @Get(':id') @UsePipes( //这里可以传一个参数,也可以传两个参数 new ParseTestPipe(schema, 'query'), new ParseTestPipe(Joi.number().required(), 'param') ) public getParamData(@Query() query, @Param('id') param) { console.log(query, param); return 'this is getParamData'; } }
注意:如果在usePipes里面的类型检测没有指定检测对象的话,默认会进行全部检测
类验证器
需要安装依赖
npm i --save class-validator class-transformer
对类验证器的扩展依赖
npm i --save @nestjs/mapped-types
在dtos文件下新建验证文件User.dto.ts
import { IsNumber, IsString, IsNumberString } from 'class-validator';
import {
PickType,
OmitType,
PartialType,
IntersectionType
} from '@nestjs/mapped-types';
export class UserDto {
@IsString()
public name: string;
@IsNumberString()
public age: number;
@IsNumber()
public id: number;
}
//表示从UserDto中继承了一些属性进行验证
export class PickUserDto extends PickType(UserDto, ['name', 'age'] as const) {}
//表示从UserDto中继承除了id的所有属性进行验证
export class OmitUserDto extends OmitType(UserDto, ['id'] as const) {}
//表示继承所有属性,但是所有属性都是可选的,相当于只验证正确性,不验证存在性
export class PartialUserDto extends PartialType(UserDto) {}
//把指定的两个类合并继承,那么就拥有了两个类的所有属性
export class IntersectionDto extends IntersectionType(PickUserDto, OmitUserDto) {}
使用示例
import { Type } from 'class-transformer';
import { IsArray, IsUUID, Length, ValidateIf, ValidateNested } from 'class-validator';
import { CourseIdDto } from './course.dto';
// 页面id验证
export class PageIdDto {
@IsUUID('all', { message: '请传入合法的page id' })
public id: string;
}
export class PageInfoDto extends PageIdDto {
@Length(1, 50, { message: '课程名称需要在1-50个字符内' })
public name: string;
@IsUUID('all', { message: '请传入合法的asset id' })
public asset: string;
@IsUUID('all', { message: 'next的id不合法' })
@ValidateIf((object: any, value: any) => !!value) // 如果next Id为空或者是uuid
public next: string;
@IsUUID('all', { message: 'timeline的id不合法' })
@ValidateIf((object: any, value: any) => !!value) // 如果next Id为空或者是uuid
public timeline: string;
@ValidateIf((object: any, value: any) => value && value.length !== 0) // 如果next Id为空或者是uuid
@IsArray() // 较验数组里的每一项
@IsUUID('all', { each: true, message: 'branches里的数据不合法,该数值为页面的uuid' })
public branches: Array<string>;
}
export class CourseAddPageDto extends CourseIdDto {
@IsArray({ message: 'pages需要是一个页面数组' })
@ValidateNested()
@Type(() => PageInfoDto)
public pages: PageInfoDto[];
}
也可以使用正则进行匹配
export class TestItemDto {
@Matches(/^https?/i)
@IsNotEmpty()
href: string;
}
在框架中引用的方式
方法一,在需要使用的方法中引入
@Post()
async create(
@Body(new ValidationPipe()) userDto: UserDto,
) {
this.userService.create(UserDto);
}
方法二,使用装饰器引入
@Get(':id')
@UsePipes(
new ValidationPipe({
transform: true
})
)
public getParamData(@Query() query: PartialUserDto, @Param('id') param) {
console.log(query, param);
return 'this is getParamData';
}
方法三,在全局进行引入
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
...
//管道验证配置
app.useGlobalPipes(
new ValidationPipe({
transform: true
})
);
await app.listen(3000);
}
bootstrap();
注意:ValidationPipe里的配置可以查看官网 https://docs.nestjs.com/techniques/validation class-validator的使用方法 https://www.npmjs.com/package/class-validator
可以对数据验证的报错信息进行修改
import { CustomError } from '@app/errors/custom.error';
import { ResponseStatus } from '@app/interfaces/response.interface';
import { ValidationError, ValidationPipe, ValidationPipeOptions, HttpStatus } from '@nestjs/common';
import { HttpErrorByCode } from '@nestjs/common/utils/http-error-by-code.util';
export default class CustomValidationPipe extends ValidationPipe {
public constructor(options: ValidationPipeOptions) {
super(options);
}
public createExceptionFactory(validationErrors?: ValidationError[]) {
return (validationErrors = []) => {
if (this.isDetailedOutputDisabled)
return new HttpErrorByCode[this.errorHttpStatusCode]();
const errors = this.flattenValidationErrors(validationErrors);
return new CustomError(
{ status: ResponseStatus.Error, message: errors[0] },
this.errorHttpStatusCode || HttpStatus.OK,
);
};
}
}
5、nest.js中Module的使用
模块(Module)是一个使用了 @Module() 装饰的类。@Module() 装饰器提供了一些 Nest 需要使用的元数据,用来组织应用程序的结构。
module的创建
nest g module ***/**/testModule
@Module() 装饰器接收一个参数对象,有以下取值
module的使用
在controllers中定义相关的controller控制器,但是在controller中需要使用的相当的服务, 那么就需要在provides中引入相当的服务,但是如果这个服务在其他的模块,那么为了方便开发,可以使用模块的导入与导出, 需要被共享部份的modules需要导出服务,而引入其他萨法诺娃的modules需要导入服务,具体见下面例子
需要被共享部份的modules
import { Module } from '@nestjs/common';
import { BasicController } from './controllers/basic/basic.controller';
import { UserService } from './services/user/user.service';
@Module({
controllers: [BasicController],
providers: [UserService],
exports: [UserService] // 服务的导出
})
export class BasicModule {}
需要引入模块的模块
import { UserService } from './../basic/services/user/user.service';
import { YfService } from './../../yf/yf.service';
import { BasicModule } from './../basic/basic.module';
import { NewsService } from './service/news/news.service';
import { Module } from '@nestjs/common';
import { UpgradeController } from './controllers/upgrade/upgrade.controller';
@Module({
imports: [BasicModule],
controllers: [UpgradeController],
providers: [NewsService, YfService]
})
export class UpgradeModule {}
这样就可以在controller中正常的导入服务进行使用了
import { YfService } from './../../../../yf/yf.service';
import { UserService } from './../../../basic/services/user/user.service';
import { NewsService } from './../../service/news/news.service';
import { Controller, Get } from '@nestjs/common';
@Controller('upgrade')
export class UpgradeController {
public constructor(
private readonly newsService: NewsService,
private readonly userService: UserService,
private readonly yfService: YfService
) {}
@Get()
public index() {
return {
user: this.userService.getInfo(),
news: this.newsService.getNewsList(),
other: this.yfService.getAll()
};
}
}
如果一个模块经常被其他模块使用,也就是经常被引用,那么可以考虑设置为全局模块,那么其他模块可以在不引入当前模块的前提下使用该模块
import { Module, Global } from '@nestjs/common';
import { BasicController } from './controllers/basic/basic.controller';
import { UserService } from './services/user/user.service';
@Global()
@Module({
controllers: [BasicController],
providers: [UserService],
exports: [UserService]
})
export class BasicModule {}
6、nest.js中的守卫
守卫是一个使用 @Injectable() 装饰器的类。 守卫应该实现 CanActivate 接口。
守卫有一个单独的责任。它们确定请求是否应该由路由处理程序处理。到目前为止,访问限 制逻辑大多在中间件内。这样很好,因为诸如 token 验证或将 request 对象附加属性与 特定路由没有强关联。但中间件是非常笨的。它不知道调用 next() 函数后会执行哪个处 理程序。另一方面,守卫可以访问 ExecutionContext 对象,所以我们确切知道将要执行 什么。
说白了:在 Nextjs 中如果我们想做权限判断的话可以在守卫中完成,也可以在中间件中完 成。
文档位置: https://docs.nestjs.com/guards
创建守卫
nest g guard guard/auth
示例如下
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
return true; // 如果返回true则会向访问到需要访问的地址,如果返回false那么就会报403错误
}
}
使用守卫
可以对整个控制器中所有的地址使用,也可以对控制器中指定的方法进行使用
import { NewsService } from './../../service/news/news.service';
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from 'src/guard/auth.guard';
@Controller('upgrade')
@UseGuards(AuthGuard) // 如果在controller中使用路由守卫, 那么就会对该控制器下的所有地址执行路由守卫
export class UpgradeController {
public constructor(private readonly newsService: NewsService) {}
@Get()
public index() {
return {
news: this.newsService.getNewsList()
};
}
@Get('test')
@UseGuards(AuthGuard) // 如果只在方法中使用,那么只对指定的方法指定路由守卫
public getTest() {
return 'this is test';
}
}
在全局中使用路由守卫
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
守卫的参数
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
console.log('执行了守卫');
console.log(context.switchToHttp().getRequest().cookies); // 获取cookies
console.log(context.switchToHttp().getRequest().session); // 获取session
console.log(context.switchToHttp().getRequest().path); // 获取需要访问的路由地址
return true; // 如果返回true则会向访问到需要访问的地址,如果返回false那么就会报403错误
}
}
可以使用SetMetadata配置角色信息
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
使用
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
守卫中处理
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return matchRoles(roles, user.roles);
}
}
注意: 这个配置角色可以和自定义装饰器里的全部导入配合使用,配置@Get(路径) @Role引用外部的角色路径配置清单实现角色可配置化