nest

117 阅读11分钟

快速掌握

1.搭建

npm install @nestjs/cli -g

nest new 项目名

需要时不时升级一下版本,就能保证它是最新的了:

npm update @nestjs/cli -g

2.生成单个的文件

nest generate controller/module/service xxx

他会自动更新到module的依赖里面。

3.生成资源

nest g 资源名

--flat和--no-flat是否生成对应的文件

--spec和--no-spec是否生成测试文件

--skip-import指定不在AppModule里面引用

4.构建项目

nest build

构建时,会在dist目录下生成编译后的代码

--wepback会进行打包,--tsc是默认的,它不打包。打包的话会打包成单模块,可以提升性能

--config(nest-cli.json的配置文件)

compilerOptions里的webpack:true相当于nest build --webpack,generateOptions里的spec和flat对应的就是nest g 对于flat和spec的设置

5.nest start启动服务

--watch或者-w 改动文件后自动重新build

--debug或者-d 调试服务

nest info查看项目信息

为什么要使用nest

首先我们先来看看开发Node应用的3个层次:

1.直接用http、https包的createServer api.

2.使用express、koa这几种处理请求的库.

3.使用nest、egg、midway这几类企业级的框架.

原生的用于简单场景还可以接受,但在大型开发中,使用createServer进行开发,效率十分低下。而像express这几种请求库并不能约束代码的书写,所以并不适合开发大型项目。大型项目一般使用企业级的框架进行开发,这些框架规定了代码的写法,同时很多功能都现成的解决方案的框架。在企业级的框架中,nest最受欢迎。

五种http数据传输方式

毕竟是要开发后端,那么就先了解前后端传输数据的几种方式吧。

1.url param

用来取服务端框架或者单页应用的路由

2.query

通过url中?后面的用&分割的字符串传递数据,但是一些非英文字符和一些特殊字符要经过编码(encodeURLComponent api/用queryStringify封装)

3.form-urlencoded

把query的数据放在body里面同时指定content-type是application/x-www-form-urlencoded,内容的处理和query一样。但是由于有encode的过程,所数据庞大效率就比较低下。因此就可以用下面的那一种。

4.form-data

数据依旧放在body里面,它通过-------+一串数字进行分割,不需要进行url encode,需要指定content type为multipart/form-data,适合用来传输文件。

5.json

如果只是单纯的只传入json格式的数据把content type设置为application/json

IOC

为什么需要IOC呢?说之前我们先来看看后端系统中对象与对象之间的关系。

后端需要接收请求并作出回应,实现对数据数据的增删改查等操作。那么首先就要先有能管理保存信息的一个地方,所以要先实例化出Config配置对象,保存数据(如用户的密码,帐号等),其次,我们需要能从存放数据的地方取出数据,于是接下来就要实例化DataSource数据库连接对象,接着取出数据后就需要实现对数据的增删改查,于是就需要实例化Repository对象,接下来就是一些业务逻辑的处理以及对请求的响应的,Controller对象接收请求后调用Service返回响应,故而接下来要先创建Service对象最后再创建Controller对象(这都还是列举了主要的对象,还有其他对象),这样一条从前端到后端数据所要走的路就搭建好啦。这个些对象的初始化顺序不能弄错,有的对象还要保证不能new多次,手动创建之后还要把他们组装起来,是不是就很繁琐。于是这时候IOC存在的意义就可想而知了。

IOC有一个放对象的容器当程序初始化时,就会扫描class上声明的依赖关系,然后把这些class都new出来放到容器里,创建对象的时候会把他们依赖的对象注入,实现手动到等待注入的转化。

在Controller接收请求并调用在Service实现的业务逻辑,通过Module模块import。

调试nest

image.png

点击创建launch.json文件

2.通过nest start --debug启动nest服务

3.删除原有配置,输入attach就会出现选项,点击Node.js:Attach

image.png 4.然后打断点,最后调接口

provider

nest实现了IOC容器,会从入口文件开始扫描,然后分析module间的关系,自动把provider注入到目标对象中。provider一般是用@Injectable修饰的class。

可以用构造器注入(@Injectable)也可以属性注入(@Inject)

@Controller
export class AppController{
provide:[AppService]
...
}
//完整写法
provide:[{
provide:AppService,
useClass:AppService
}]
//通过provide注入token,useClass注入对象的类
@Controller
export class AppController{
@Inject(AppService)
private readonly appService:AppService
...
}
//但是如果token是字符串,那么就要手动注入
@Controller
export class AppController{
constructor(@Inject('appService') private readonly appService:AppService){}
...
}

