1, 介绍
- Nest使用强大的 HTTP Server 框架,如 Express(默认)和 Fastify。Nest 在这些框架之上提供了一定程度的抽象,同时也将其 API 直接暴露给开发人员。这样可以轻松使用每个平台的无数第三方模块。
周任务和开发设计项目背景
作为前端leader,组内和很多系统,每周会举行前端周会,会议分三个模块
- 周任务回顾
- 代码评审
- 知识分享
公司内部有confluence,svn等系统存放文档,但是觉得其格式不太符合我的需求,需要有汇总分析功能。于是自己写了个系统,主要是给前端团队管理周任务和开发设计。
2,技术栈
前端采用vue3框架,主要分为
- 首页 - 我的代办任务
- 系统版本 - 列表 (增删改查)
- 任务列表 --关联 系统版本 列表 (增删改查)
任务下采用md记载基本描述,以及开发设计 - 周任务列表 -- 关联 任务 列表 (增删改查)
- 用户管理 -- 管理员新增用户,普通用户只能修改自己密码
- 登陆
后台采用 nest 框架,搭配一台mysql 对数据持久化存储
- multer 对文件管理,md支持粘贴图片,功能参考 掘金 md编辑器
- swagger 对api进行管理
- jwt 对登陆认证
- nest 守卫对增删改接口 控制
- minio 文件存储
3,nest概念
3.1、Exception filters异常过滤器
Exception filters异常过滤器可以捕获在后端接受处理任何阶段所跑出的异常,捕获到异常后,然后返回处理过的异常结果给客户端(比如返回错误码,错误提示信息等等)。
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception.getStatus();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
我们可以看到host是实现了ArgumentsHost接口的,在host中可以获取运行环境中的信息,如果在http请求中那么可以获取request和response,如果在socket中也可以获取client和data信息。
同样的,对于异常过滤器,我们可以指定在某一个模块中使用,或者指定其在全局使用等。
3.2、Pipes管道
Pipes一般用户验证请求中参数是否符合要求,起到一个校验参数的功能。
比如我们对于一个请求中的某些参数,需要校验或者转化参数的类型:
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}
上述的ParseIntPipe就可以把参数转化成十进制的整型数字。我们可以这样使用:
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
return await this.catsService.findOne(id);
}
对于get请求中的参数id,调用new ParseIntPipe方法来将id参数转化成十进制的整数。
3.3、middleware中间件
在nestjs中的middle完全跟express的中间件一摸一样。不仅如此,我们还可以直接使用express中的中间件,比如在我的应用中需要处理core跨域:
import * as cors from 'cors';
async function bootstrap() {
onst app = await NestFactory.create(/* 创建app的业务逻辑*/)
app.use(cors({
origin:'http://localhost:8080',
credentials:true
}));
await app.listen(3000)
}
bootstrap();
在上述的代码中我们可以直接通过app.use来使用core这个express中的中间件。从而使得server端支持core跨域等。
初此之外,跟nestjs的中间件也完全保留了express中的中间件的特点:
- 在中间件中接受response和request作为参数,并且可以修改请求对象request和结果返回对象response
- 可以结束对于请求的处理,直接将请求的结果返回,也就是说可以在中间件中直接res.send等。
- 在该中间件处理完毕后,如果没有将请求结果返回,那么可以通过next方法,将中间件传递给下一个中间件处理。
在nestjs中,中间件跟express中完全一样,除了可以复用express中间件外,在nestjs中针对某一个特定的路由来使用中间件也十分的方便:
class ApplicationModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('cats');
}
}
上面就是对于特定的路由url为/cats的时候,使用LoggerMiddleware中间件。
3.4 、Guards守卫
Guards守卫,其作用就是决定一个请求是否应该被处理函数接受并处理,当然我们也可以在middleware中间件中来做请求的接受与否的处理,与middleware相比,Guards可以获得更加详细的关于请求的执行上下文信息。
通常Guards守卫层,位于middleware之后,请求正式被处理函数处理之前。
下面是一个Guards的例子:
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}
这里的context实现了一个ExecutionContext接口,该接口中具有丰富的执行上下文信息。
export interface ArgumentsHost {
getArgs<T extends Array<any> = any[]>(): T;
getArgByIndex<T = any>(index: number): T;
switchToRpc(): RpcArgumentsHost;
switchToHttp(): HttpArgumentsHost;
switchToWs(): WsArgumentsHost;
}
export interface ExecutionContext extends ArgumentsHost {
getClass<T = any>(): Type<T>;
getHandler(): Function;
}
除了ArgumentsHost中的信息外,ExecutionContext还包含了getClass用户获取对于某一个路由处理的,控制器。而getClass用于获取返回对于指定路由后台处理时的处理函数。
对于Guards处理函数,如果返回true,那么请求会被正常的处理,如果返回false那么请求会抛出异常。
3.5、interceptors拦截器
拦截器可以给每一个需要执行的函数绑定,拦截器将在该函数执行前或者执行后运行。可以转换函数执行后返回的结果等。
概括来说:
interceptors拦截器在函数执行前或者执行后可以运行,如果在执行后运行,可以拦截函数执行的返回结果,修改参数等。
再来举一个超时处理的例子:
@Injectable()
export class TimeoutInterceptor implements NestInterceptor{
intercept(
context:ExecutionContext,
call$:Observable<any>
):Observable<any>{
return call$.pipe(timeout(5000));
}
}
该拦截器可以定义在控制器上,可以处理超时请求。
4,原理部分
4.1 DI依赖注入与IoC控制反转
使用后端开发的都不陌生。
- DI IoC容器动态的将某个依赖关系注入到组件之中
- 控制反转IoC(Inversion of Control)是说创建对象的控制权进行转移,以前创建对象的主动权和创建时机是由自己把控的,而现在这种权力转移到第三方,比如转移交给了IoC容器,它就是一个专门用来创建对象的工厂
nest Injectable demo
type Constructor<T = any> = new (...args: any[]) => T;
const Injectable = (): ClassDecorator => target => {};
class OtherService {
a = 1;
}
@Injectable()
class TestService {
constructor(public readonly otherService: OtherService) {}
testMethod() {
console.log(this.otherService.a);
}
}
const Factory = <T>(target: Constructor<T>): T => {
// 获取所有注入的服务
const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
const args = providers.map((provider: Constructor) => new provider());
return new target(...args);
};
Factory(TestService).testMethod();
在nestjs中也参考了angular中的依赖注入的思想,也是用module、controller和service。
@Module({
imports:[otherModule],
providers:[SaveService],
controllers:[SaveController,SaveExtroController]
})
export class SaveModule {}
上面就是nestjs中如何定一个module,在imports属性中可以注入其他模块,在providers 注入相应的在控制器中需要用到的service,在控制器中注入需要的controller。
4.2,元数据、反射与装饰器
基本概念
- MetaData:也称元数据,元数据是用来描述数据的数据。可以借助仓库reflect-metadata
- Reflect:es6规范中,Reflect已存在,简单来说,这个API的作用就是可以实现对变量操作的函数化,也就是反射,具体可看阮一峰es6关于reflect的教程
- Decorator:装饰器,主要用来扩展类和类的方法,使其功能更强大。具体可看阮一峰es6关于decorator的教程。
装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数
nest controller和getter的实现
const METHOD_METADATA = 'method';
const PATH_METADATA = 'path';
const Controller = (path: string): ClassDecorator => {
return target => {
Reflect.defineMetadata(PATH_METADATA, path, target);
}
}
const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
return (target, key, descriptor) => {
Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
}
}
const Get = createMappingDecorator('GET');
const Post = createMappingDecorator('POST');
@Controller('/test')
class SomeClass {
@Get('/a')
someGetMethod() {
return 'hello world';
}
@Post('/b')
somePostMethod() {
return 'zhangjing';
}
}
function isFunction(arg: any): boolean {
return typeof arg === 'function';
}
function isConstructor(arg: string) {
return arg === 'constructor';
}
function mapRoute(instance) {
const prototype = Object.getPrototypeOf(instance);
// 筛选出类的 methodName
const methodsNames = Object.getOwnPropertyNames(prototype)
.filter(item => !isConstructor(item) && isFunction(prototype[item]))
return methodsNames.map(methodName => {
const fn = prototype[methodName];
// 取出定义的 metadata
const route = Reflect.getMetadata(PATH_METADATA, fn);
const method = Reflect.getMetadata(METHOD_METADATA, fn);
return {
route,
method,
fn,
methodName
}
})
}
console.log(Reflect.getMetadata(PATH_METADATA, SomeClass)); // '/test'
console.log(mapRoute(new SomeClass()));
5,部署
均采用的docker部署,包括:
- mysql
- node :后端nest
- monio
- nginx :前端代码vue3
部署配置见另一文章 docker
6,常见的数据传输方式:
- url param
import { Controller, Get, Param } from '@nestjs/common';
@Controller('params-parse')
export class ParamsParseController {
@Get(':id')
urlParma(@Param('id') id: string) {
return `传递的id:${id}`;
}
}
- query
import { Controller, Get, Param, Query } from '@nestjs/common';
@Controller('params-parse')
export class ParamsParseController {
@Get('query')
query(@Query('code') code: string) {
return `传递的code:${code}`;
}
}
- form-data
import {Controller,Post,Body,UploadedFiles,UseInterceptors} from '@nestjs/common';
import { CreateParamsParseDto } from './dto/create-params-parse.dto';
import { AnyFilesInterceptor } from '@nestjs/platform-express';
import { Express } from 'express';
@Controller('params-parse')
export class ParamsParseController {
@Post('file')
@UseInterceptors(AnyFilesInterceptor({ dest: 'uploads/' }))
file(
@Body() createParamsParseDto: CreateParamsParseDto,
@UploadedFiles() files: Array<Express.Multer.File>,
) {
return `传递的对象:${JSON.stringify(createParamsParseDto)}`;
}
}
- form-urlencoded
export class CreateParamsParseDto {
code: string;
}
import { Controller, Post, Body } from '@nestjs/common';
import { CreateParamsParseDto } from './dto/create-params-parse.dto';
@Controller('params-parse')
export class ParamsParseController {
@Post('urlencoded')
body(@Body() createParamsParseDto: CreateParamsParseDto) {
return `传递的对象:${JSON.stringify(createParamsParseDto)}`;
}
}
- json
import { Controller, Post, Body } from '@nestjs/common';
import { CreateParamsParseDto } from './dto/create-params-parse.dto';
@Controller('params-parse')
export class ParamsParseController {
@Post('json')
json(@Body() createParamsParseDto: CreateParamsParseDto) {
return `传递的对象:${JSON.stringify(createParamsParseDto)}`;
}
}
-- 推荐文章
欢迎关注我的前端自检清单,我和你一起成长