文章修正于 2024/01/16 (去掉错别字/把逻辑理更顺)
本文概要和目录
书接上文,我们继续来完善我们的这个Nest应用
重要提醒!:请不要照着文章照抄,建议你先阅读通篇,了解全貌之后再去实践。
统一返回体
我们回顾我们的目前的应用 不难发现,在返回体Res上我们比较混乱没有一致的返回格式,是时候改变了,让我们使用interceptor 改造他们
- 新建一个拦截器去处理
我们默认你已经学习并且掌握了 Interceptor拦截器的所有知识点,如果你还不能掌握 请前往 我之前的文章
// http-req.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
data: T;
}
@Injectable()
export class HttpReqTransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response<T>> {
return next
.handle()
.pipe(map((data) => ({ data, code: 200, msg: '', success: true })));
// 我们非常的单纯把 返回体统一进行了包装 ,不要学我哈,我这里就是为了写而瞎写的,具体的返回code是什么。以及其他的内容 你需要判断,自己设计逻辑。
// 落地到实际运用,你也许会从data中再剥离一部分属性 来构建这个标准的返回体,这完全取决于你的团队要求
}
}
- 如何使用
其他任何 Interceptor 一样去使用就好了,没有什么区别
@ApiTags('Tag相关')
@ApiBearerAuth()
@Controller('tag')
@UseInterceptors(new HttpReqTransformInterceptor<any>()) // 统一返回体
export class TagController {
constructor(private readonly tagService: TagService) {}
@UseGuards(AuthGuard('jwt'))
@Get('/tags')
async getAll() {
const value = await this.tagService.getAll();
return value;
}
@UseGuards(AuthGuard('local'))
@Post()
async createTag(@Body() tagInfo: Tag) {
const value = await this.tagService.create(tagInfo);
return value;
}
@Put('/:id')
async updateTag(@Param() params: InterParams, @Body() tagInfo: Tag) {
const value = await this.tagService.updateById(params.id, tagInfo);
return value;
}
@Delete('/:id')
async deleteTag(@Param() params: InterParams) {
const value = this.tagService.deleteById(params.id);
return value;
}
}
最后你将会得到下面这样的返回
{
"data": [],
"code": 200,
"msg": "",
"success": true
}
-
和Dto冲突了怎么办?认证 冲突了怎么办?
细心的小伙伴。已经发现了上面的问题了,那就是,如果我没有验证通过或者我的引应用程序发送了错误🙅♂️ 出BUG啦,那么你将会得到下面的返回体.
实际上哈,在正常的工作中,正常的res 和 异常的res 本身就是两套,我们的设计是没有问题的。下面只是说一下。当然你要统一格式的话也是可以的 去把异常的filler 改一下就好了,在此不赘述
// 我们故意不传递 正常的 验证信息
{
"statusCode": 401,
"error": "Unauthorized",
"msg": "Client Error"
}
// 写一个程序bug
{
"statusCode": 500,
"msg": "Service Error: Error: xxx"
}
// 我们期待的是
{
"data": [],
"code": 200,
"msg": "",
"success": true
}
// 这里的解决方案是把 制造这种异常的制造者干掉!,当然你也可以把你自己干掉,你就修改成和Nest系统内置的保存一样的接口类型就好了
// 首先是我们要处理所有的异常过滤器让他们变成我们所期待的样子
// 我们把所有的异常过滤器都修改成统一的返回数据结构体
//AllExceptionsFilter
response.status(status).json({
code: status,
message: exception.message,
data: null,
success: false,
});
}
// HttpExceptionFilter
response.status(status).json({
code: status,
message: exception.message,
data: null,
success: false,
});
// 同时你还需要在返回的时候进行描述,当然你可以不必,因为过滤器把这件事都处理了,有的同学说,这么这里讲
// 劈叉了呢,不是拦截器吗怎么就变成过滤器了?原因很简单,因为全局最上层有Filter 错误处理都在fillter 所以要同时
// 处理Filter 和 Interceptor的关系
//如果你代码中有抛Error的地方加上你的特殊标记
throw new UnauthorizedException('您账户已经在另一处登陆,请重新登陆');
上传文件
上传文件无疑是非常常见的功能了,我们来看看在Nest中如何做这件事情,(我们有轮子 不怕!🤪)
- MulterModule模块
这个是Nestjs提供的一个@nestjs/platform-express 专门用来处理文件,它的使用非常的简单
MulterModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
storage: diskStorage({ // 配置存储
destination: join(__dirname, '../../../', '/upload'), // 要存储的位置
filename: (req, file, cb) => { // 文件名处理
const filename = `${randomUUID()}.${file.mimetype.split('/')[1]}`;
return cb(null, filename);
},
}),
}),
inject: [ConfigService],
}),
- FileInterceptor UploadedFile 装饰器
仅仅是单上面的内容我们是没办法处理全局的逻辑的,我们需要一个路由来接受参数
// FileInterceptor是从@nestjs/platform-express 导入的一个装饰器,这里是用来处理单个文件📃
@UseInterceptors(FileInterceptor('file'))
@Post('upload')
uploadFile(@Body() body: any, @UploadedFile() file: Express.Multer.File) {
return {
file: file.filename,
path: file.path,
// 路径请结合前面的main多静态目录来实现 我们只返回文件的相对路径,
// 为了让外部能够访问 你需要再这里拼上 service 部署的domian地址,
size: file.size,
};
}
// 上述就是一个非常简单的 上传文件了,用form-data格式上传文件上来就好了,下面的例子是关于多文件上传的
@UseInterceptors(FilesInterceptor('files'))
@Post('uploads')
uploadFiles(
@Body() body: any,
@UploadedFiles() files: Array<Express.Multer.File>,
) {
return files.map((item) => ({
name: item.fieldname,
path: item.path,
size: item.size,
}));
}
- 第三方上传阿里云OSS
有的时候我们不仅仅需要上传到自己的服务器,我们还需要上传到第三方的OSS,在Nest中我们可以集成aliyun的OSS-SDK来做到这个功能,在这里我就不详细的说明 阿里云OSS的操作了 你可以直接去看他们的官方文档 help.aliyun.com/document_de… 我们需要获取你的验证凭据,有了凭据你就能操作它们的OSS了,并且阿里云OSS还提供了Nodejs的SDK 集成,并且对于到底是把 流 存本地还是存内存 其实各有各的处理方案。我这里把他们存本地了不用阿里云的OSS,后期搭配Job定时去清理这些文件
import { HttpService } from '@nestjs/axios';
import {
Body,
Controller,
Get,
Post,
Render,
UploadedFile,
UploadedFiles,
UseInterceptors,
Req,
Res,
Param,
} from '@nestjs/common';
import { Request } from 'express';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import * as OSS from 'ali-oss';
import multer, { diskStorage } from 'multer';
import path, { join, normalize } from 'path';
import { randomUUID } from 'crypto';
@Controller('files')
export class FilesController {
oss: OSS;
constructor(private readonly httpService: HttpService) {
this.oss = new OSS({
region: 'oss-cn-beijing',
accessKeyId: 'yourID',
accessKeySecret: 'yourKey',
bucket: 'yourBucket',
});
}
@UseInterceptors(FileInterceptor('file'))
@Post('upload')
uploadFile(@Body() body: any, @UploadedFile() file: Express.Multer.File) {
return {
file: file.filename,
path: file.path,
size: file.size, // 路径请结合前面的main多静态目录来实现
};
}
@UseInterceptors(FilesInterceptor('files'))
@Post('uploads')
uploadFiles(
@Body() body: any,
@UploadedFiles() files: Array<Express.Multer.File>,
) {
return files.map((item) => ({
name: item.fieldname,
path: item.path,
size: item.size,
}));
}
@Post('upload-oss')
@UseInterceptors(
FileInterceptor('file', {
storage: diskStorage({
destination: join(__dirname, '../../../', '/upload-oos'),
filename: (req, file, cb) => {
const filename = `${randomUUID()}.${file.mimetype.split('/')[1]}`;
return cb(null, filename);
},
}),
}),
)
async oos(@UploadedFile() file: Express.Multer.File) {
// 上传的时候我们运行你上传到内存中 然后发送给第三方但是这样做不好,
// 如果存储文件太多或者并非量 你的机器会撑不住,因此我们建议的做法是先存到某
// 临时目录,然后调用第三方去upload 最后由定时job删除这个up目录就好了
// 主要还是文件的上传和下载 上传比较简单
const value = await this.oss.put(file.filename, normalize(file.path));
return value;
}
// 启用oss 下载需要做临时验证
@Get('upload-oss/:file')
async getOSSFile(@Param() params: { file: string }) {
// 上传的时候我们运行你上传到内存中 然后发送给第三方但是这样做不好,
// 如果存储文件太多或者并非量 你的机器会撑不住,因此我们建议的做法是先存到某
// 临时目录,然后调用第三方去upload 最后由定时job删除这个up目录就好了
// 主要还是文件的上传和下载 上传比较简单
const value = this.oss.signatureUrl(params.file, {
expires: 3600,
});
return {
url: value,
};
}
- 上面说明了如何存储文件那么如何访问文件呢?
我们需要使用 express.static 来达到这个功能
// main.ts
// 配置文件访问 文件夹为静态目录,以达到可直接访问下面文件的目的
const rootDir = join(__dirname, '..');
app.use('/static', express.static(join(rootDir, '/upload')));
// app.use('/static', express.static(join(rootDir, '/upload'))); // 允许配置多个
请求转发
请求转发相对的简单
// module
---省略部分代码(如果你不知道我在写什么,那么你一定么有好好的阅读我前面的文章)---
HttpModule.register({
timeout: 5000,
maxRedirects: 5,
}),
// httpService是nest内置的一个axios模块 使用前需要去module注入 利用这个我们可以去 “爬”人家的数据了 🐶🐶🐶
@Get('httpUser')
async getIpAddress() {
const value = await this.httpService
.get('https://api.gmit.vip/Api/UserInfo?format=json')
.toPromise();
return { ...value.data };
}
定时Job
前面我们提过一嘴“要把多余的没有用的上传文件删除掉,在把日志给处理掉”,这些实现都离不开job,我们现在来说说Nest中如何做job
理论知识
-
首先你需要了解job这个概念和一些常见知识
定时任务允许你按照指定的日期/时间、一定时间间隔或者一定时间后单次执行来调度。 从字面上也提出好理解就是一个job 一个现定时/不定时的工作项任务,可以重复执行的。我们对它的定时衍生出来了一套规定和语法,在
Linux
世界中,这经常通过操作系统层面的cron
包等执行。
名称 | 含义 |
---|---|
* * * * * * | 每秒 |
45 * * * * * | 每分钟第 45 秒 |
_ 10 _ * * * | 每小时,从第 10 分钟开始 |
0 _/30 9-17 _ * * | 上午 9 点到下午 5 点之间每 30 分钟 |
0 30 11 * * 1-5 | 周一至周五上午 11:30 |
-
我们看看在nest中如何声明job
在Nestjs中我们使用 @nestjs/schedule 这个库它底层上对node-cron的封装,
// 最简单的job ,(它在你app启动之后,会自动的每45s执行一次)
// 使用前别忘了去注入
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [ScheduleModule.forRoot()],
})
export class AppModule {}
// 启动一个服务去使用它
@Injectable()
export class TasksService {
@Cron('45 * * * * *')
handleCron() {
console.log('666')
}
}
// 它还有很多骚操作比如定义间隔 就像定时器,定义延时的话可以调用这个装饰器 :“@Timeout(5000)”
@Interval(10000)
handleInterval() {
this.logger.debug('Called every 10 seconds');
}
- 如何运行它?
上面的我们提到过这个是自动运行的,那么我们有没有可能手动的去启动它,停止它呢?
// 手动运行
@Cron('30 * * * * *', {
name: 'notifications',
})
handleTimeout() {
console.log('66666');
}
// 在某个 controller/service 中进行手动调度测试
constructor(
private schedulerRegistry: SchedulerRegistry
) {}
@Get('/job')
async stopJob(@Param() params: { start: boolean }) {
const job = this.schedulerRegistry.getCronJob('notifications');
// this.schedulerRegistry 更多详细操作请去看官方文档 https://docs.nestjs.cn/7/techniques?id=%e5%ae%9a%e6%97%b6%e4%bb%bb%e5%8a%a1
if (params.start) {
job.start();
console.log(job.lastDate());
} else {
job.stop();
}
}
实践指南
为了简单起见我们这里只设置了一个job(它用来清理上传到OSS的没有的文件)
// 使用前 你需要去 module中进行注入哈
import { Module } from '@nestjs/common';
import { JobService } from './job.service';
// 这个模块专门处理job 如果其他模块由job的需求,全收敛到这里来处理
@Module({
imports:[ ScheduleModule.forRoot() ]
providers: [],
controllers: [],
exports: [JobService],
})
export class JobModule {}
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression, Interval, Timeout } from '@nestjs/schedule';
import * as fs from 'fs';
import { join } from 'path';
// 清除日志目录和本地上传的文件oss临时文件
@Injectable()
export class JobService {
emptyDir = (fileUrl) => {
const files = fs.readdirSync(fileUrl); //读取该文件夹
files.forEach(function (file) {
const stats = fs.statSync(fileUrl + '/' + file);
if (stats.isDirectory()) {
this.emptyDir(fileUrl + '/' + file);
} else {
fs.unlinkSync(fileUrl + '/' + file);
}
});
};
// 每天晚上11点执行一次
@Cron(CronExpression.EVERY_DAY_AT_11PM)
handleCron() {
// 删除OSS文件和日志文件
const OSSRootDir = join(__dirname, '../../../upload-oos');
// 日志一般是转存 而不是删除哈,注意 这里只是简单的例子而已
const accesslogDir = join(__dirname, '../../../logs/access');
const appOutDir = join(__dirname, '../../../logs/app-out');
const errorsDir = join(__dirname, '../../../logs/errors');
this.emptyDir(OSSRootDir);
this.emptyDir(accesslogDir);
this.emptyDir(appOutDir);
this.emptyDir(errorsDir);
}
}
swagger
swagger 就非常的简单了直接上代码 官方文档在这里 docs.nestjs.cn/7/recipes?i… 虽然文档这额里很简单,我在这里记录一下我的遇到的坑
- main中加入
// 构建swagger文档
const options = new DocumentBuilder()
.setTitle('Base-Http-example')
.addBearerAuth()
.setDescription('一个完善的HttpNodejs服务')
.setVersion('1.0')
.addTag('Http')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api', app, document);
- 去Controller中绑定
@ApiTags('User相关')
@Controller('user')
@UseInterceptors(new HttpReqTransformInterceptor<any>()) // 统一返回体
export class UserController {
....省略很多代码,Swagger用法在官方已经描述得非常详细了。各种都有也没什么坑
}
- 去Dto中写参数说明 也就是写Model
export class UserInfoDTO {
@ApiProperty({
description: '名称',
default: '用户1',
})
@IsNotEmpty({ message: '用户名不允许为空' })
username: string;
@ApiProperty()
@IsNotEmpty({ message: '密码不允许为空' })
password: string;
@ApiProperty()
@IsNotEmpty({ message: '更新创建时间必选' })
@IsNumber()
update_time: number;
@ApiProperty()
@IsNotEmpty({ message: '创建时间必选' })
create_time: number;
@ApiProperty()
@IsNotEmpty({ message: '状态必填' })
state: number;
}
上面都是最基础的用法,如果还希望有更多的骚气操作请前去官方文档
利用redis做单点登
接下来我们将会说明,如何用。redis做单点登录
- 首先是安装redis
这个应该对各位来说是比较的简单的哈,可以直接去看 菜鸟教程
- 我们自己实现一个Module
其实 我们有现成的轮子,但是为零学习 我们还是造了一个
在我们的moduels中实现三个文件 他们分别是cache 的controller module和service (实际上我们只需要service就好了哈哈哈)
@Controller('cache')
export class CacheController {}
@Module({
imports: [],
controllers: [CacheController],
providers: [CacheService],
exports: [CacheService],
})
export class CacheModule {}
import { Injectable } from '@nestjs/common';
import RedisC, { Redis } from 'ioredis';
@Injectable()
// 目前的版本比较的简单 只是一个设置值清除值,在启动微服务之后,这个地方就不是这样写了
export class CacheService {
redisClient: Redis;
// 先做一个最简易的版本,只生产一个 链接实例
constructor() {
this.redisClient = new RedisC({
port: 6379, // Redis port
host: '192.168.101.10', // Redis host
family: 4, // 4 (IPv4) or 6 (IPv6)
password: '',
db: 0,
});
}
// 编写几个设置redis的便捷方法
/**
* @Description: 封装设置redis缓存的方法
* @param key {String} key值
* @param value {String} key的值
* @param seconds {Number} 过期时间
* @return: Promise<any>
*/
public async set(key: string, value: any, seconds?: number): Promise<any> {
value = JSON.stringify(value);
if (!seconds) {
await this.redisClient.set(key, value);
} else {
await this.redisClient.set(key, value, 'EX', seconds);
}
}
/**
* @Description: 设置获取redis缓存中的值
* @param key {String}
*/
public async get(key: string): Promise<any> {
const data = await this.redisClient.get(key);
if (data) return data;
return null;
}
/**
* @Description: 根据key删除redis缓存数据
* @param key {String}
* @return:
*/
public async del(key: string): Promise<any> {
return await this.redisClient.del(key);
}
/**
* @Description: 清空redis的缓存
* @param {type}
* @return:
*/
public async flushall(): Promise<any> {
return await this.redisClient.flushall();
}
}
- 有了redis和对他的基础操作之后,我们看看如何实现单点登录
主要的逻辑 是 在签发token的时候把token存到redis中标识key就是用户id,如果下次还有同样的用户id登录就把原来的redis中的key对应的值换成新的,这样先前的那个用户 再次访问的时候发现这个token和之前的不相同,那么就认为它在别的地方登录了,本地就强制下线
// 在jwt的几个文件中做修改
// /src/moduels/auth/ath.service.ts
async certificate(user: User) {
const payload = {
username: user.username,
sub: user.id,
};
console.log('JWT验证 - Step 3: 处理 jwt 签证');
try {
const token = this.jwtService.sign(payload);
// 把token存储到redis中,如果这个用户下次还登录就把这个值更新了,载validate的时候看看能不能
// 找到原来的key的值没有就说明更新了就强制要求用户下线 于是这单点登录功能就完成了 ,过期时间和token一致
await this.CacheService.set(
`user-token-${user.id}-${user.username}`,
token,
60 * 60 * 8,
);
return {
code: 200,
data: {
token,
},
msg: `登录成功`,
};
} catch (error) {
return {
code: 600,
msg: `账号或密码错误`,
};
}
}
// jwt逻辑 和local逻辑也要变化一下
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
// 本地local的策略于jwt关系不大,
console.log('你要调用我哈------------');
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly CacheService: CacheService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
passReqToCallback: true,
});
}
// JWT验证 - Step 4: 被守卫调用
async validate(req: Request, payload: any) {
const originToken = ExtractJwt.fromAuthHeaderAsBearerToken()(req);
// 只有验证通过之后才会来到这里
// console.log(`JWT验证 - Step 4: 被守卫调用`);
const cacheToken = await this.CacheService.get(
`user-token-${payload.sub}-${payload.username}`,
);
//单点登陆验证
if (cacheToken !== JSON.stringify(originToken)) {
throw new UnauthorizedException('您账户已经在另一处登陆,请重新登陆');
}
return {
username: payload.username,
};
}
}
于是这样就完成了✅了! 比较简单哈,需要注意的就是你设置的key的在redis中的过期时间,还有就是我们实际上可以优化这个代码 把redis链接抽离出来,传递class 而不是在用的时候采取new ,这个各位大佬,可以自己去琢磨实现一下,也👏 欢迎在评论区 留言分享你的实现.
如何做微服务?通信架构如何设计?
使用Nestjs我们将会实现一个非常easy的微服务,通信的话可以使用MQ 也可以直接调用,或者其RPC的方案,但是我没有引入其他复杂的东西,只实现了 直接调用 的方式,在后续的文章中我们会深入的学习其他的调用方式
理论知识
- 被调用方需要准备什么?
nest中的微服务需要依赖一个库 @nestjs/microservices,使用它我们可以非常方便的创建微服务应用,现在我们启动另一个项目,(很简单,我们啥也不写就是注册一个服务)注意我们使用最简单的TCP来通信,Nest默认情况下,微服务通过 TCP协议 监听消息。
options的值如下
host | 连接主机名 |
---|---|
port | 连接端口 |
retryAttempts | 连接尝试的总数 |
retryDelay | 连接重试延迟(ms) |
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: '192.168.101.2',
port: 3333,
},
},
);
app.listen();
}
bootstrap();
// 然后我们去它 controller 写点东西
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
// 模式2基于 事件 (不一定要求有返回)
@EventPattern('math:log')
async handleUserCreated(text: Record<string, unknown>) {
// business logic
console.log(text, '基于事件的传输方式');
}
// 模式1基于 请求-响应 (要求一来一返)
@MessagePattern('math:wordcount')
wordCount(text: string): { [key: string]: number } {
return this.appService.calculateWordCount(text);
}
}
- 什么是模式
模式是微服务之间识别消息的方式,它有下面几种 :
- 请求-响应(默认的方式)
- 基于事件的,上面你所看到的代码中农已经有了详细的说明
- 掉用方该如何做才能掉用这个服务呢?
// 同样的在module中注入
@Module({
imports: [
ClientsModule.register([
{
name: 'NEST_MICRO',
transport: Transport.TCP,
options: {
host: '192.168.101.2',
port: 3001,
},
},
]),
-----省略部分代码-----
// 在你将要掉用它的地方 注入它
constructor(
private readonly userService: UserService,
@Inject('NEST_MICRO') private client: ClientProxy,
) {}
// 启用一个微服务
@Post('/math/wordcount')
async wordCount(@Body() { text }: { text: string }) {
// 第一种模式 请求响应
const value2 = await this.client.send('math:wordcount', text).toPromise();
// 第二种模式 事件
await this.client.emit('math:log', text).toPromise();
return value2;
}
- 那么我到底适合哪种模式呢?
一般来说 Kafka
或 NATS
更符合事件模式,(既您只想发布事件而不希望等待的时候⌛️)这种情况下使用事件的方式就很好了(具体的实现 将在后续的文章中揭晓)
实践指南
// 首先在新建一个项目 main中写道 (被掉用方)
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: '192.168.101.2',
port: 3333,
},
},
);
app.listen();
}
bootstrap();
// 在它的controller中写道 (被掉用方)
import { Controller, Get } from '@nestjs/common';
import { EventPattern, MessagePattern } from '@nestjs/microservices';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
// 模式2基于 事件 (不一定要求有返回)
@EventPattern('math:log')
async handleUserCreated(text: Record<string, unknown>) {
// business logic
console.log(text, '基于事件的传输方式');
}
// 模式1基于 请求-响应 (要求一来一返)
@MessagePattern('math:wordcount')
wordCount(text: string): { [key: string]: number } {
return this.appService.calculateWordCount(text);
}
}
// 在我们的掉用方的AppModule全局注入 (掉用方)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import App_globalConfig from './config/configuration';
import DatabaseConfig from './config/database';
import { AppService } from './app.service';
import { ArticleModule } from './modules/article/article.module';
import { TagModule } from './modules/tag/tag.module';
import { UserModule } from './modules/user/user.module';
import { AuthModule } from './modules/auth/auth.module';
import { FilesModule } from './modules/files/files.module';
import { JobModule } from './modules/job/job.module';
import { CacheModule } from './modules/cache/cache.module';
import { ClientsModule, Transport } from '@nestjs/microservices'; // 注册一个用于对微服务进行数据传输的客户端
@Module({
imports: [
ClientsModule.register([ // 同样的你可以使用registrAsync方式读取config配置
{
name: 'NEST_MICRO',
transport: Transport.TCP,
options: {
host: '192.168.101.2',
port: 3001,
},
},
]),
ScheduleModule.forRoot(),
ConfigModule.forRoot({
isGlobal: true,
load: [App_globalConfig, DatabaseConfig],
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
type: 'mysql',
host: configService.get('database.host'),
port: Number(DatabaseConfig().port),
username: DatabaseConfig().username,
password: DatabaseConfig().password,
database: DatabaseConfig().database,
entities: [__dirname + '/**/*.entity{.ts,.js}'], // 扫描本项目中.entity.ts或者.entity.js的文件
synchronize: true,
};
},
inject: [ConfigService],
}),
UserModule,
TagModule,
ArticleModule,
AuthModule,
FilesModule,
JobModule,
CacheModule,
],
providers: [AppService],
})
export class AppModule {}
// 在userModule进行使用
export class UserController {
constructor(
private readonly userService: UserService,
@Inject('NEST_MICRO') private client: ClientProxy,
) {}
// 启用一个微服务
@Post('/math/wordcount')
async wordCount(@Body() { text }: { text: string }) {
const value2 = await this.client.send('math:wordcount', text).toPromise();
await this.client.emit('math:log', text).toPromise();
return value2;
}
Nest到底咋运行的?
如果你拉取我的代码你build之后,然后呢?怎么去部署会运维它呢??🌚🌚 这里我们将会展开讨论到底,嘿嘿这里只是简单的说说,如果大家喜欢看,我后续会完善整个项目
关于build 和运行时
实际上Nest是一个运行时的的框架,它需要和node_module一起start,如果你仅仅是把build的东西就希望它能像前端一样拿出去跑 那就大错特错啦,当然也有不少大神这么干过 ,它们把nest的在build的时候的webpack配置改了,让它去把这些依赖的库全放到build里面去,其实我觉得这样的做法大大的不妥。你可能要处理更多的BUG和不确定,因此我还是推荐大家 不要这么干,老实点把node_module拿出去跑就好了
关于运维
其实我希望描述的是nodejs 在业界的部署方式,拿我司举例(Newegg),我们的部署方式是使用pm2去管理它,同 样的基于pm2的功能我们还完成了自动重启等功能。详情可以去看他们的文档
pm2.fenxianglu.cn/docs/advanc…
k8s和Docker
k8s和Docker的部署比较的简单,我举例子docker的部署吧,在我前面的几篇文章中就有说明,如何使用docker去build一个image,这里不重复的讲了,同样的使用之前文档中的gitlab你完全可以实现自己的工作流。
关于nginx的后端反向代理,
实际上这个在单机中做比较的简单,在docker或者k8s中需要倒腾一下,我这里的方案是在宿主机假设nginx然后映射到容器的prot中去 这里就不过多的介绍了,(nginx使用非常的简单)