Nest 真香

来一口老弟

陈仕豪 发表于 2018-08-22

今天的主题不是雀巢咖啡,也不是雀巢奶粉,毕竟我王境泽就是饿死,也不会打你们一点广告。

Nest.js

前言

近几年由于 Node.js 的发展,JavaScript 变成了一种全栈通用语言,同时诞生了诸如 Angular、React、Vue 等一系列提高开发者生产力的优秀前端项目框架,这些框架让我们开发更快速、测试更便捷、拓展更简单。

尽管 Node.js 服务端开发领域有着诸如 Express,Koa,Fastify 等一系列优秀的开源库、工具,但却缺乏真正意义上的框架。

于是我一直在寻找,油腻的师姐,在哪里……

直到那个冬季,我遇见了她 —— Nest。

Nest 是什么

官方概述是这样说的。

A progressive Node.js framework for building efficient, reliable and scalable server-side applications, heavily inspired by Angular.
一个深受 Angular 启发,旨在构建高效、可靠、高拓展性服务端应用程序的先进的 Node.js 框架。

正如 Nest 作者所言——深受 Angular 启发,所以 Nest 的开发体验与 Angular 有些类似。既可以使用 TypeScript(与 Angular 相同,官方推荐),也可以使用 JavaScript 构建项目,同时结合了面向对象编程(Object Oriented Programming)、函数式编程(Functional Programming)、函数响应式编程(Functional Reactive Programming)等元素,可兼容主流第三方库与插件。

目前 Nest 的 Github star 数在 8000 左右,比我关注那会高了不少。

Nest 特点

从下图可以看到官方宏观概述的特点

  1. 高拓展性。可正常使用任何第三方库。

  2. 多功能性。适用于多种类型的服务端应用程序编写。

  3. 前沿先进。结合最前沿的 JavaScript 技术特性,将设计模式和成熟方案的理念带入 Node.js 领域中。

以上是较为宏观的特性概述,关于其技术上的特点,我认为较为突出的有以下几点:

  1. 官方推荐使用 TypeScript,工程化意义重大。

  2. 基于 Express 与 socket.io 套件,兼容其他库。

  3. 语法风格类似 Angular 2+,也类似 Java Spring 框架,大量使用装饰器(ES7 Decorator)函数,无侵入式的语法使得代码逻辑更为清晰。

  4. 遵循控制反转(IoC, Inversion of Control)思想,大量使用依赖注入(DI, Dependency Injection)的设计模式,大大降低了单元间的耦合度。

  5. 内置的异常控制层(Exception Filter),大至应用,小至逻辑,对各个级别的异常作捕获与处理,在程序响应上对用户更为友好。

  6. 内置的权限控制层(Guard)与拦截层(Interceptor)。

  7. 集成工程化的测试,官方使用 Jest 测试框架。

小试牛刀

我刚开始接触 Nest 时,新建项目还没有现在这么方便,相关依赖的安装都比较人肉。

不过好在前不久正式版 @nestjs/cli 脚手架终于发布了(犹记得翘首以盼的我),新建项目也变得十分方便。

废话,少说。脚手架,走一波。(单押 X2)

环境准备

  • Visual Studio Code(萝卜青菜,各有所爱)

  • Node.js v9.2.0(>= 8.9.0 即可)

  • @nestjs/cli(官方脚手架)

1
$ npm i -g @nestjs/cli

· nest-demo(项目初始化)

1
$ nest new nest-demo

初识 Nest

如无意外,通过脚手架初始化项目的过程大致如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
⚡️  Creating your Nest project...
🙌 We have to collect additional information:

? description : description
? version : 0.0.0
? author : chenshihao

💥 Thank you for your time!

CREATE /nest-demo/.prettierrc (51 bytes)
CREATE /nest-demo/README.md (339 bytes)
CREATE /nest-demo/nodemon.json (147 bytes)
CREATE /nest-demo/package.json (1527 bytes)
CREATE /nest-demo/src/app.controller.spec.ts (588 bytes)
CREATE /nest-demo/src/app.controller.ts (266 bytes)
CREATE /nest-demo/src/app.module.ts (249 bytes)
CREATE /nest-demo/src/app.service.ts (138 bytes)
CREATE /nest-demo/src/main.hmr.ts (329 bytes)
CREATE /nest-demo/src/main.ts (208 bytes)
CREATE /nest-demo/test/app.e2e-spec.ts (593 bytes)
CREATE /nest-demo/test/jest-e2e.json (154 bytes)
CREATE /nest-demo/tsconfig.json (477 bytes)
CREATE /nest-demo/tslint.json (895 bytes)
CREATE /nest-demo/webpack.config.js (695 bytes)
CREATE /nest-demo/.nestcli.json (60 bytes)

? Which package manager would you ❤️ to use? yarn
▹▸▹▹▹ Take ☕️ or 🍺 during the packages installation process and enjoy your time

🚀 Successfully created project nest-demo

