nest学习(4)

学习小册(nest通关秘籍)

Nest创建微服务

前面的http服务都是单体架构的,所有业务逻辑都在一个服务实现。
在这里插入图片描述
项目越来越大后,模块越来越多,可以将业务模块拆成单独的微服务。
在这里插入图片描述
微服务之间一般不使用http通信的,因为http会携带大量的header。增大通信开销。一般直接用tcp。

启动一个微服务
在这里插入图片描述
使用TCP通信,然后暴露一个端口

@MessagePattern('sum')
sum(numArr: Array<number>): number {
    return numArr.reduce((total, item) => total + item, 0);
}

提供方法也不是@Post这些了,而是MessagePattern。
这样我们就创建了一个微服务
在这里插入图片描述
再创建一个正常的http服务,然后引用这个微服务。
通过ClientsModule来注入。
在这里插入图片描述
然后直接用就行

@Inject('USER_SERVICE')
private userClient: ClientProxy;

@Get('sum')
calc(@Query('num') str) {
    const numArr = str.split(',').map((item) => parseInt(item));

    return this.userClient.send('sum', numArr);
}

注入的对象就是连接这个微服务的客户端代理。
这样调用sum接口就会调用微服务的sum方法

前面在微服务里是用 @MessagePattern 声明的要处理的消息。
如果并不需要返回消息的话,可以用 @EventPattern 声明:

@EventPattern('log')
log(str: string) {
    console.log(str);
}

通过抓包我们可以得到他们的通信内容
在这里插入图片描述
他们tcp之间通过json消息格式来通信。

Nest的Monorepo和Libarary

Monorepo 是一种项目代码管理方式,指单个仓库中管理多个项目,有助于简化代码共享、版本控制、构建和部署等方面的复杂性,并提供更好的可重用性和协作性。Monorepo 提倡了开放、透明、共享的组织文化,这种方法已经被很多大型公司广泛使用,如 Google、Facebook 和 Microsoft 等。

如果我们每个服务都用一个git仓库,那微服务多了后,维护成本也变高了,此时可以用monorepo模式。
在这里插入图片描述
Nest支持monorepo模式。
在这里插入图片描述
通过nest g app app2即可,然后会创建apps目录,里面放着对应的每个服务代码。

在这里插入图片描述
nest-cli.json保存着每个项目的基本信息。

每个模块还可以有公共模块,nest支持library。
nest g lib lib1
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
这样就可以在其他微服务里面直接应用这个lib。
在这里插入图片描述
在这里插入图片描述

配置中心和注册中心

不同的服务需要使用相同的配置
在这里插入图片描述
所以需要一个专门管理配置信息的服务。

注册中心

服务之间会相互依赖,怎么知道对应的服务挂了吗,或者还有哪些节点可用。
在这里插入图片描述
微服务在启动的时候,向注册中心注册,销毁的时候也在注册中心注销,并通过定时发送心跳包来回报自己的状态。
在查找其他微服务的时候,去注册中心查一下这个服务的所有节点信息,然后再选一个来用,这个叫做服务发现。
在这里插入图片描述
在这里插入图片描述
配置中心和注册中心是必备组件
可以做配置中心、注册中心的中间件还是挺多的,比如 nacos、apollo、etcd 等。

etcd 实现注册中心和配置中心。

etcd是一个kv的存储服务,k8s 就是用它来做的注册中心、配置中心。
用法跟redis类似,但可以监听key的变化。
如下

const { Etcd3 } = require('etcd3');
const client = new Etcd3({
    hosts: 'http://localhost:2379',
    auth: {
        username: 'root',
        password: 'guang'
    }
});

// 保存配置
async function saveConfig(key, value) {
    await client.put(key).value(value);
}

// 读取配置
async function getConfig(key) {
    return await client.get(key).string();
}

// 删除配置
async function deleteConfig(key) {
    await client.delete().key(key);
}
   
// 服务注册
async function registerService(serviceName, instanceId, metadata) {
    const key = `/services/${serviceName}/${instanceId}`;
    const lease = client.lease(10);
    await lease.put(key).value(JSON.stringify(metadata));
    lease.on('lost', async () => {
        console.log('租约过期,重新注册...');
        await registerService(serviceName, instanceId, metadata);
    });
}

// 服务发现
async function discoverService(serviceName) {
    const instances = await client.getAll().prefix(`/services/${serviceName}`).strings();
    return Object.entries(instances).map(([key, value]) => JSON.parse(value));
}

// 监听服务变更
async function watchService(serviceName, callback) {
    const watcher = await client.watch().prefix(`/services/${serviceName}`).create();
    watcher.on('put', async event => {
        console.log('新的服务节点添加:', event.key.toString());
        callback(await discoverService(serviceName));
    }).on('delete', async event => {
        console.log('服务节点删除:', event.key.toString());
        callback(await discoverService(serviceName));
    });
}

// (async function main() {
//     await saveConfig('config-key', 'config-value');
//     const configValue = await getConfig('config-key');
//     console.log('Config value:', configValue);
// })();

