NestJS: backend-API:er för företag · Lektion

Dynamisk registrering av providers med DiscoveryService

Skanna och koppla providers vid körning med DiscoveryService och MetadataScanner för pluginsystem.

Lektion 2 av 413 steg

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.TRANSIENT kan ha wrapper.instance === null vid 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 + en PLUGIN_KEY), på klassnivå för hela plugins och på metodnivå för hanterare.
  • Skanna containern med DiscoveryService.getProviders() och filtrera wrappers via Reflector; använd MetadataScanner.getAllMethodNames() för hookar per metod.
  • Välj rätt tidpunkt med onApplicationBootstrap för säkerhet över flera moduler, och hoppa över wrappers som saknar instance/metatype.
  • Skydda mot dubblettnycklar och kontraktsbrott; håll plugins i singleton-scope och lös dem via ModuleRef endast 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.

Gratis att börja

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

  1. Portar och adaptrar för domänisolering
  2. Dynamisk registrering av providers med DiscoveryService
  3. Laddning av moduler vid behov och feature toggles
  4. Utökningspunkter med Module Reference API
← Tillbaka till NestJS: backend-API:er för företag