以上操作创建了种子项目并用 yarn 安装了依赖包。如果没有安装成功的话,可手动进行安装。

我们可以看到,脚手架工具创建了若干文件,包括

  • 根目录

    大多为配置文件,如 prettier、nodemon、tslint 等工具的配置文件(若不熟悉可以暂时忽略,将重心放在 *.ts 文件上有助于快速入手框架)

  • test 文件夹

    端对端(e2e)测试用例与配置文件

  • src 文件夹

    主程序入口文件及若干模块文件。其中文件层次关系大致如下

我们对文件结构有了初步的认识

  • 入口文件 main.ts 引导程序运行,加载模块 app.module.ts

  • 模块中,包括控制器 app.controller.ts,服务提供商 app.service.ts,测试用例 app.controller.spec.ts 等组件。

接下来我们直接以开发模式运行程序。

1
$ npm run start:dev

如无意外,我们可以看到控制台有以下输出日志。

1
2
3
4
5
6
7
8
9
10
11
12
> nest-demo@0.0.0 start:dev /Users/victor/Desktop/nest-demo
> nodemon

[nodemon] 1.18.3
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: /Users/victor/Desktop/nest-demo/src/**/*
[nodemon] starting `ts-node -r tsconfig-paths/register src/main.ts`
[Nest] 16999 - 2018-8-20 11:33:49 [NestFactory] Starting Nest application...
[Nest] 16999 - 2018-8-20 11:33:49 [InstanceLoader] AppModule dependencies initialized +8ms
[Nest] 16999 - 2018-8-20 11:33:49 [RoutesResolver] AppController {/}: +14ms
[Nest] 16999 - 2018-8-20 11:33:49 [RouterExplorer] Mapped {/, GET} route +3ms
[Nest] 16999 - 2018-8-20 11:33:49 [NestApplication] Nest application successfully started +3ms

从图中我们能够得到的信息大致为

  • 运行 nodemon 监听项目文件

  • 通过 ts-node 引导入口文件 main.ts

  • 初始化模块与路由

在 main.ts 可以看到,默认端口号为 3000

1
2
3
4
5
6
7
main.ts

async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();

我们直接访问地址 http://localhost:3000 看看效果

1
2
$ curl localhost:3000
Hello World!

样例不作为参考,正式开发中,推荐使用 Postman 进行 API 调试

Hello World!

看到 “Hello World! ” 的字符串,不禁让猿热血澎湃 —— 他做到了!

我们回过头来看看这个值是怎么来的。

通过结构示例图,我们知道模块 (module) 是一个非常重要的概念,而每个模块的控制器 (controller) 更是扮演着运筹帷幄的角色。

1
2
3
4
5
6
7
8
9
10
11
app.controller.ts

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@Get()
root(): string {
return this.appService.root();
}
}

app.controller.ts 文件中定义了一个类,通过装饰器 @Controller() 使其成为一个路由控制类。类方法中,有构造函数 constructor 以及带有装饰器 @Get() 的 root 函数。

1
2
3
4
5
6
7
8
app.service.ts

@Injectable()
export class AppService {
root(): string {
return 'Hello World!';
}
}

root 函数通过调用 service 中的 root 方法,返回字符串 “Hello World!”。

此外,关于 HTTP 响应的操作,Nest 大体上已经帮我们安排的明明白白了,这也是官方推荐的写法。

  • 当我们的函数返回 JavaScript 对象或数组时,返回值会被自动转化成 JSON 对象;

  • 当我们的函数返回字符串时,返回值不作处理。

  • 响应状态码默认情况下总是 200,除了 POST 请求为 201 外,当然,我们是有办法通过装饰器轻松修改返回值的。

当然啦,如果有倔强的老哥非要操作一下 response 对象,也不是不可以滴。

读到这里,我们便大致了解能用 GET 方法访问 http://localhost:3000 得到“Hello World!”的来龙去脉了吧。

装饰器

在上述代码片段中,我们会发现许多装饰器,如 @Controller()、@Injectable() 等。

装饰器是什么?装饰器的定义大致如下

An ES2016 decorator is an expression which returns a function and can take a target, name and property descriptor as arguments. You apply it by prefixing the decorator with an @ character and placing this at the very top of what you are trying to decorate. Decorators can be defined for either a class or a property.
ES7 装饰器是一种返回函数,且可传递目标对象、名称与属性描述作为参数的表达式。你可以使用@字符作为前缀并将装饰器放在你想装饰的对象上。装饰器可以被用在一个类或者一个属性上。

想要了解更多关于装饰器的知识,可以参考这篇文章或者自行搜索
https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841

装饰器 (Decorator) 作为 ES7 的一大特性,在 TypeScript 中可以被自由运用,在 Nest 中更是得到了合理的开发与利用。

