Dynamisk registrering av providers med DiscoveryService
Skanna och koppla providers vid körning med DiscoveryService och MetadataScanner för pluginsystem.
Dynamisk registrering av providers med DiscoveryService är en gratis lektion i NestJS: backend-API:er för företag på CoddyKit. Detta är lektion 2 av 4. Ni kan läsa hela lektionen gratis nedan och sedan öva praktiskt i webbläsaren med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt. Den ingår i lärvägen för NestJS: backend-API:er för företag, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i NestJS: backend-API:er för företag innehåller totalt 4 lektioner.
Problemet med plugin-koppling
I en plugin-arkitektur vet du inte vid kompilering vilka hanterare, strategier eller adaptrar som kommer att finnas. En hexagonal kärna definierar portar; plugins tillhandahåller adaptrar. Utmaningen är: hur hittar och kopplar ramverket dessa adaptrar utan att du registrerar var och en manuellt?
- Hårdkodade arrayer i en modul är sköra — varje nytt plugin kräver ändringar i kärnkoden.
- Du vill att providers ska deklarera sin roll själva via dekoratorer och sedan upptäckas vid körning.
NestJS tillhandahåller DiscoveryService och MetadataScanner från @nestjs/core för just detta. De låter dig skanna den aktiva DI-containern och reagera på metadata.
Märk providers med en dekorator
Mönstret börjar med en anpassad dekorator som stämplar metadata på en klass. Vi använder SetMetadata (eller Reflector.createDecorator) så att skannern senare kan filtrera på den.
Här markerar en @Plugin()-dekorator en klass som ett upptäckbart plugin och innehåller ett name som registret kan använda som nyckel.
import { SetMetadata } from '@nestjs/common';
export const PLUGIN_KEY = 'app:plugin';
export interface PluginMeta {
name: string;
}
export const Plugin = (meta: PluginMeta): ClassDecorator =>
SetMetadata(PLUGIN_KEY, meta);
// A plugin author writes:
@Plugin({ name: 'csv-exporter' })
export class CsvExporter {
export(rows: unknown[]): string {
return rows.map((r) => JSON.stringify(r)).join('\n');
}
}Vad DiscoveryService ger dig
DiscoveryService exporteras av DiscoveryModule. Injicera den så får du två viktiga metoder:
getProviders()— en wrapper för varje provider-instans i applikationscontainern.getControllers()— en wrapper för varje controller.
Varje objekt är en InstanceWrapper med .instance (det aktiva objektet), .metatype (klassen) och .name. Du filtrerar dessa wrappers genom att läsa metadata från metatype med en Reflector.
import { Module } from '@nestjs/common';
import { DiscoveryModule } from '@nestjs/core';
import { PluginRegistry } from './plugin.registry';
@Module({
imports: [DiscoveryModule], // exposes DiscoveryService + MetadataScanner
providers: [PluginRegistry],
exports: [PluginRegistry],
})
export class PluginCoreModule {}Skanna providers vid uppstart
Kör upptäckten efter att containern har byggts färdigt. Implementera OnModuleInit (eller OnApplicationBootstrap om du behöver att alla moduler är klara). Filtrera wrappers vars metatype innehåller metadata med din PLUGIN_KEY.
Skydda dig mot null-wrappers: vissa poster (value providers och placeholders med request-scope) saknar metatype eller instance.
import { Injectable, OnModuleInit } from '@nestjs/common';
import { DiscoveryService, Reflector } from '@nestjs/core';
import { PLUGIN_KEY, PluginMeta } from './plugin.decorator';
@Injectable()
export class PluginRegistry implements OnModuleInit {
private readonly plugins = new Map<string, object>();
constructor(
private readonly discovery: DiscoveryService,
private readonly reflector: Reflector,
) {}
onModuleInit(): void {
for (const wrapper of this.discovery.getProviders()) {
const { instance, metatype } = wrapper;
if (!instance || !metatype) continue;
const meta = this.reflector.get<PluginMeta>(PLUGIN_KEY, metatype);
if (!meta) continue;
this.plugins.set(meta.name, instance);
}
}
get(name: string): object | undefined {
return this.plugins.get(name);
}
}MetadataScanner för hookar på metodnivå
Ibland är plugin-punkten inte klassen utan en metod — till exempel @EventHandler('order.created') på enskilda metoder. MetadataScanner går igenom varje metod i instansens prototyp så att du kan läsa metadata per metod.
Använd getAllMethodNames(prototype) (det moderna API:t) och granska varje hanterare med Reflector.
import { Injectable, OnModuleInit } from '@nestjs/common';
import { DiscoveryService, MetadataScanner, Reflector } from '@nestjs/core';
export const EVENT_KEY = 'app:event';
@Injectable()
export class EventBinder implements OnModuleInit {
constructor(
private readonly discovery: DiscoveryService,
private readonly scanner: MetadataScanner,
private readonly reflector: Reflector,
) {}
onModuleInit(): void {
for (const w of this.discovery.getProviders()) {
if (!w.instance || !w.metatype) continue;
const proto = Object.getPrototypeOf(w.instance);
for (const method of this.scanner.getAllMethodNames(proto)) {
const event = this.reflector.get<string>(EVENT_KEY, proto[method]);
if (event) this.bind(event, w.instance, method);
}
}
}
private bind(event: string, target: object, method: string): void {
// register target[method] as a listener for `event`
}
}Dekoratorn på metodnivå
Kombinera bindaren med en metoddekorator. Observera att det är en MethodDecorator — SetMetadata fäster värdet på metodens descriptor.value, vilket är exakt det som reflector.get(EVENT_KEY, proto[method]) läser.
Detta håller kopplingen deklarativ: en plugin-författare lägger till en annotation och kärnan kopplar den — inga manuella anrop till emitter.on(...).
import { SetMetadata } from '@nestjs/common';
import { EVENT_KEY } from './event.binder';
export const OnEvent = (event: string): MethodDecorator =>
SetMetadata(EVENT_KEY, event);
@Injectable()
export class InventoryPlugin {
@OnEvent('order.created')
reserveStock(payload: { orderId: string }): void {
// adjust stock for payload.orderId
}
@OnEvent('order.cancelled')
releaseStock(payload: { orderId: string }): void {
// restore stock
}
}Modellera upptäckt i vanlig TypeScript
Om vi bortser från NestJS är grundidén enkel: ett register mappar en nyckel till en instans som upptäcks i en lista och dirigerar sedan anrop utifrån nyckeln. Den här fristående modellen fångar registersemantiken som du kopplar till DiscoveryService.
Exakt samma Map-baserade uppslagning driver det riktiga registret — det är bara källan till instanserna som skiljer sig.
interface Exporter {
readonly name: string;
export(rows: object[]): string;
}
class CsvExporter implements Exporter {
name = 'csv';
export(rows: object[]): string {
return rows.map((r) => Object.values(r).join(',')).join('\n');
}
}
class JsonExporter implements Exporter {
name = 'json';
export(rows: object[]): string {
return JSON.stringify(rows);
}
}
class Registry {
private map = new Map<string, Exporter>();
register(...plugins: Exporter[]): void {
for (const p of plugins) this.map.set(p.name, p);
}
run(name: string, rows: object[]): string {
const p = this.map.get(name);
if (!p) throw new Error('Unknown exporter: ' + name);
return p.export(rows);
}
}
const reg = new Registry();
reg.register(new CsvExporter(), new JsonExporter());
const data = [{ id: 1, sku: 'A' }, { id: 2, sku: 'B' }];
console.log(reg.run('csv', data));
console.log(reg.run('json', data));Tidpunkt för upptäckt och livscykel
Tidpunkten spelar roll. DI-containern är endast komplett under vissa livscykelfaser:
onModuleInit— körs per modul efter att dess providers har lösts. Det fungerar om alla plugins finns i samma modul.onApplicationBootstrap— körs en gång efter att alla moduler har initierats. Det är säkrast vid plugin-skanning över flera moduler.
Om du skannar för tidigt får du en tom eller ofullständig providerlista. Föredra onApplicationBootstrap när plugins kan levereras i feature-moduler som laddas senare.
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
import { DiscoveryService, Reflector } from '@nestjs/core';
import { PLUGIN_KEY, PluginMeta } from './plugin.decorator';
@Injectable()
export class PluginRegistry implements OnApplicationBootstrap {
private readonly plugins = new Map<string, object>();
constructor(
private readonly discovery: DiscoveryService,
private readonly reflector: Reflector,
) {}
onApplicationBootstrap(): void {
const found = this.discovery
.getProviders()
.filter((w) => w.instance && w.metatype)
.map((w) => ({
meta: this.reflector.get<PluginMeta>(PLUGIN_KEY, w.metatype!),
instance: w.instance,
}))
.filter((x) => x.meta);
for (const { meta, instance } of found) {
this.plugins.set(meta!.name, instance);
}
}
}Scope-fällor: REQUEST och TRANSIENT
Upptäckt fungerar smidigt för singletoner. Var försiktig med andra scopes:
- Providers med
Scope.REQUEST/Scope.TRANSIENTkan hawrapper.instance === nullvid uppstart — det finns ingen enskild instans att cacha. - En upptäckt request-scope-instans skulle bli inaktuell och läcka tillstånd mellan förfrågningar om du cachar den.
En bra tumregel är: håll plugins i singleton-scope. Om ett plugin verkligen behöver data från en förfrågan, upptäcker du klassen och löser en ny instans per förfrågan via ModuleRef.resolve() i stället för att cacha instansen.
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
@Injectable()
export class ScopedPluginInvoker {
constructor(private readonly moduleRef: ModuleRef) {}
// metatype was discovered earlier; resolve fresh per request
async invoke<T>(metatype: new (...a: any[]) => T): Promise<T> {
return this.moduleRef.resolve(metatype, undefined, { strict: false });
}
}Validera och skydda registret
Ett pluginsystem som i tysthet ignorerar dubbletter eller saknade kontrakt är en mardröm att felsöka. Lägg till skydd under upptäckten:
- Dubblettnamn — kasta ett fel i stället för att skriva över, så att två plugins inte kan kollidera på samma nyckel.
- Kontraktskontroll — verifiera att instansen implementerar den förväntade metodstrukturen innan du litar på den.
Om du misslyckas snabbt vid uppstart blir ett pluginfel ett tydligt uppstarts-fel i stället för ett svårtolkat körningsfel.
private register(name: string, instance: object): void {
if (this.plugins.has(name)) {
throw new Error(`Duplicate plugin name: ${name}`);
}
if (typeof (instance as { export?: unknown }).export !== 'function') {
throw new Error(`Plugin ${name} missing export()`);
}
this.plugins.set(name, instance);
}Varför detta passar hexagonal design
Registrering baserad på upptäckt är den körningsmässiga kopplingen för portar och adaptrar:
- Kärnan definierar en port (ett gränssnitt) och ett register som indexeras efter förmåga.
- Varje adapter/plugin deklarerar sig själv med en dekorator — det beror på kärnans kontrakt, aldrig tvärtom.
- Att lägga till en förmåga innebär att lägga till en ny annoterad provider; inga ändringar i kärnans koppling.
Detta vänder på beroenderiktningen (Dependency Inversion Principle) och håller kärnan stängd för ändringar men öppen för utökningar — själva kärnan i plugin-arkitektur.
Snabb kontroll
Du bygger ett pluginregister som cachar varje upptäckt providers .instance i en Map under onApplicationBootstrap. Ett plugin har deklarerats med Scope.REQUEST. Vad går fel och vad är den rätta lösningen?
Sammanfattning
Du byggde ett körningsbaserat pluginsystem med NestJS primitives för upptäckt:
- Märk plugins med en metadatadekorator (
SetMetadata+ enPLUGIN_KEY), på klassnivå för hela plugins och på metodnivå för hanterare. - Skanna containern med
DiscoveryService.getProviders()och filtrera wrappers viaReflector; användMetadataScanner.getAllMethodNames()för hookar per metod. - Välj rätt tidpunkt med
onApplicationBootstrapför säkerhet över flera moduler, och hoppa över wrappers som saknarinstance/metatype. - Skydda mot dubblettnycklar och kontraktsbrott; håll plugins i singleton-scope och lös dem via
ModuleRefendast när request-scope verkligen behövs.
Vinsten: en hexagonal kärna som är stängd för ändringar men öppen för nya annoterade adaptrar — noll ändringar i kärnan per plugin.
Lär dig TypeScript med en AI-lärare – gratis
Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.
- Kurser
- 20
- Lektioner
- 76
Vanliga frågor
Är lektionen ”Dynamisk registrering av providers med DiscoveryService” gratis?
Ja – hela texten till ”Dynamisk registrering av providers med DiscoveryService” kan läsas gratis här på webben. Om Ni vill öva interaktivt med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt och låsa upp resten av kursen i NestJS: backend-API:er för företag, kan Ni uppgradera till CoddyKit PRO. Kursen i NestJS: backend-API:er för företag innehåller totalt 4 lektioner.
Vad lär jag mig i ”Dynamisk registrering av providers med DiscoveryService”?
Skanna och koppla providers vid körning med DiscoveryService och MetadataScanner för pluginsystem. Ni övar på NestJS: backend-API:er för företag med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.
Behöver jag någon erfarenhet för att börja lära mig NestJS: backend-API:er för företag?
Du behöver inga förkunskaper. Utbildningen i NestJS: backend-API:er för företag på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 2 av 4.
Hur lång tid tar lektionen ”Dynamisk registrering av providers med DiscoveryService”?
De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.
Kan jag skriva och köra kod i den här NestJS: backend-API:er för företag-lektionen?
Ja. Varje NestJS: backend-API:er för företag-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.
Alla lektioner i den här kursen
- Portar och adaptrar för domänisolering
- Dynamisk registrering av providers med DiscoveryService
- Laddning av moduler vid behov och feature toggles
- Utökningspunkter med Module Reference API