likes
comments
collection
share

这可能是你看过最全的 「NestJS」 - 【一期工程收尾】

作者站长头像
站长
· 阅读数 11

前置说明

本文是 为了完善 和修复原来的一些坑而来的,Nest这个系列,会一直更新下去不会让各位掘友失望,也不会烂尾;这期我们来完善一下项目结构,让这个东西更贴近生产标准,还有就是 回答一下 评论区的一些问题

Github 地址

本次仓库更新一览

  • 聚合一些业务无关的通用模块
  • 替换掉原来的redis (之前的redis 是一个不时尚的写法)
  • 加入 zk 简化启动和代码中写死的配置
  • 回答 issues 和 掘友的一些问题

聚合 一些通用的模块

我们把一些 通用的模块,与业务无关的模块给聚合起来 到core中,方便以后的复用逻辑

这可能是你看过最全的 「NestJS」 - 【一期工程收尾】


@Module({
  imports: [
    CoreModule,
    //下面的应该仅包含业务模块 不包含 通用模块,通用模块聚合 在core中
    UserModule,
    TagModule,
    ArticleModule,
  ],
  providers: [
    {
      provide: APP_GUARD,
      useClass: MyAuthGuard,
    },
  ],
})
export class AppModule {}

突然之间,整个项目都变得简单了起来

ZK

zk是什么,它是一个分布式的管理工具 Zookeeper: 是一个分布式的、开源的程序协调服务,是 hadoop 项目下的一个子项目。他提供的主要功 能包括:配置管理、名字服务、分布式锁、集群管理。我们这暂时用到了它 来做我们的config 配置中心,后续我们的服务发现 名字服务等,都将由它来处理,请关注后续的内容

关于ZK的实现,请看代码具体的例子,整个核心就是Nest的 动态模块 的动态注入! 相关的文章,我前文有分享,请自己查看, 下面放一些核心代码, NEST中非常重要的东西之一 就是 动态模块 DynamicModule!

管理zk上的配置,我这里用了一个软件来管理它

这可能是你看过最全的 「NestJS」 - 【一期工程收尾】

// 设计一个全局的modle 然后别的地方再饮用 里面的service 的时候就不需要在自己的module 去导入了
// 官方文档有写

// 由于 node-zk 是异步 所以我也使用 异步 模块来实现

@Global()
@Module({
  providers: [
    ZKService,
    {
      provide: ZOOKEEPER_CLIENT,
      useFactory: async (optionsProvider: ConfigService) => {
        const zookeeperClient = await getZKClient({
          url: optionsProvider.get(EnumInterInitConfigKey.zkHost),
        });
        return zookeeperClient;
      },
      inject: [ConfigService],
    },
  ],
  exports: [ZKService],
})
//.....
 const getClient = async (options: any) => {
        const {url, ...opt} = options;

        const client = zookeeper.createClient(url, opt);

        client.once('connected', () => {
          Logger.log('zk connected success...');
        });

        client.on('state', (state: string) => {
          const sessionId = client.getSessionId().toString('hex');
        });

        client.connect();
        return client;
      };

// 这样写的话,用的时候直接丢就好啦
@Global()
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      expandVariables: true,
      load: [configuration],
    }),
    FaqModule,
    ZKModule.forRootAsync(),
  ],
  exports: [FaqModule],
})
export class CoreModule {}

// 只需要在用的地方 直接用ZkServic 就ok ,但是这样不够优雅,于是进化成啦🧬 这样 把它内聚一下

@Global()
@Module({
  providers: [ZKService],
  exports: [ZKService],
})
export class ZKModule implements OnModuleDestroy {
  static forRootAsync(): DynamicModule {
    return {
      module: ZKModule,
      imports: [],
      providers: [ZKService, this.createClient()],
    };
  }
  constructor(
    @Inject(ZOOKEEPER_CLIENT)
    private readonly zkCk: any,
  ) {}

  onModuleDestroy() {
    this.zkCk.close();
  }

  private static createClient = (): Provider => ({
    provide: ZOOKEEPER_CLIENT,
    useFactory: async (optionsProvider: ConfigService) => {
      const getClient = async (options: any) => {
        const {url, ...opt} = options;

        const client = zookeeper.createClient(url, opt);

        client.once('connected', () => {
          Logger.log('zk connected success...');
        });

        client.on('state', (state: string) => {
          const sessionId = client.getSessionId().toString('hex');
        });

        client.connect();
        return client;
      };

      const zookeeperClient = await getClient({
        url: optionsProvider.get(EnumInterInitConfigKey.zkHost),
      });

      return zookeeperClient;
    },
    inject: [ConfigService],
  });
}

// 用的地方就变成啦 forootAsync 
 ZKModule.forRootAsync(),

// 其他的redis 还是mq 都可以这样用 都是这样用的
import { ClusterModule, ClusterModuleOptions } from '@liaoliaots/nestjs-redis';
.....
  ClusterModule.forRootAsync({
      imports: [ConfigModule, ZooKeeperModule],
      inject: [MpsConfigService, MpsZKService],
      useFactory: async (
        configService: MpsConfigService,
        zkService: MpsZKService,
      ): Promise<ClusterModuleOptions> => {
        const data = JSON.parse(
          (await zkService.getData(configService.zooKeeperRedisNode)) as string,
        ) as ZooKeeperRedisResponse[];
        const cluster =
          process.env.K0S_CLUSTER_LOCATION ||
          configService.redisClusterLocationDefault;
        const { Host, Password } = data.find((_) => _.Name === cluster)!;
        const nodes = Host.split(',').map((_) => ({
          host: _.split(':')[0],
          port: Number(_.split(':')[1]),
        }));

        return {
          readyLog: true,
          config: {
            nodes,
            redisOptions: { password: Password },
            onClientCreated(client) {
              client.on('error', (err) => {
                Logger.error(err, 'CoreModule');
              });
              client.on('ready', () => {
                Logger.log('Connected to Redis.', 'CoreModule');
              });
            },
          },
        };
      },
    }),
  // 用的时候直接引入 第三方写的 
  import { ClusterService } from '@liaoliaots/nestjs-redis'; 就ok ,它会自动租册

// 比如说mq是没有的,怎么办?也很简单,使用动态模块 的特性 自己搞一个就好了
    MessageQueueModule.forRootAsync(), 具体代码就不写啦
      

最终的配置 (这东西不会写在代码里,只会放回zk服务器上去)

  {
   "mysql":{
      "host":"localhost",
      "prot":3306,
      "name":"root",
      "pwd":"123456",
      "lib":"node_blog"
   },
   "redis":{
      "host":"localhost",
      "prot":6379,
      "pwd":"",
      "family":4,
      "db":0
   },
   "uploads":{
      "prefix":0,
      "dir":"uploads"
   },
   "auth": {
     "secret": "shinobi7414"
   }
}

redis 替换

直接替换成 第三方封装好的zk ,当然你当然可以自己封装, 实际上,如何 @@liaoliaots/nestjs-redis 的源代码也是使用啦 动态模块,我这里图方便 就直接用了,毕竟它成熟哈哈哈,但是原理和源码如何设计的 我希望你明白

// 第三方库非常 方便的提供了这样的东西,但它的底层依然是 基于动态模块去做的 ,再说一遍动态模块非常的重要‼️
    RedisModule.forRootAsync({
      useFactory: async (zkService: ZKService) => {
        const { redis } = await zkService.getDataWithJSON<InterZKConfigNest>(
          EnumZkConfigPath.nest,
        );
        return {
          config: {
            host: redis.host,
            port: redis.prot,
            db: redis.db,
            family: redis.family,
            password: redis.pwd,
          },
        };
      },
      inject: [ZKService],
    }),

去掉微服务

我们目前还不需要,去掉

回答 issues 和 掘友的一些问题

  1. 程序中的jwt 验证到底如何设计?

那么首先,我们来简单的说两个概念,一个 是 jwt授权 另一个是 角色权限role认证

  • jwt(授权) 是对外部请求的 检查,看看它是否能够使用我们的这些服务

  • role(认证) 是基于角色 🎭 进行的细粒度 控制,比如 某x 有某权限,某x 没有访问x的权限,但是可以访问其他。

    这一点,实际上也比较的简单,就是设计一个 守卫 guard 就好啦,但是这就涉及到,改动我们的数据库设计..... 比较烦炸,放到第二期去做吧,我在前面的文章中也提到过这个话题,感兴趣的可以去翻一下

有的时候 我们基本上全局都是 需要jwt的,但是比如注册,这个一定是不需要的,你注册都没有注册,哪有什么JWT给你,于是我们这样做 全局设置

我们不需要 passport 的local 策略 目前而言 "passport" 有点 “特性” 😂 无法解决....


// 元数据装饰器
export const NotAuth = () => SetMetadata('no-auth', true);

//  自己定义一个 Guard 扩展它的逻辑
@Injectable()
export class MyAuthGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    //    在这里取metadata中的no-auth,得到的会是一个bool
    const noAuth = this.reflector.get<boolean>('no-auth', context.getHandler());

    const guard = MyAuthGuard.getAuthGuard(noAuth);
    if (noAuth) {
      return true;
    } else {
      return guard.canActivate(context); //    执行所选策略Guard的canActivate方法
    }
  }

  //    根据NoAuth的t/f选择合适的策略Guard
  private static getAuthGuard(noAuth: boolean): IAuthGuard {
    return new (AuthGuard('jwt'))();
  }
}

// 全局提供
@Module({
  imports: [
    CoreModule,
    //下面的应该仅包含业务模块 不包含 通用模块,通用模块聚合 在core中
    UserModule,
    TagModule,
    ArticleModule,
  ],
  providers: [
    {
      provide: APP_GUARD,
      useClass: MyAuthGuard,
    },
  ],
})

// 使用它

  @NotAuth()
  @Get('noAuth')
  noAuth() {
    return '66';
  }

基于此 我们需要重新设计 和实现我们的系统,

  1. Issues中的node 17 yarn 的时候error

    对此我深感抱歉我的系统并没能发现有什么问题,bcrypt 模块如果有install的异常,多半是网络问题和系统需要预先装什么东西,在之前的版本是这样的,现在你或许可以换一个更好的网络来尝试

  2. 服务发现

这个需要结合ZK来做,放到第二期

  1. 工程结构

工程目录发生了调整,具体请看 仓库