纵观 Nest ,装饰器贯穿了整个 Nest 框架,如:

  • 模块,一个带有 @Module() 装饰器的类

  • 控制器,一个带有 @Controller() 装饰器的类

  • 提供商,一个带有 @Injectable() 装饰器的类

  • 通过装饰器语法装饰参数,更清晰便捷地取值

为了更好地说明装饰参数,我们举个栗子,修改一下 app.controller.ts 和 app.service.ts

1
2
3
4
5
6
7
8
app.controller.ts

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

@Get()
root(@Query('name') name: string = 'Victor'): string {
return this.appService.root(name);
}

往 app.controller.ts 的 GET 方法中添加参数 name,其中 Query 指的是 url 中的 params 参数

1
2
3
4
5
6
7
8
app.service.ts

@Injectable()
export class AppService {
root(name: string = 'World'): string {
return `Hello ${name}!`;
}
}

配合控制层,往 app.service.ts 添加参数 name 以说明值的改变。

此时服务会自动重启,我们以 GET 方式请求新地址

http://localhost:3000?name=victor

1
2
$ curl http://localhost:3000?name=Victor
Hello Victor!

Surprise!好了,一个 url 传参的鲜活栗子就这么轻松加愉快的举完了。

除了 @Query() 之外,Nest 内置的还有一些装饰器提供给我们直接使用,列举一下

  • @Request() —— Express 请求对象

  • @Response() —— Express 响应对象

  • @Session() —— Session 对象

  • @Param(param?: string) —— RESTful 风格的路由参数

  • @Body(param?: string) —— 请求体

  • @Headers(param?: string) —— 请求头

官方提供的装饰器固然方便,然鹅在错综复杂的需求下,仅仅有这几个装饰器还是略显不足的。在这里,我们甚至可以自定义装饰器。

这里我们新建一个文件 name.decorator.ts,为了避免逻辑干扰,在这里我们直接返回传递的值 data。

1
2
3
4
5
6
7
name.decorator.ts

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

export const Name = createParamDecorator((data, req) => {
return data;
});

在 controller 中引用自定义装饰器,并传入常量 Victor。

1
2
3
4
5
6
7
8
app.controller.ts

import { Name } from 'name.decorator';

@Get()
root(@Name('Victor') name: string): string {
return this.appService.root(name);
}

此时服务重启,再次访问 http://localhost:3000

1
2
$ curl localhost:3000
Hello Victor!

通过装饰器的作用,我们成功的将 name 的值设置成 Victor,这是一个简单的例子,在实际生产环境中可以添加更为复杂的逻辑。

依赖注入

如果说装饰器语法是 Nest 健美的身材,那么依赖注入则可以称为 Nest 有趣的灵魂了。

简单来说,依赖注入(DI, Dependency Injection)是一种设计模式,旨在提供对象实例,调用单元在使用依赖对象实例时无需关心其提供方式,而是统一由 DI 系统提供。

想要了解更多依赖注入知识,可以参考以下文章或者自行搜索

https://angular.io/guide/dependency-injection-pattern

事实上,Nest 的设计理念是,“万物皆可为提供商” —— 如服务类(service)、仓库类(repository)、工厂类(factory)、帮助类(helper)等等。

我们从实际代码入手了解这一概念,前面提到的 @Injectable() 装饰器,其装饰的对象正是我们所说的“依赖对象”,也就是例子中的 service 类。

1
2
3
4
5
6
7
8
app.module.ts

@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

在 module 文件中,我们声明了 AppService 类作为 provider,这是 AppController 可以无实例化直接使用 appService 对象的伏笔。

1
2
3
4
5
6
7
8
9
10
11
app.controller.ts

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@Get()
root(): string {
return this.appService.root();
}
}

AppService 作为可注入的依赖对象,通过在构造函数 constructor 中声明的方式,配合 DI 模式,解决了两者之间的依赖关系与独立性。于是,我们可以直接在方法中调用 appService。

1
2
3
4
5
6
7
8
app.service.ts

@Injectable()
export class AppService {
root(): string {
return 'Hello World!';
}
}

理解了依赖注入的概念,可以更好的阅读与学习 Nest 的其他内容,如中间件(Middlewire)、管道(Pipe)、防御层(Guard)、拦截器(Interceptor)等等,都是通过依赖注入的方式与我们的控制层进行关联的。

实际上,配合 @Inject() 装饰器,可以实现更为多样的注入类型,这是更为高级的 provider 注入方式,由于文章篇幅原因,在此就不赘述了。

结束语

这是我的第一篇公众号文章。

初心是为了介绍 Nest 框架,希望是不仅有落到实处的代码示例,不至于显得太空虚;同时也有更为重要的核心概念介绍,让读者有更好的大局观与学习方向。

一篇文章往往不足以描述事物的全部,还有很多优秀的思想与巧妙的设计等着我们去探索,我们下篇文章见!

参考资料

原创作者

陈仕豪,年十八,骚话连篇笑哈哈。skr~

原创链接

https://mp.weixin.qq.com/s/LKCecn2z1Pln90atQ0kcWw