(async function main() {
    const serviceName = 'my_service';
    
    await registerService(serviceName, 'instance_1', { host: 'localhost', port:3000 });
    await registerService(serviceName, 'instance_2', { host: 'localhost', port:3002 });

    const instances = await discoverService(serviceName);
    console.log('所有服务节点:', instances);

    watchService(serviceName, updatedInstances => {
        console.log('服务节点有变动:', updatedInstances);
    });
})();

  • 不同服务的配置需要统一管理,并且在更新后通知所有的服务,所以需要配置中心。
  • 微服务的节点可能动态的增加或者删除,依赖他的服务在调用之前需要知道有哪些实例可用,所以需要注册中心。
  • 服务启动的时候注册到注册中心,并定时续租期,调用别的服务的时候,可以查一下有哪些服务实例可用,也就是服务注册、服务发现功能。
集成到Nest
  • 先写成一个provider
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Etcd3 } from 'etcd3';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: 'ETCD_CLIENT',
      useFactory() {
        const client = new Etcd3({
            hosts: 'http://localhost:2379',
            auth: {
                username: 'root',
                password: 'guang'
            }
        });
        return client;
      }
    }
  ],
})
export class AppModule {}

这样就可以直接注入该provider。

封装动态模块
import { DynamicModule, Module, ModuleMetadata, Type } from '@nestjs/common';
import { EtcdService } from './etcd.service';
import { Etcd3, IOptions } from 'etcd3';

export const ETCD_CLIENT_TOKEN = 'ETCD_CLIENT';

export const ETCD_CLIENT_OPTIONS_TOKEN = 'ETCD_CLIENT_OPTIONS';

@Module({})
export class EtcdModule {

  static forRoot(options?: IOptions): DynamicModule {
    return {
      module: EtcdModule,
      providers: [
        EtcdService,
        {
          provide: ETCD_CLIENT_TOKEN,
          useFactory(options: IOptions) {
            const client = new Etcd3(options);
            return client;
          },
          inject: [ETCD_CLIENT_OPTIONS_TOKEN]
        },
        {
          provide: ETCD_CLIENT_OPTIONS_TOKEN,
          useValue: options
        }
      ],
      exports: [
        EtcdService
      ]
    };
  }
}

通过动态模块,将options通过forRoot动态传入。然后编写etcdSerivce,其他地方用的时候直接注入该动态模块调用EtcdService的方法就行。
在这里插入图片描述

基于gRPC实现跨语言的微服务通信

多语言实现的微服务之间如何通信呢,http的话是文本传输,效率低。跨语言调用服务一般会用gRPC。
在这里插入图片描述
将server改造成grpc的微服务

import { NestFactory } from '@nestjs/core';
import { GrpcOptions, Transport } from '@nestjs/microservices';
import { GrpcServerModule } from './grpc-server.module';
import { join } from 'path';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<GrpcOptions>(GrpcServerModule, {
    transport: Transport.GRPC,
    options: {
      url: 'localhost:8888',
      package: 'book',
      protoPath: join(__dirname, 'book/book.proto'),
    },
  });

  await app.listen();
}
bootstrap();

tpc改为GRPC。
在指定位置场景book.proto文件。
src/book/book.proto

// 版本语法
syntax = "proto3";

// 包名称
package book;

// 提供的服务方法
service BookService {
  rpc FindBook (BookById) returns (Book) {}
}

// 定义参数BookById的格式
message BookById {
  int32 id = 1;
}

// 定义返回的Book的格式
message Book {
  int32 id = 1;
  string name = 2;
  string desc = 3;
}

这是protocol buffer 的语法,因为要跨语言通信,不同语言语法不同,所以需要一个通用的通信语言。

这里book.proto只是定义了格式,具体实现需要在controller中实现。

@GrpcMethod('BookService', 'FindBook')
findBook(data: { id: number}) {
    const items = [
      { id: 1, name: '前端调试通关秘籍', desc: '网页和 node 调试' },
      { id: 2, name: 'Nest 通关秘籍', desc: 'Nest 和各种后端中间件' },
    ];
    return items.find(({ id }) => id === data.id);
}

通过@GrpcMethod标识为grpcde的远程盗用方法。
并在nest-cli.json中添加assets配置,build的时候可以复制到disst目录下。
在这里插入图片描述
然后在另一个服务可以联该grpc服务了。

在这里插入图片描述
先在module中import进来。
同样调用方也是需要book.proto文件的,不然不知道怎么解析协议数据。文件内容跟grpc服务保持一致即可。

然后就可以在controller里面去掉用了
在这里插入图片描述
通过 @Inject注入,在模块初始化的时候,拿到BookService实例。然后就可以调用他的方法了。
在这里插入图片描述
这就是基于grpc的远程方法调用。

通过 protocol buffer 的语法定义通信数据的格式,比如 package、service 等。
然后再server端实现方法,在client端调用该方法。

java里面是,安装这两个依赖
在这里插入图片描述
定义同样的proto文件。
在这里插入图片描述
然后创建对应的service即可。
在这里插入图片描述
在client端调用该java服务的时候,跟调用nest服务是一样的。

