本文已参与「新人创作礼」活动,一起开启掘金创作之路。
背景
最近项目需要到一个基于事件驱动的一些业务模块,就封装一下类似@nestjs/bull一样的API去进行事件注册和触发。
通过Nest Core的功能去完成全局扫描metadata
通过@nestjs/core,我们可以找到相应框架实现的功能和工具,去完成我们需要写业务逻辑前的工作
DiscoveryService —— 扫描所有app module注册到的controllers和services
首先我们需要扫描所有的controller和service,通过DiscoveryService可以实现,伪代码如下:
// explorer.service.ts
import { Injectable } from '@nestjs/common';
import { DiscoveryService} from '@nestjs/core';
@Injectable()
export class AccessorService {
constructor(
private readonly discoveryService: DiscoveryService
) {}
run() {
const providers = this.discoveryService.getProviders();
const controllers = this.discoveryService.getControllers();
// ...
}
}
那么调用这些方法返回的是什么?从类型定义上看,是一个InstanceWrapper,主要是在nestjs对controller和service注入到module的时候,对class实例后的对象封装了一层wrapper
MetadataScanner —— 扫描出有Metadata的类方法
然后再去遍历我们定义的decorator和metadata,这时候就需要MetadataScanner
// explorer.service.ts
import { Injectable } from '@nestjs/common';
import { DiscoveryService} from '@nestjs/core';
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
@Injectable()
export class AccessorService {
constructor(
private readonly discoveryService: DiscoveryService,
private readonly metadataScanner: MetadataScanner,
) {}
run() {
const providers = this.discoveryService.getProviders();
const controllers = this.discoveryService.getControllers();
[...providers, ...controllers]
.filter(wrapper => wrapper.isDependencyTreeStatic())
.filter(wrapper => wrapper.instance)
.forEach((wrapper: InstanceWrapper) => {
const { instance } = wrapper;
const prototype = Object.getPrototypeOf(instance);
this.metadataScanner.scanFromPrototype(
instance,
prototype,
(methodKey: string) => {
console.log(instance, methodKey)
// do something with class method with custom decorator...
}
);
});
}
}
我的理解是:
wrapper.isDependencyTreeStatic()方法是用于判断实例是否在注册依赖的时候初始化的wrapper.instance就是wrapper实际封装起来的的class实例metadataScanner提供了scanFromPrototype方法去扫描class里面方法(method)的decorator里面的metadata
获取metadata
我看的开源项目里面,一般都会建一个AccessorService去读取相应的metadata,而metadata key一般就是放个常量文件啦
// constants.ts
export const MY_PROCESSOR = Symbol('MY_PROCESSOR');
export const MY_PROCESS = Symbol('MY_PROCESS');
// accessor.service.ts
/* eslint-disable @typescript-eslint/ban-types */
import { Injectable, Type } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { MY_PROCESSOR, MY_PROCESS } from './constants';
@Injectable()
export class AccessorService {
constructor(private readonly reflector: Reflector) {}
isProcess(target: Type<any> | Function): boolean {
if (!target) {
return false;
}
return !!this.reflector.get(MY_PROCESS, target);
}
getProcessMetadata(target: Type<any> | Function): string | undefined {
return this.reflector.get(MY_PROCESS, target);
}
isProcessor(target: Type<any> | Function): boolean {
if (!target) {
return false;
}
return !!this.reflector.get(MY_PROCESSOR, target);
}
getProcessorMetadata(target: Type<any> | Function): string | undefined {
return this.reflector.get(MY_PROCESSOR, target);
}
}
开始写业务
// explorer.service.ts
// 获取class装饰器metadata
private loadProcessor(instance: Record<string, any>) {
if (this.metadataAccessor.isProcessor(instance.constructor)) {
const metadata = this.metadataAccessor.getProcessorMetadata(
instance.constructor,
);
// do something
this.logger.log(`Load Processor with ${JSON.stringify(metadata)}`);
}
}
// 获取class method装饰器metadata
private loadProcess(instance: Record<string, any>, methodKey: string) {
if (this.metadataAccessor.isProcess(instance[methodKey])) {
const metadata = this.metadataAccessor.getProcessMetadata(
instance[methodKey],
);
// do something
this.logger.log(`Load Process with ${JSON.stringify(metadata)}`);
}
}
到这里已经完成了大部分工作,接下来还需要implements OnApplicationBootstrap和OnApplicationShutdown完成在service生命周期过程的调用和回收资源,完整的service文件就变成这样
// explorer.service.ts
import {
Injectable,
Logger,
OnApplicationBootstrap,
OnApplicationShutdown,
} from '@nestjs/common';
import { DiscoveryService } from '@nestjs/core';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import { AccessorService } from './accessor.service';
@Injectable()
export class ExplorerService
implements OnApplicationBootstrap, OnApplicationShutdown
{
private readonly logger = new Logger(ExplorerService.name);
constructor(
private readonly discoveryService: DiscoveryService,
private readonly metadataScanner: MetadataScanner,
private readonly metadataAccessor: AccessorService,
) {}
onApplicationShutdown(signal?: string) {
this.logger.log('app has been shutdown.');
}
onApplicationBootstrap() {
this.explore();
}
explore() {
const providers = this.discoveryService.getProviders();
const controllers = this.discoveryService.getControllers();
[...providers, ...controllers]
.filter((wrapper) => wrapper.isDependencyTreeStatic())
.filter((wrapper) => wrapper.instance)
.forEach((wrapper: InstanceWrapper) => {
const { instance } = wrapper;
// 类实例对象的metadata去做事情
this.loadProcessor(instance);
const prototype = Object.getPrototypeOf(instance);
this.metadataScanner.scanFromPrototype(
instance,
prototype,
(methodKey: string) => {
// 类实例对象的metadata去做事情
this.loadProcess(instance, methodKey);
},
);
});
}
private loadProcessor(instance: Record<string, any>) {
if (this.metadataAccessor.isProcessor(instance.constructor)) {
const metadata = this.metadataAccessor.getProcessorMetadata(
instance.constructor,
);
// do something
this.logger.log(`Load Processor with ${JSON.stringify(metadata)}`);
}
}
private loadProcess(instance: Record<string, any>, methodKey: string) {
if (this.metadataAccessor.isProcess(instance[methodKey])) {
const metadata = this.metadataAccessor.getProcessMetadata(
instance[methodKey],
);
// do something
this.logger.log(`Load Process with ${JSON.stringify(metadata)}`);
}
}
}
最后你就可以写你自己的decorator去定义你需要的metadata,就可以完成你的业务逻辑咯。例如
// processor.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { MY_PROCESSOR, MY_PROCESS } from './constants';
export function Process(opts: { event: string }): MethodDecorator {
return SetMetadata(MY_PROCESS , opts); // NOTE: no options for now
}
export function Processor(opts: { namespace: string }): ClassDecorator {
return SetMetadata(MY_PROCESSOR, opts);
}
// example.service.ts
import { Injectable } from '@nestjs/common';
import { Processor, Process } from './test.decorator.ts';
@Injectable()
@Processor({namespace:'job'})
export class TaskService {
@Process({ event: 'test' })
async run() {
console.log('do somthing');
}
}
有兴趣的话可以去看看我写的template git repository,或者看看@nestjs/event-emitter和@nestjs/bull的源码
reference: