主要功能
基于 NestJS,我们用两台不同架构的设备搭建起一个分布式系统,其中树莓派作为网关,接受 HTTP 请求,笔记本电脑提供计算服务。
设备清单
一台 arm64 架构的树莓派(网关),IP地址为192.168.0.6
一台 amd64 架构的电脑(计算),IP地址为192.168.0.3
网关
一、安装Nestjs框架和一些必要的依赖项
npm i --g @nestjs/cli
nest new gateway
npm i --save @nestjs/microservices
二、修改代码
在 main.ts 我们让 app 监听 8888 端口。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(8888);
}
bootstrap();
在 app.service.ts 中,其通过 `@nestjs/microservices` 模块提供的 TCP 客户端将消息发送到电脑的计算服务器。在 `@Client` 装饰器中指定服务器的主机和端口配置连接,然后调用 `this.client.send()` 方法发送格式化的消息。
import { Injectable } from '@nestjs/common';
import { Client, ClientProxy, Transport } from '@nestjs/microservices';
import { Observable, map } from 'rxjs';
@Injectable()
export class AppService {
@Client({
transport: Transport.TCP,
options: {
host: '192.168.0.3',
port: 3000,
}
})
private client: ClientProxy;
getHello(): Observable<number> {
const pattern = { cmd: 'sum' };
const payload = [1, 2, 3];
return this.client.send(pattern, payload).pipe(
map((data) => {
return data;
})
);
}
}
三、运行
计算服务
一、安装Nestjs框架和一些必要的依赖项
npm i --g @nestjs/cli
nest new math
npm i --save @nestjs/microservices
二、修改代码
首先修改 main.ts 中的代码,把我们的数学计算服务改为微服务,也就是 NestFactory.createMicroservice。
其中 options 里面我们把主机设成了 localhost,端口为3000。
import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.TCP,
options: {
host: 'localhost',
port: 3000,
},
},
);
await app.listen();
}
bootstrap();
然后我们修改 controller 里的方法,返回计算结果。
import { Controller, Get } from '@nestjs/common';
// import { AppService } from './app.service';
import { MessagePattern } from '@nestjs/microservices';
@Controller()
export class AppController {
@MessagePattern({ cmd: 'sum' })
accumulate(data: number[]): number {
return (data || []).reduce((a, b) => a + b);
}
}
三、运行
测试
我们用 wget 测试接口,如果 Content 输出6说明正确调用了电脑的计算服务。
MVC、IOC
MVC 是 Model View Controller 的简写。MVC 架构下,请求会先发送给 Controller,由它调度 Model 层的 Service 来完成业务逻辑,然后返回对应的 View。
Nest.js 提供了 @Controller 装饰器用来声明 Controller:
而 Service 会用 @Injectable 装饰器来声明:
通过 @Controller、@Injectable 装饰器声明的 class 会被 Nest.js 扫描,创建对应的对象并加到一个容器里,这些所有的对象会根据构造器里声明的依赖自动注入,也就是 DI(dependency inject),这种思想叫做 IOC(Inverse Of Control)。
IOC 架构的好处是不需要手动创建对象和根据依赖关系传入不同对象的构造器中,一切都是自动扫描并创建、注入的。
此外,Nest.js 还提供了 AOP (Aspect Oriented Programming)的能力,也就是面向切面编程的能力:
AOP
AOP 是什么意思呢?什么是面向切面编程呢?
一个请求过来,可能会经过 Controller(控制器)、Service(服务)、Repository(数据库访问) 的逻辑:
如果想在这个调用链路里加入一些通用逻辑该怎么加呢?比如日志记录、权限控制、异常处理等。
容易想到的是直接改造 Controller 层代码,加入这段逻辑。这样可以,但是不优雅,因为这些通用的逻辑侵入到了业务逻辑里面。能不能透明的给这些业务逻辑加上日志、权限等处理呢?
那是不是可以在调用 Controller 之前和之后加入一个执行通用逻辑的阶段呢?
比如这样:
这样的横向扩展点就叫做切面,这种透明的加入一些切面逻辑的编程方式就叫做 AOP (面向切面编程)。
AOP 的好处是可以把一些通用逻辑分离到切面中,保持业务逻辑的存粹性,这样切面逻辑可以复用,还可以动态的增删
其实 Express 的中间件的洋葱模型也是一种 AOP 的实现,因为你可以透明的在外面包一层,加入一些逻辑,内层感知不到。
而 Nest.js 实现 AOP 的方式更多,一共有五种,包括 Middleware、Guard、Pipe、Inteceptor、ExceptionFilter:、
中间件 Middleware
Nest.js 基于 Express 自然也可以使用中间件,但是做了进一步的细分,分为了全局中间件和路由中间件:
全局中间件就是 Express 的那种中间件,在请求之前和之后加入一些处理逻辑,每个请求都会走到这里:
路由中间件则是针对某个路由来说的,范围更小一些:
这个是直接继承了 Express 的概念,比较容易理解。
再来看一些 Nest.js 扩展的概念,比如 Guard:
Guard
Guard 是路由守卫的意思,可以用于在调用某个 Controller 之前判断权限,返回 true 或者 flase 来决定是否放行:
创建 Guard 的方式是这样的:
Guard 要实现 CanActivate 接口,实现 canActive 方法,可以从 context 拿到请求的信息,然后做一些权限验证等处理之后返回 true 或者 false。
通过 @Injectable 装饰器加到 IOC 容器中,然后就可以在某个 Controller 启用了:
Controller 本身不需要做啥修改,却透明的加上了权限判断的逻辑,这就是 AOP 架构的好处。
而且,就像 Middleware 支持全局级别和路由级别一样,Guard 也可以全局启用:
Guard 可以抽离路由的访问控制逻辑,但是不能对请求、响应做修改,这种逻辑可以使用 Interceptor:
Interceptor
Interceptor 是拦截器的意思,可以在目标 Controller 方法前后加入一些逻辑:
创建 Inteceptor 的方式是这样的:
Interceptor 要实现 NestInterceptor 接口,实现 intercept 方法,调用 next.handle() 就会调用目标 Controller,可以在之前和之后加入一些处理逻辑。
Controller 之前之后的处理逻辑可能是异步的。Nest.js 里通过 rxjs 来组织它们,所以可以使用 rxjs 的各种 operator。
Interceptor 支持每个路由单独启用,只作用于某个 controller,也同样支持全局启用,作用于全部 controller:
除了路由的权限控制、目标 Controller 之前之后的处理这些都是通用逻辑外,对参数的处理也是一个通用的逻辑,所以 Nest.js 也抽出了对应的切面,也就是 Pipe:
Pipe
Pipe 是管道的意思,用来对参数做一些验证和转换:
创建 Pipe 的方式是这样的:
Pipe 要实现 PipeTransform 接口,实现 transform 方法,里面可以对传入的参数值 value 做参数验证,比如格式、类型是否正确,不正确就抛出异常。也可以做转换,返回转换后的值。
内置的有 8 个 Pipe,从名字就能看出它们的意思:
ValidationPipe
ParseIntPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
DefaultValuePipe
ParseEnumPipe
ParseFloatPipe
同样,Pipe 可以只对某个路由生效,也可以对每个路由都生效:
不管是 Pipe、Guard、Interceptor 还是最终调用的 Controller,过程中都可以抛出一些异常,如何对某种异常做出某种响应呢?
这种异常到响应的映射也是一种通用逻辑,Nest.js 提供了 ExceptionFilter 来支持:
ExceptionFilter
ExceptionFilter 可以对抛出的异常做处理,返回对应的响应:
创建 ExceptionFilter的形式是这样的:
首先要实现 ExceptionFilter 接口,实现 catch 方法,就可以拦截异常了,但是要拦截什么异常还需要用 @Catch 装饰器来声明,拦截了异常之后,可以异常对应的响应,给用户更友好的提示。
当然,也不是所有的异常都会处理,只有继承 HttpException 的异常才会被 ExceptionFilter 处理,Nest.js 内置了很多 HttpException 的子类:
BadRequestException
UnauthorizedException
NotFoundException
ForbiddenException
NotAcceptableException
RequestTimeoutException
ConflictException
GoneException
PayloadTooLargeException
UnsupportedMediaTypeException
UnprocessableException
InternalServerErrorException
NotImplementedException
BadGatewayException
ServiceUnavailableException
GatewayTimeoutException
当然,也可以自己扩展:
Nest.js 通过这样的方式实现了异常到响应的对应关系,代码里只要抛出不同的 HttpException,就会返回对应的响应,很方便。
同样,ExceptionFilter 也可以选择全局生效或者某个路由生效:
某个路由:
全局:
我们了解了 Nest.js 提供的 AOP 的机制,但它们的顺序关系是怎样的呢?
几种 AOP 机制的顺序
Middleware、Guard、Pipe、Interceptor、ExceptionFilter 都可以透明的添加某种处理逻辑到某个路由或者全部路由,这就是 AOP 的好处。
但是它们之间的顺序关系是什么呢?
调用关系这个得看源码了。
对应的源码是这样的:
很明显,进入这个路由的时候,会先调用 Guard,判断是否有权限等,如果没有权限,这里就抛异常了:
抛出的 HttpException 会被 ExceptionFilter 处理。
如果有权限,就会调用到拦截器,拦截器组织了一个链条,一个个的调用,最后会调用的 controller 的方法:
调用 controller 方法之前,会使用 pipe 对参数做处理:
会对每个参数做转换:
ExceptionFilter 的调用时机很容易想到,就是在响应之前对异常做一次处理。
而 Middleware 是 express 中的概念,Nest.js 只是继承了下,那个是在最外层被调用。
这就是这几种 AOP 机制的调用顺序。把这些理清楚,就算是对 Nest.js 有很好的掌握了。
总结
整个分布式系统实现起来非常简单,可见 NestJS 做了很多的封装方便开发。