小结
  • 不同语言服务可以用grpc来实现互相调用,而不是http。
  • 实现方式是protocol buffer语法来定义通信数据格式,定义package和service。
  • 然后在server端实现方法,client通过注入拿到实例,直接调用。
Prisma

Typeorm是一个传统的ORM框架,表映射到entity类,把表的关联映射成enttiy类的属性关联。
在这里插入图片描述
完成entity和表的映射之后,只需要调用userRepositor和postRepository的find,delete,save等api,typeorm会自动生成对应的sql语句并执行。

这就是ORM (Object Relational Mapping),对象和关系型数据库映射。

而Prisma不是这样的。他没有entity类的存在。

用法
定义model,
在这里插入图片描述

在这里插入图片描述

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient();

async function test1() {
    await prisma.user.create({
        data: {
            name: 'test',
            email: '111@tesst.com'
        }
    });

    await prisma.user.create({
        data: {
            name: 'test',
            email: '222@test.com'
        }
    });

    const users = await prisma.user.findMany();
    console.log(users);
}

test1();

创建 PrismaClient,用 create 方法创建了 2 个 user,然后查询出来

其他命令

  • init:创建 schema 文件
  • generate: 根据 shcema 文件生成 client 代码
  • db:同步数据库和 schema
  • migrate:生成数据表结构更新的 sql 文件
  • studio:用于 CRUD 的图形化界面
  • validate:检查 schema 文件的语法错误
  • format:格式化 schema 文件
  • version:版本信息

model 部分定义和数据库表的对应关系:
@id 定义主键
@default 定义默认值
@map 定义字段在数据库中的名字
@db.xx 定义对应的具体类型
@updatedAt 定义更新时间的列
@unique 添加唯一约束
@relation 定义外键引用
@@map 定义表在数据库中的名字
@@index 定义索引
@@id 定义联合主键

model Department {
  id        Int    @id @default(autoincrement())
  name      String  @db.VarChar(20)
  createTime DateTime @default(now())
  updateTime DateTime @updatedAt
  employees     Employee[]
}

model Employee {
  id         Int       @id @default(autoincrement())
  name      String     @db.VarChar(20)
  phone     String     @db.VarChar(30)

  deaprtmentId Int
  department     Department      @relation(fields: [deaprtmentId], references: [id])
}

创建时间我们使用 @default(now()) 的方式指定,这样插入数据的时候会自动填入当前时间。

更新时间使用 @updatedAt,会自动设置当前时间。

员工和部门是多对一关系,在员工那一侧添加一个 departmentId 的列,然后通过 @relation 声明 deaprtmentId 的列引用 department 的 id 列。

CRUD api

create、crateMany、update、updateMany、delete、deleteMany、findMany、findFirst、findFirstOrThrow、findUnique、findUniqueOrThrow。

统计相关: count、aggregate、groupBy


// 返回的最大值、最小值、计数、平均值
async function test12() {
    const res = await prisma.aaa.aggregate({
        where: {
            email: {
                contains: 'xx.com'
            }
        },
        _count: {
            _all: true,
        },
        _max: {
            age: true
        },
        _min: {
            age: true
        },
        _avg: {
            age: true
        }
    });
    console.log(res);
}

// 按照 email 分组,过滤出平均年龄大于 2 的分组,计算年龄总和返回。
async function test13() {
    const res = await prisma.aaa.groupBy({
        by: ['email'],
        _count: {
          _all: true
        },
        _sum: {
          age: true,
        },
        having: {
          age: {
            _avg: {
              gt: 2,
            }
          },
        },
    })
    console.log(res);
}

Nest集成prisma

先primsa init生成prisma文件
在这里插入图片描述

创建对应model

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model Department {
  id        Int    @id @default(autoincrement())
  name      String  @db.VarChar(20)
  createTime DateTime @default(now())
  updateTime DateTime @updatedAt
  employees     Employee[]
}

model Employee {
  id         Int       @id @default(autoincrement())
  name      String     @db.VarChar(20)
  phone     String     @db.VarChar(30)  

  deaprtmentId Int
  department     Department      @relation(fields: [deaprtmentId], references: [id])
}

npx prisma migrate reset npx prisma migrate dev --name initrest后直接初始化,这样数据库就会有两张表。

创建一个PrimsaService,方便调用

import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {

    constructor() {
        super({
            log: [
                {
                    emit: 'stdout',
                    level: 'query'
                }
            ]
        })
    }

    async onModuleInit() {
        await this.$connect();
    }
}

使用直接注入该Service即可

import { Inject, Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { Prisma } from '@prisma/client';

@Injectable()
export class DepartmentService {

    @Inject(PrismaService)
    private prisma: PrismaService;

    async create(data: Prisma.DepartmentCreateInput) {
        return await this.prisma.department.create({
            data,
            select: {
                id: true
            }
        });
    }
}

这样就可以在nest里面使用Prisma的CRUD了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

coderlin_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值