provide也可以直接指定一个值

provide:'person',
useValue:{
name:aaa,
age:20
}
```Controller
constructor(@Inject('person') private readonly person:{name:string,age:number}){}

provider的值还可以动态生成

{
provide:'person1',
useFactory(person:{name:string,age:number},appService:AppService){
return {
name:person.name,
desc:appService.getHello()
}
},
//useFactory是可以传递参数的,但是传递的参数需要注入
inject:['person',AppService]
}
//另外useFactory还支持异步,即async...await
//nest会等到异步的结果然后再注入

useExisting起别名

全局模块和生命周期

1.全局模块

全局模块解决了什么问题?我们设想一下,每个导出的provider都需要经过import才能使用,那么假如某个模块需要引入的provider很多呢,如果这时候有全局的模块就不需要挨个去引入。只需要把需要声明为全局模块的添加上@Globall,然后在需要注入的地方注入这个模块就可以直接使用了。

2.生命周期

nest启动的时候会递归的解析依赖然后注入他们,在这个过程中nast提供了一些生命周期的方法。

首先会递归的初始化模块,依次调用controller,provider的onModuleInit方法再调用module的onModuleInit方法。

初始化结束后,依次调用controller,provider的onApplicationBootstrap方法,然后再调用module的。

然后开始监听网络接口,之后nest就可以开始运行了。

销毁时,先依次调用controller,provider的onModuleDestry方法,然后再调用module的。

之后再依次调用controller,provider的beforeApplicationShutdown方法(该方法可以拿到系统的signal),再调用module的。

然后停止监听网络接口。

之后依次调用controller,provider的onApplicationShutdown方法,再调用module的。

最后停止进程

AOP框架

首先要知道,后端的框架都是MVC的架构,请求会先发送给Controller,由它去调度Module层的Service来完成业务逻辑,然后返回对应的View。

但是如果我们想在这整个过程的某一步做某些操作呢?最容易想到的就是直接在Controller层加入逻辑,但这样不美观。而nest就提供了AOP相当于在要进入某个进程前添加逻辑,这层逻辑既不会破坏内部代码逻辑的纯粹性,还可以复用以及动态增删。

而nest实现AOP的方式有5种:

1.中间件

由于nest的底层是Express,所以沿用了中间件,只不过做了细分(全局中间件和路由中间件)

2.Guard(路由守卫)

可在调用某个Controller之前判断权限。返回布尔值判断是否放行。Guard虽然可以抽离路由的访问控制逻辑,但不能对请求,响应做修改,得用Interceptor,也就是第五种.

3.pipe

用来对参数做检验和转换,pipe可以对某个参数生效,也可以对某个路由生效,还可以对每个路由都生效。

4.ExceptionFilter

用来对抛出的异常做处理,返回对应的响应。它也有全局和局部。

顺序

首先如果有相应的中间件,那么就会先执行中间件。到某个路由,首先调用的就是Guard判断是否有权限,有则调用Interceptor,对Controller前后扩展一些逻辑,在达到目标前Controller之前还会通过Pipe来进行检验和转换,抛出的异常都会被ExceptionFilter处理。

装饰器

1.通过@Controller声明controller,通过@injectable声明provider。

2.通过@Optional声明可选的依赖,以至于该依赖不存在,没有相应的provider也能正常构建这个对象。

3.@Global全局声明,可直接注入,不需要import。

4.@Catch捕获错误会捕获所有异常,如果是通过Exception Filter声明的就会调用filter进行处理,然后通过@UseFilter应用到handler上,里面的参数是@Catch下的类。interceptor,guard,pipe(可以单独用于某一个参数)类似。

5.@Query取url后的?后面的参数,@Param取路径中的参数,@Body取到请求中body中的参数,@Headers,@Ip,@Seeion,使用@Seession要安装express中间件,然后在main.ts里面引入启用。

app.use({
secret:'aaa',
cookie:{maxAge:10000}
})

之后的请求都会带上cookie,然后就可以在相应的handler里面使用sesssion来存储信息

6.@Req,@Res获取相应的对象。若此时用了@Res并且此时返回了响应内容,那么nest就不会再把handler返回的内容作为响应内容了,这是为了避免自己返回的nest返回的造成冲突,所以我们可以自己返回,或者通过passthrough:true告诉nest,即@res({passthrough:true})。

7.@Nest注入后nest也不会自己返回响应,当有两个handler处理同一个路由时,可以在第一个注入并调用nest,第一个handler就会把请求转发到第二个,让第二做,注意此时不管是第一个还是第二个依旧不会主动处理handler的返回值。

8.@HttpCode指定状态码;@Header修改响应头;@Redirect重定向路径;@Render指定渲染模板,使用时需要再main.ts指定静态资源(app.useStaticAssets)路径和模板路径(app.setBaseViewsDir)以及模板引擎(app.setViewEngine),然后在handler指定模板。

9.自定义装饰器

虽然nest内置了很多装饰器,但当这些装饰器不满足需求时,我们能不能自己开发呢?答案是当然可以

  • 我们之前都是这样使用SetMetadata的

@SetMetadata('aaa','admin')

然后在guard里面使用reflector来取metadata

this.reflector.get('aaa',context.getHandler())

  • 对其做一层封装

export const Aaa=(...args:string[])=>{ return SetMetadata('aaa',args) }

使用时

@Aaa('admin')

  • 如果装饰器太多了,还可以进行合并,写一个封装的函数即可
export function Bbb(path,role){
return applyDecorators(
Get(path)
Aaa(role)
UseGuard(AaaGuard)
)
}

@Get()
@SetMetadata('aaa','admin')
@UseGuard(AaaGuard)

@Get('Hellow')
@Aaa('admin')
@UseGuard(AaaGuard)

@Bbb('hellow','admin')

//以上三种是等效的
  • 自定义参数装饰器
export Ccc=createParamDecorator(
(data:string,ctx:ExecutionContext)=>{
return 'ccc'
}
)

//使用时
@Get('ccc')
getHello(@Ccc()c){
return c
}

//结果
ccc

//参数装饰器返回的值就是参数的值

Excutioncontext是当前线程的执行上下文,通过swichToHttp切换到http协议,可以取出request对象(通过getRequest)。

  • class装饰器
export Ddd=()=>Controller('ddd')

//使用
@Ddd()

切换不同执行上下文

为什么需要切换不同的执行上下文呢?因为nest支持的服务很多,而不同服务拿到的参数类型是不一样的,我们想实现在不同上下文中也能使用Filter,Guard,ExceptionFilter,为解决这个问题,我们就需要用到Argument和ExecutionContext。

我们先来看看它如何实现可以在不同上下文下使用filter。首先先建一个自定义的异常类(我们知道@Catch会捕获所有未捕获的异常,而给它传入ExceptionFilter就会把捕获到的错误交给filter处理,接下来的自定义异常类,可以实现自己定义错误类型)

export class AaaException{
construct(public aa:string,public bbb:string){}

}

接着在@Catch里面声明

@Catch(AaaException)
export class AaaFilter implements ExceptionFilter{
catch(exception:AaaException,host:ArgumentHost){}
}

最后启用它

@Get()
@UseFilter(AaaFilter)
getHello():string{
//抛出自定义异常
throw new AaaException('aaa','bbb')
}

可以看到上面的host是ArgumentHost类型,host身上有很多方法,其中就有可以拿到当前上下文的参数的getArgs方法,拿到了参数也就知道了参数类型,知道了该如何对其进行处理。

接下来看看guard

image.png

可以看到context是ExecutionContext类型,而ExecutionContext身上的方法和ArgumentHost一样就是多了getClass和getHandler,这两个方法分别调用COntroller的class和handler,这就实现了根据目标对象身上有没有某些装饰器而决定是否可以调用handler.

Module和provider的循环依赖如何解决

首先我们知道nest会从入口文件进行扫描,然后实例化相对应的依赖,假如Aaa和Bbb相互引用,即

@Module({
imports:[
BbbModule
]})
export class AaaModule

@Module({
imports:[
AaaModule
]})
export class BbbModule

那么假如先实例化AaaModule,此时发现Aaa和Bbb有依赖关系,而此时Bbb还在Aaa之后。还没有被实例化,于是Aaa中就引入了ubdefine,服务跑起来就会报错。

要解决那么就先独立创建这两个类,然后再相互引入。即用forwardRef

@Module({
imports:[
forwardRef(()=>BbbModule)
]})
export class AaaModule

@Module({
imports:[
forwarRef(()=>AaaModule)
]})
export class BbbModule

Service也是一样的,相互注入时,也需要用forwardRef的方式注入,但是此时必须要手动输入,即

@Injectable()
export class CccService{
constructor(@Inject(forwardRef(()=>DddService))
private dddService:DddService){}
}