Nest 是对标 Java 的 Spring 框架的后端框架,它有很多概念。
对于新手来说,上来就接触这些概念可能会有点懵。
这节集中来过一下(这节不用实践,理解概念就好):
通过不同的 url 来访问后端接口:
/user/create
/user/list
/book/create
/book/list
不同的 url 是不同的路由。
这些路由在 Controller 里声明:
比如声明一个 /user/create 的 post 接口、一个 /user/list 的 get 接口,只要在 class 上和方法上分别加上 @Controller、@Post、@Get 的装饰器就可以了:
@Controller('user')
export class UserController {
@Post('create')
create() {}
@Get('list')
list() {}
}controller 的方法叫做 handler,是处理路由的。
post 的请求体,get 的请求参数,都可以通过装饰器来取:
通过 @Param 取 url 中的参数,比如 /user/111 里的 111
通过 @Query 来取 url 中的 query 参数,比如 /user/xx?id=222 里的 222
通过 @Body 取 /book/create 的请求体内容:
请求体一般会传递 json,比如 { username: 'xxx', password: 'xxx' }
会通过 dto (data transfer object)来接收。
传递到 handler 的就已经是 dto 对象了。
也就是说 controller 是处理路由和解析请求参数的。
请求参数解析出来了,下一步就是做业务逻辑的处理了,这些东西不写在 controller 里,而是放在 service 里。
service 里做业务逻辑的具体实现,比如操作数据库等
同理,/book/list、/book/create 接口是在另一个 BookController 里:
它的业务逻辑实现也是在 BookService 里。
很明显,UserController 和 UserService 是一块的,BookController 和 BookService 是一块的。
所以,Nest 有了模块的划分,每个模块里都包含 controller 和 service:
通过 @Module 声明模块,它包含 controllers 和 providers。
为啥不是 services 而是 providers 呢?
因为 Nest 实现了一套依赖注入机制,叫做 IoC(Inverse of Control 反转控制)。
简单说就是你只需要声明依赖了啥就行,不需要手动去 new 依赖,Nest 的 IoC 容器会自动给你创建并注入依赖。
比如这个 UserController 依赖了 JwtService,那只需要通过 @Inject 声明依赖,然后就可以在方法里用了:
运行的时候会自动查找这个 JwtService 的实例来注入。
在 @Module 里的 providers 数组里,就是声明要往 IoC 容器里提供的对象,所以这里叫做 providers。
provider 有很多种写法:
默认的 XxxService 只是一种简化的写法。
还可以直接 useValue 创建,通过 useFactory 创建等。
刚才提到了 IoC 会自动从容器中查找实例来注入,注入的方式有两种:
属性注入和构造器注入。
这种写在构造器里的依赖,就是构造器注入。
@Inject 写在属性上的依赖,就是属性注入。
效果是一样的,只是注入的时机不同。
每个模块都会包含 controller、service、module、dto、entities 这些东西:
controller 是处理路由,解析请求参数的。
service 是处理业务逻辑的,比如操作数据库。
dto 是封装请求参数的。
entities 是封装对应数据库表的实体的。
nest 应用跑起来后,会从 AppModule 开始解析,初始化 IoC 容器,加载所有的 service 到容器里,然后解析 controller 里的路由,接下来就可以接收请求了。
其实这种架构叫做 MVC 模式,也就是 model、view、controller。
controller 接收请求参数,交给 model 处理(model 就是处理 service 业务逻辑,处理 repository 数据库访问),然后返回 view,也就是响应。
应用中会有很多 controller、service,那如果是跨多个 controller 的逻辑呢?
这种在 Nest 提供了 AOP (Aspect Oriented Programming 面向切面编程)的机制
具体来说,有 Middleware、Guard、Interceptor、Pipe、Exception Filter 这五种。
它们都是在目标 controller 的 handler 前后,额外加一段逻辑的。
比如你可以通过 interceptor 实现请求到响应的时间的记录:
这种逻辑适合放在 controller 里么?
不适合,这种通用逻辑应该通过 interceptor 等方式抽离出来,然后需要用的时候在 controller 上声明一下:
这些就是 Nest 的核心概念了。
此外,创建 Nest 项目自然也是有 cli 工具的。
比如创建新项目:
创建项目里的某个新模块:
都可以通过 @nestjs/cli 来做。
所以后面的学习顺序是先学会 cli 的使用,然后学习 Nest 的核心概念,之后学下涉及到的数据库、orm 框架等,最后来做项目实战。
这节讲了很多 Nest 里的概念。
比如 controller、handler、service、dto、module、entity、ioc、aop、nest cli 等。
controller:控制器,用于处理路由,解析请求参数
handler:控制器里处理路由的方法
service:实现业务逻辑的地方,比如操作数据库等
dto:data transfer object,数据传输对象,用于封装请求体里数据的对象
module:模块,包含 controller、service 等,比如用户模块、书籍模块
entity:对应数据库表的实体
ioc:Inverse of Control,反转控制或者叫依赖注入,只要声明依赖,运行时 Nest 会自动注入依赖的实例
aop:Aspect Oriented Programming 面向切面编程,在多个请求响应流程中可以复用的逻辑,比如日志记录等,具体包含 middleware、interceptor、guard、exception filter、pipe
nest cli:创建项目、创建模块、创建 controller、创建 service 等都可以用这个 cli 工具来做
现在只要能大概理解概念就行,后面会深入学习这些。
项目开发离不开工程化的部分,比如创建项目、编译构建、开发时 watch 文件变动自动构建等。
Nest 项目自然也是这样,所以它在 @nestjs/cli 这个包里提供了 nest 命令。
可以直接 npx 执行,npm 会把它下载下来然后执行:
npx @nestjs/cli new 项目名也可以安装到全局,然后执行,更推荐这种:
npm install -g @nestjs/cli
nest new 项目名不过后者要时不时升级下版本,不然可能用它创建的项目版本不是最新的:
npm update -g @nestjs/cli那 nest 都提供了啥命令呢?
nest -h 看看:
有创建新项目的 nest new,有生成某些代码的 nest generate,还有编译构建的 nest build,开发模式的 nest start 等。
分别看一下:
nest new 用过,就是创建一个新的 nest 项目的。
它有这么几个选项:
--skip-git 和 --skip-install 很容易理解,就是跳过 git 的初始化,跳过 npm install。
--package-manager 是指定包管理器的,之前创建项目的时候会让选择:
指定之后,就跳过包管理器选择这步了:
--language 可以指定 typescript 和 javascript,一般都选择 ts,用默认的就好。
--strict 是指定 ts 的编译选项是否开启严格模式的,也就是这么 5 个选项:
默认是 false,也可以指定为 true:
这个之后需要的话再改就行。
nest 命令除了可以生成整个项目外,还可以生成一些别的代码,比如 controller、service、module 等。
比如生成 module:
nest generate module aaa它会生成 module 的代码:
还会自动在 AppModule 里引入:
当然你也可以生成 controller、service 等代码:
nest generate controller aaa同样,它也会更新到 module 的依赖里去。
生成 service 也是一样:
当然,如果是要完整生成一个模块的代码,不需要一个个生成,可以用
nest generate resource xxx它会让你选择是哪种代码,因为 nest 支持 http、websocket、graphql、tcp 等,这里选择 http 的 REST 风格 api:
然后会让你选择是否生成 CRUD 代码:
选择是。
然后就会生成整个模块的 CRUD + REST api 的代码:
当然,它同样会自动在 AppModule 引入:
这就是 nest generate,可以快速生成各种代码:
这些代码模版的集合是在 @nestjs/schematics 这个包里定义的。
nest new 创建项目的时候有个 --collection 选项,就是配置这个的。
不过一般不需要配置。
你可以在 @nestjs/schematics 里看到这些代码模版的定义:
它的实现原理很简单,就是模版引擎填充变量,打印成代码:
nest generate 也有不少选项:
--flat 和 --no-flat 是指定是否生成对应目录的:
--spec 和 --no-spec 是指定是否生成测试文件:
--skip-import 是指定不在 AppModule 里引入:
也就是不生成这部分代码:
至于 --project,这是指定生成代码在哪个子项目的,等之后用到 monorepo 项目的时候再说。
这就是 nest cli 提供的快速生成各种代码的能力,是不是还挺方便的?
然后就是 nest build 了,它是用来构建项目的:
执行 nest build,会在 dist 目录下生成编译后的代码。
同样,它也有一些选项:
--webpack 和 --tsc 是指定用什么编译,默认是 tsc 编译,也可以切换成 webpack。
这是 tsc 的编译产物:
这是 webpack 的编译产物:
tsc 不做打包、webpack 会做打包,两种方式都可以。
node 模块本来就不需要打包,但是打包成单模块能提升加载的性能。
--watch 是监听文件变动,自动 build 的。
但是 --watch 默认只是监听 ts、js 文件,加上 --watchAssets 会连别的文件一同监听变化,并输出到 dist 目录,比如 md、yml 等文件。
--path 是指定 tsc 配置文件的路径的。
那 --config 是指定什么配置文件呢?
是 nest cli 的配置文件。
刚刚说的那些选项都可以在 nest-cli.json 里配置:
比如 compilerOptions 里设置 webpack 为 true 就相当于 nest build --webpack,一样的效果:
webpack 设置为 false 就是用 tsc 了。
deleteOutDir 设置为 true,每次 build 都会都清空 dist 目录。
而 assets 是指定 nest build 的时候,把那些非 js、ts 文件也复制到 dist 目录下。
可以通过 include、exclude 来精确匹配,并且可以单独指定是否 watchAssets。
不过只支持 src 下文件的复制,如果是非 src 下的,可以自己写脚本复制:
然后是 generateOptions,这些就和 nest generate 时的 --no-spec、--no-flat 一样的效果。
比如我把 flat 设置为 false、spec 设置为 false,那再 generate 代码时就是这样的:
生成了一层目录,并且没有生成测试文件。
sourceRoot 是指定源码目录。
entryFile 是指定入口文件的名字,默认是 main。
而 $schema 是指定 nest-cli.json 的 schema,也就是可以有哪些属性的:
https://json.schemastore.org/nest-cli
这是一种 json schema 的规范,还是挺容易看懂的:
如果想全面了解 nest-cli.json 都有啥属性,可以看看这个 schema 定义。
最后,再来看下 nest start 命令:
可以看到每次重新 build 了,并且用 node 把 main.js 跑了起来。
它有这些选项:
--watch 是最常用的选项了,也就是改动文件之后自动重新 build:
--debug 是启动调试的 websocket 服务,用来 debug。
--exec 可以指定用什么来跑,默认是用 node 跑,你也可以切换别的 runtime。
其余选项和 nest build 一样,就不复述了。
最后还有个 nest info 命令,这个就是查看项目信息的,包括系统信息、 node、npm 和依赖版本:
nest 在 @nestjs/cli 包里提供了 nest 命令,它可以用来做很多事情:
也就是这些子命令:
并且,很多选项都可以在 nest-cli.json 里配置,比如 generateOptions、compilerOptions 等。
学会用 nest cli,是学好 nest 很重要的一步。
对于前端来说,后端主要是提供 http 接口来传输数据,而这种数据传输的方式主要有 5 种:
分别来看一下:
可以把参数写在 url 中,比如:
http://guang.zxg/person/1111这里的 1111 就是路径中的参数(url param),服务端框架或者单页应用的路由都支持从 url 中取出参数。
通过 url 中 ? 后面的用 & 分隔的字符串传递数据。比如:
http://guang.zxg/person?name=guang&age=20这里的 name 和 age 就是 query 传递的数据。
其中非英文的字符和一些特殊字符要经过编码,可以使用 encodeURIComponent 的 api 来编码:
const query = "?name=" + encodeURIComponent('光') + "&age=20"
// ?name=%E5%85%89&age=20或者使用封装了一层的 query-string 库来处理。
const queryString = require('query-string');
queryString.stringify({
name: '光',
age: 20
});
// ?name=%E5%85%89&age=20直接用 form 表单提交数据就是这种,它和 query 字符串的方式的区别只是放在了 body 里,然后指定下 content-type 是 application/x-www-form-urlencoded。
因为内容也是 query 字符串,所以也要用 encodeURIComponent 的 api 或者 query-string 库处理下。
这种格式也很容易理解,get 是把数据拼成 query 字符串放在 url 后面,于是表单的 post 提交方式的时候就直接用相同的方式把数据放在了 body 里。
通过 & 分隔的 form-urlencoded 的方式需要对内容做 url encode,如果传递大量的数据,比如上传文件的时候就不是很合适了,因为文件 encode 一遍的话太慢了,这时候就可以用 form-data。
form data 不再是通过 & 分隔数据,而是用 --------- + 一串数字做为 boundary 分隔符。因为不是 url 的方式了,自然也不用再做 url encode。
form-data 需要指定 content type 为 multipart/form-data,然后指定 boundary 也就是分割线。
body 里面就是用 boundary 分隔符分割的内容。
很明显,这种方式适合传输文件,而且可以传输多个文件。
但是毕竟多了一些只是用来分隔的 boundary,所以请求体会增大。
form-urlencoded 需要对内容做 url encode,而 form data 则需要加很长的 boundary,两种方式都有一些缺点。如果只是传输 json 数据的话,不需要用这两种。
可以直接指定content type 为 application/json 就行:
平时传输 json 数据基本用的是这种。
那这 5 种接口怎么用 Nest 实现呢?
分别看一下:
首先要把 Nest 服务端跑起来,并且支持 api 接口、静态页面。
Nest 创建一个 crud 服务是非常快的,只需要这么几步:
nest new xxxnest g resource personnest start --watch这样一个有 person 的 crud 接口的服务就跑起来了。
服务跑起来以后是这样的
打印出了有哪些接口可以用,可以在浏览器测试下:
api 接口跑通了,再支持下静态资源的访问:
main.ts 是负责启动 Nest 的 ioc 容器的,调用下 useStaticAssets 来支持静态资源的请求:
import { NestExpressApplication } from '@nestjs/platform-express';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.useStaticAssets('public', { prefix: '/static'});
await app.listen(3000);
}
bootstrap();(注意要给 create 方法传入 NestExpressApplication 的泛型参数才有 useStaticAssets这些方法)
指定 prefix 为 static,然后在静态文件目录 public 下添加一个 html:
<html>
<body>hello</body>
</html>重启服务,然后浏览器访问下试试:
api 接口和静态资源的访问都支持了,接下来就分别实现下 5 种前后端 http 数据传输的方式吧。
url param 是 url 中的参数,Nest 里通过 :参数名 的方式来声明(比如下面的 :id),然后通过 @Param(参数名) 的装饰器取出来注入到 controller:
@Controller('api/person')
export class PersonController {
@Get(':id')
urlParam(@Param('id') id: string) {
return `received: id=${id}`;
}
}@Controller('api/person') 的路由和 @Get(':id') 的路由会拼到一起,也就是只有 /api/person/xxx 的 get 请求才会走到这个方法。
前端代码就是一个 get 方法,参数放在 url 里:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>
<body>
<script>
async function urlParam() {
const res = await axios.get('/api/person/1');
console.log(res);
}
urlParam();
</script>
</body>启动服务,在浏览器访问下:
控制台打印了服务端返回的消息,证明服务端拿到了通过 url param 传递的数据。
通过 url 传递数据的方式除了 url param 还有 query:
query 是 url 中 ? 后的字符串,需要做 url encode。
在 Nest 里,通过 @Query 装饰器来取:
@Controller('api/person')
export class PersonController {
@Get('find')
query(@Query('name') name: string, @Query('age') age: number) {
return `received: name=${name},age=${age}`;
}
}注意,这个 find 的路由要放到 :id 的路由前面,因为 Nest 是从上往下匹配的,如果放在后面,那就匹配到 :id 的路由了。
前端代码同样是通过 axios 发送一个 get 请求:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>
<body>
<script>
async function query() {
const res = await axios.get('/api/person/find', {
params: {
name: '光',
age: 20
}
});
console.log(res);
}
query();
</script>
</body>
</html>参数通过 params 指定,axios 会做 url encode,不需要自己做。
然后测试下:
服务端成功接受了通过 query 传递的数据。
上面两种(url param、query)是通过 url 传递数据的方式,下面 3 种是通过 body 传递数据。
form urlencoded 是通过 body 传输数据,其实是把 query 字符串放在了 body 里,所以需要做 url encode:
用 Nest 接收的话,使用 @Body 装饰器,Nest 会解析请求体,然后注入到 dto 中。
dto 是 data transfer object,就是用于封装传输的数据的对象:
export class CreatePersonDto {
name: string;
age: number;
}import { CreatePersonDto } from './dto/create-person.dto';
@Controller('api/person')
export class PersonController {
@Post()
body(@Body() createPersonDto: CreatePersonDto) {
return `received: ${JSON.stringify(createPersonDto)}`
}
}前端代码使用 post 方式请求,指定 content type 为 application/x-www-form-urlencoded,用 qs 做下 url encode:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
<script src="https://unpkg.com/qs@6.10.2/dist/qs.js"></script>
</head>
<body>
<script>
async function formUrlEncoded() {
const res = await axios.post('/api/person', Qs.stringify({
name: '光',
age: 20
}), {
headers: { 'content-type': 'application/x-www-form-urlencoded' }
});
console.log(res);
}
formUrlEncoded();
</script>
</body>
</html>测试下:
服务端成功的接收到了数据。
其实比起 form urlencoded,使用 json 来传输更常用一些:
json 需要指定 content-type 为 application/json,内容会以 JSON 的方式传输:
后端代码同样使用 @Body 来接收,不需要做啥变动。form urlencoded 和 json 都是从 body 取值,Nest 内部会根据 content type 做区分,使用不同的解析方式。
@Controller('api/person')
export class PersonController {
@Post()
body(@Body() createPersonDto: CreatePersonDto) {
return `received: ${JSON.stringify(createPersonDto)}`
}
}前端代码使用 axios 发送 post 请求,默认传输 json 就会指定 content type 为 application/json,不需要手动指定:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>
<body>
<script>
async function json() {
const res = await axios.post('/api/person', {
name: '光',
age: 20
});
console.log(res);
}
json();
</script>
</body>
</html>测试下:
服务端成功接收到了通过 json 传递的数据!
json 和 form urlencoded 都不适合传递文件,想传输文件要用 form data:
form data 是用 -------- 作为 boundary 分隔传输的内容的:
Nest 解析 form data 使用 AnyFilesInterceptor 拦截器,用 @UseInterceptors 装饰器启用,文件内容通过 @UploadedFiles 来取,非文件的内容同样是通过 @Body 来取。
import { Body, Controller, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
import { AnyFilesInterceptor } from '@nestjs/platform-express';
import { CreatePersonDto } from './dto/create-person.dto';
@Controller('api/person')
export class PersonController {
@Post('file')
@UseInterceptors(AnyFilesInterceptor({
dest: 'uploads/'
}))
body2(@Body() createPersonDto: CreatePersonDto, @UploadedFiles() files: Array<Express.Multer.File>) {
console.log(files);
return `received: ${JSON.stringify(createPersonDto)}`
}
}这一步需要
npm i -D @types/multer引入相关类型声明。
前端代码使用 axios 发送 post 请求,指定 content type 为 multipart/form-data:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>
<body>
<input id="fileInput" type="file" multiple/>
<script>
const fileInput = document.querySelector('#fileInput');
async function formData() {
const data = new FormData();
data.set('name','光');
data.set('age', 20);
data.set('file1', fileInput.files[0]);
data.set('file2', fileInput.files[1]);
const res = await axios.post('/api/person/file', data, {
headers: { 'content-type': 'multipart/form-data' }
});
console.log(res);
}
fileInput.onchange = formData;
</script>
</body>
</html>file input 指定 multiple 可以选择多个文件。
测试下:
服务端接收到了 name 和 age:
去服务器控制台看下:
可以看到,服务器成功的接收到了上传的文件。
用 axios 发送请求,使用 Nest 起后端服务,实现了 5 种 http/https 的数据传输方式:
其中前两种是 url 中的:
后三种是 body 中的:
application/x-www-form-urlencoded,并且对数据用 qs 或者 query-string 库做 url encodemultipart/form-data,并且用 FormData 对象来封装传输的内容。这 5 种 http 的传输数据的方式覆盖了绝大多数开发场景,如果你想进阶全栈,理解这 5 种接口是首先要做到的。
后端系统中,会有很多对象:
此外,还有数据库链接对象 DataSource,配置对象 Config 等等。
这些对象有着错综复杂的关系:
Controller 依赖了 Service 实现业务逻辑,Service 依赖了 Repository 来做增删改查,Repository 依赖 DataSource 来建立连接,DataSource 又需要从 Config 对象拿到用户名密码等信息。
这就导致了创建这些对象是很复杂的,你要理清它们之间的依赖关系,哪个先创建哪个后创建。
比如这样:
const config = new Config({ username: 'xxx', password: 'xxx'});
const dataSource = new DataSource(config);
const repository = new Repository(dataSource);
const service = new Service(repository);
const controller = new Controller(service);要经过一系列的初始化之后才可以使用 Controller 对象。
而且像 config、dataSource、repository、service、controller 等这些对象不需要每次都 new 一个新的,一直用一个就可以,也就是保持单例。
在应用初始化的时候,需要理清依赖的先后关系,创建一大堆对象组合起来,还要保证不要多次 new,是不是很麻烦?
解决这个痛点的方式就是 IoC(Inverse of Control)。
java 的 Spring 就实现了 IoC,Nest 也同样实现了。
那什么是 IoC 呢?
之前手动创建和组装对象不是很麻烦么,我能不能在 class 上声明依赖了啥,然后让工具去分析我声明的依赖关系,根据先后顺序自动把对象创建好了并组装起来呢?
比如这样声明 AppController 依赖了这两个 Service,然后让工具分析依赖自动帮我创建好这三个对象并设置依赖关系。
这就是 IoC 的实现思路。
它有一个放对象的容器,程序初始化的时候会扫描 class 上声明的依赖关系,然后把这些 class 都给 new 一个实例放到容器里。
创建对象的时候,还会把它们依赖的对象注入进去。
这样不就完成了自动的对象创建和组装么?
这种依赖注入的方式叫做 Dependency Injection,简称 DI。
而这种方案为什么叫 IoC 也很容易理解了,本来是手动 new 依赖对象,然后组装起来,现在是声明依赖了啥,等待被注入。
从主动创建依赖到被动等待依赖注入,这就是 Inverse of Control,反转控制。
在 class 上声明依赖的方式,大家都选择了装饰器的方式(在 java 里这种语法叫做注解)。
比如上面就是声明这个 class 要放到 IOC 容器里,然后它的依赖是啥。
这样 IOC 容器扫描到它就知道怎么创建它的对象了。
知道了 IOC 是啥,下面来看看真实的 Nest 项目里是怎么用 IoC 的:
npx nest new nest-ioc执行上面的命令,它会创建一个 nest 项目:
选择一个 npm 包管理工具,然后 nest cli 会自动创建项目结构并安装依赖:
然后进入这个目录,执行 npm run start,把服务跑起来:
浏览器访问 http://localhost:3000 就可以看到 nest 服务返回的 hello world:
看看代码里它是怎么创建对象的:
它有一个 AppService 声明了 @Injectable,代表这个 class 可注入,那么 nest 就会把它的对象放到 IOC 容器里。
AppController 声明了 @Controller,代表这个 class 可以被注入,nest 也会把它放到 IoC 容器里。
AppController 的构造器参数依赖了 AppService。
或者这样通过属性的方式声明依赖:
前者是构造器注入,后者是属性注入,两种都可以。
为什么 Controller 是单独的装饰器呢?
因为 Service 是可以被注入也是可以注入到别的对象的,所以用 @Injectable 声明。
而 Controller 只需要被注入,所以 nest 单独给它加了 @Controller 的装饰器。
然后在 AppModule 里引入:
通过 @Module 声明模块,其中 controllers 是控制器,只能被注入。
providers 里可以被注入,也可以注入别的对象,比如这里的 AppService。
然后在入口模块里跑起来:
那么 nest 就会从 AppModule 开始解析 class 上通过装饰器声明的依赖信息,自动创建和组装对象。
所以 AppController 只是声明了对 AppService 的依赖,就可以调用它的方法了:
nest 在背后自动做了对象创建和依赖注入的工作。
nest 还加了模块机制,可以把不同业务的 controller、service 等放到不同模块里。
nest g module other会生成如下代码:
用 nest cli 的 generate 命令生成一个模块。
会在 AppModule 里自动 imports 这个模块:
当 import 别的模块后,那个模块 exports 的 provider 就可以在当前模块注入了。
比如再生成 OtherService:
nest g service other会生成 Service 的代码:
并自动添加到 OtherModule 的 providers 中:
改下 OtherService,添加一个方法:
然后在 OtherModule 里 exports:
那当 AppModule 引用了 OtherModule 之后,就可以注入它 exports 的 OtherService 了。
在 AppService 里注入下:
import { OtherService } from './other/other.service';
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
@Inject(OtherService)
private otherService:OtherService;
getHello(): string {
return 'Hello World!' + this.otherService.xxx();
}
}
把服务跑起来:
npm run start:dev浏览器访问下,可以看到 AppModule 的 AppService 调用 OtherModule 的 OtherService 成功了:
这就是 Nest 的 IoC 机制。
后端系统有很多的对象,这些对象之间的关系错综复杂,如果手动创建并组装对象比较麻烦,所以后端框架一般都提供了 IoC 机制。
IoC 机制是在 class 上标识哪些是可以被注入的,它的依赖是什么,然后从入口开始扫描这些对象和依赖,自动创建和组装对象。
Nest 里通过 @Controller 声明可以被注入的 controller,通过 @Injectable 声明可以被注入也可以注入别的对象的 provider,然后在 @Module 声明的模块里引入。
并且 Nest 还提供了 Module 和 Module 之间的 import,可以引入别的模块的 provider 来注入。
虽然 Nest 这套实现了 IoC 的模块机制看起来繁琐,但是却解决了后端系统的对象依赖关系错综复杂的痛点问题。
Nest 实现了 IoC 容器,会从入口模块开始扫描,分析 Module 之间的引用关系,对象之间的依赖关系,自动把 provider 注入到目标对象。
而这个 provider 也有好几种,这节就来看一下。
创建个 nest 项目:
nest new custom-provider可以看到 AppService 是被 @Injectable 修饰的 class:
在 Module 的 providers 里声明:
这就是 provider。
其实这是一种简写,完整的写法是这样的:
{
provide: AppService,
useClass: AppService
}通过 provide 指定 token,通过 useClass 指定对象的类,Nest 会自动对它做实例化后用来注入。
在 AppController 的构造器里参数里声明了 AppService 的依赖,就会自动注入:
如果不想用构造器注入,也可以属性注入:
@Inject(AppService)
private readonly appService: AppService;通过 @Inject 指定注入的 provider 的 token 即可。
有的同学说,在构造器参数里指定 AppService 的依赖的时候也没指定 token 啊?
那是因为 AppService 这个 class 本身就是 token。
当然,这个 token 也可以是字符串:
{
provide: 'app_service',
useClass: AppService
}如果 token 是字符串的话,注入的时候就要用 @Inject 手动指定注入对象的 token 了:
@Inject('app_service') private readonly appService: AppService调试下:
点击调试面板的 create launch.json file,创建调试配置文件:
添加这样一个调试配置:
{
"type": "node",
"request": "launch",
"name": "debug nest",
"runtimeExecutable": "npm",
"args": [
"run",
"start:dev",
],
"skipFiles": [
"<node_internals>/**"
],
"console": "integratedTerminal",
}在 getHello 方法打个断点,点击调试启动:
浏览器访问 http://localhost:3000 ,代码会在断点处断住。
可以看到,这时候 appService 就有值了。
也就是说,用字符串或者 class 做 token 的 provider,都可以正确被注入到目标对象。
相比之下,用 class 做 token 可以省去 @Inject,比较简便。
除了指定 class 外,还可以直接指定一个值,让 IoC 容器来注入。
{
provide: 'person',
useValue: {
name: 'aaa',
age: 20
}
}使用 provide 指定 token,使用 useValue 指定值。
然后在对象里注入它:
@Inject('person') private readonly person: {name: string, age: number}调试一下可以看到,确实是注入了:
provider 的值可能是动态产生的,Nest 也同样支持:
{
provide: 'person2',
useFactory() {
return {
name: 'bbb',
desc: 'cccc'
}
}
}可以使用 useFactory 来动态创建一个对象。
在对象里注入:
@Inject('person2') private readonly person2: {name: string, desc: string}类型是 {name: string, desc: string} 。
调试下,也是可以拿到创建出的对象的:
这个 useFactory 支持通过参数注入别的 provider:
{
provide: 'person3',
useFactory(person: { name: string }, appService: AppService) {
return {
name: person.name,
desc: appService.getHello()
}
},
inject: ['person', AppService]
}通过 inject 声明了两个 token,一个是字符串 token 的 person,一个是 class token 的 AppService。
也就是注入这两个 provider:
在 return 那里打个断点。
可以看到,在调用 useFactory 方法的时候,Nest 就会注入这两个对象:
useFactory 支持异步:
{
provide: 'person5',
async useFactory() {
await new Promise((resolve) => {
setTimeout(resolve, 3000);
});
return {
name: 'bbb',
desc: 'cccc'
}
},
},Nest 会等拿到异步方法的结果之后再注入:
这样就可以更灵活的创建注入对象。
此外,provider 还可以通过 useExisting 来指定别名:
{
provide: 'person4',
useExisting: 'person2'
}这里就是给 person2 的 token 的 provider 起个新的 token 叫做 person4。
然后就可以用新 token 来注入了:
这些自定义 provider 的方式里,最常用的是 useClass,不过一般会用简写,也就是直接指定 class。
useClass 的方式由 IoC 容器负责实例化,也可以用 useValue、useFactory 直接指定对象。
useExisting 只是用来起别名的,有的场景下会用到。
比如 @nestjs/typeorm 里就用到了 useValue、useFactory、useExisting:
它用 useValue 来注入一段字符串:
用 useFactory 根据传入的 options 动态创建数据库连接对象:
用 useExisting 给 DataSource 起了一个 Connection 的别名:
这里是一个版本用了 Connection,一个版本用了 DataSource,通过 useExisting 起别名就可以兼容两者。
此外,如果觉得构造器注入写起来不方便,可以使用属性注入,效果一样:
一般情况下,provider 是通过 @Injectable 声明,然后在 @Module 的 providers 数组里注册的 class。
默认的 token 就是 class,这样不用使用 @Inject 来指定注入的 token。
但也可以用字符串类型的 token,不过注入的时候要用 @Inject 单独指定。
除了可以用 useClass 指定注入的 class,还可以用 useValue 直接指定注入的对象。
如果想动态生成对象,可以使用 useFactory,它的参数也注入 IOC 容器中的对象,然后动态返回 provider 的对象。
如果想起别名,可以用 useExisting 给已有的 token,指定一个新 token。
灵活运用这些 provider 类型,就可以利用 Nest 的 IOC 容器注入任何对象。
Nest 实现了一套模块系统,模块可以通过 imports 声明对其他模块的引用。
那 Module 和 Module 如果相互引用、形成循环依赖了怎么办?
这节就来学习下循环依赖的处理方式。
执行
nest new module-test -p npm创建一个 nest 项目。
然后执行
nest g module aaa
nest g module bbb创建两个 Module。
然后这两个 Module 相互引用。
这时候你执行
nest start -w把服务跑起来,会报这样的错误:
意思是在解析 BbbModule 的时候,它的第一个 imports 是 undefined。
这有两个原因,一个是这个值本来就是 undefined,第二个就是形成了循环依赖。
因为 Nest 创建 Module 的时候会递归创建它的依赖,而它的依赖又依赖了这个 Module,所以没法创建成功,拿到的就是 undefined。
那怎么办呢?
其实可以先单独创建这两个 Module,然后再让两者关联起来。
也就是用 forwardRef 的方式,两边都在 imports 里用 forwardRef 包裹:
import { Module, forwardRef } from '@nestjs/common';
import { BbbModule } from './bbb/bbb.module';
@Module({
imports: [forwardRef(() => BbbModule)],
})
export class AaaModule {}import { Module, forwardRef } from '@nestjs/common';
import { AaaModule } from './aaa/aaa.module';
@Module({
imports: [forwardRef(() => AaaModule)],
})
export class BbbModule {}因为用了 nest start --watch 的方式启动的,nest 会自动重启,这时候就没有错误了:
nest 会单独创建两个 Module,之后再把 Module 的引用转发过去,也就是 forwardRef 的含义。
除了 Module 和 Module 之间会循环依赖以外,provider 之间也会。
比如 Service 里可以注入别的 Service,自身也可以用来注入。
所以也会有循环引用。
来测试下:
nest g service ccc --no-spec --flat
nest g service ddd --no-spec --flat分别创建 ccc 和 ddd 两个 service,--no-spec 是不生成测试文件,--flat 是平铺。
就会创建这两个 service,并在 AppModule 引入了:
然后让两者相互注入:
import { Injectable } from '@nestjs/common';
import { CccService } from './ccc.service';
@Injectable()
export class DddService {
constructor(private cccService: CccService) {}
ddd() {
return this.cccService.ccc() + 'ddd';
}
}import { Injectable } from '@nestjs/common';
import { DddService } from './ddd.service';
@Injectable()
export class CccService {
constructor(private dddService: DddService) {}
ccc() {
return 'ccc';
}
eee() {
return this.dddService.ddd() + 'eee';
}
}两个 service 分别依赖了对方的方法。
在 AppService 里调用下:
import { Injectable } from '@nestjs/common';
import { CccService } from './ccc.service';
import { DddService } from './ddd.service';
@Injectable()
export class AppService {
constructor(private cccService: CccService, private dddService: DddService){}
getHello(): string {
return this.dddService.ddd() + this.cccService.eee();
}
}这时候 nest start --watch 会报错:
说是没法解析 DddService 的依赖,也是因为循环依赖导致的。
这时候也是通过 forwardRef 解决:
这时候就不能用默认的注入方式了,通过 @Inject 手动指定注入的 token,这里是 forwardRef 的方式注入,两个 service 都要改:
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { CccService } from './ccc.service';
@Injectable()
export class DddService {
constructor(@Inject(forwardRef(() => CccService)) private cccService: CccService) {}
ddd() {
return this.cccService.ccc() + 'ddd';
}
}import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { DddService } from './ddd.service';
@Injectable()
export class CccService {
constructor(@Inject(forwardRef(() => DddService)) private dddService: DddService) {}
ccc() {
return 'ccc';
}
eee() {
return this.dddService.ddd() + 'eee';
}
}这样报错就没了:
浏览器访问下:
两个 service 的相互调用也成功了。
这样就解决了循环依赖的问题。
Module 之间可以相互 imports,Provider 之间可以相互注入,这两者都会形成循环依赖。
解决方式就是两边都用 forwardRef 来包裹下。
它的原理就是 nest 会先创建 Module、Provider,之后再把引用转发到对方,也就是 forward ref。
前面讲了 IOC、AOP、全局模块、动态模块、自定义 provider、生命周期等概念,比较零散,这节画图来整体串一串。
首先,provider 是可以注入的对象,它们都有 token,比如 @Injectable 装饰器声明的 class
token 可以是 class 也可以是 string:
provider 可以是 useClass 指定 class,也可以 useValue 指定值,或者 useFactory 动态创建。
provider 之间可以相互注入,还可以注入到 controller 里。
provider、controller 放在一个个 Module 里:
module 里 exports 的 provider 在模块被 imports 之后就可以用于别的模块的注入了。
或者可以通过 @Global 把这个模块声明为全局的,那样 Module 内的 provider 就可以在各处注入了。
Provider 可以通过 useFactory 动态创建,Module 也是,可以通过 register、forRoot、forFeature 等方法来动态创建。
在 main.ts 里调用 NestFactory.create 方法,就会从 AppModule 开始递归解析 Module,实例化其中的 provider、controller,并依次调用它们的 onModuleInit 生命周期方法。
之后会再递归调用每个 Module 的 provider、controller 的还有 Module 自身的 onApplicationBootstrap 生命周期方法。
这样 Nest 就能对外提供服务了。
再就是 Nest 从接收到请求,到返回响应的这个流程,有很多切面。
路由最终是在 controller 的方法,也就是 handler 里处理的。
在这个过程中,会经历很多层切面:
首先,请求会被 middleware 处理,这一层可以复用 express 的中间件生态,实现 session、static files 等功能。
这个 middleware 也可以是 Nest 的那种 class 的 middleware,可以注入 provider。
然后在具体的路由会经历 Guard 的处理,它可以通过 ExecutionContext 拿到目标 class、handler 的metadata 等信息,可以实现权限验证等功能。
之后是 Interceptor 可以在请求前后做一些处理,它也可以通过 ExecutionContext 拿到 class、handler 的信息。
在到达 handler 之前,还会对参数用 Pipe 做下检验和转换。
这个过程中不管是哪一层抛的异常,都会被 Exception Filter 处理下,返回给用户友好的响应信息。
这就是整个请求到响应的流程。
通过 AOP 的切面,可以把通用逻辑封装起来,在各处复用。
之后,Nest 销毁的时候,也会依次调用 Module 的 provider、controller 还有 Module 自己的 onModuleDestroy 方法、beforeApplicationShutdown 还有 onApplicationShutdown 的生命周期方法。
后两者的区别是 beforeApplicationShutdown 可以拿到终止信号。
这就是 Nest 从创建、启动,到处理请求返回响应,再就是销毁的整个流程。
通过 IOC 实现了对象的自动创建、依赖的自动组装。
通过 AOP 实现了通用逻辑的抽取和复用。
IOC 内部的 Module 和 Provider 也都支持动态创建,灵活度很高。
通过图解串了一下 Module、Provider、Controller、动态模块、全局模块、自定义 Provider、生命周期、Middleware、Guard、Pipe、Interceptor、Exception Filter 等概念。
对 IOC、AOP 也有了更加清晰的认识。
把这些核心概念理解了,对 Nest 就算有比较好的掌握了。