NestJS-API-er for virksomhetsbackend · leksjon

Dynamisk registrering av providere med DiscoveryService

Skann og koble providere under kjøring med DiscoveryService og MetadataScanner for plugin-systemer.

Leksjon 2 av 413 trinn

Dynamisk registrering av providere med DiscoveryService er en gratis leksjon i NestJS-API-er for virksomhetsbackend på CoddyKit. Dette er leksjon 2 av 4. Du kan lese hele leksjonen gratis nedenfor – og deretter øve praktisk i nettleseren med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Den er en del av læringsløpet i NestJS-API-er for virksomhetsbackend, og fremdriften din synkroniseres mellom nettet og CoddyKit-appen. Kurset i NestJS-API-er for virksomhetsbackend inneholder totalt 4 leksjoner.

Problemet med kobling av programtillegg

I en programtilleggsarkitektur vet De ikke ved kompilering hvilke behandlere, strategier eller adaptere som vil finnes. En sekskantet kjerne definerer porter; programtillegg leverer adaptere. Utfordringen er: Hvordan finner og kobler rammeverket disse adapterne uten at De registrerer hver enkelt manuelt?

  • Hardkodede tabeller i en modul er skjøre — hvert nytt programtillegg krever endringer i kjernekoden.
  • De ønsker at leverandører skal deklarere rollen sin selv via dekoratorer og deretter oppdages ved kjøring.

NestJS leverer @nestjs/cores DiscoveryService og MetadataScanner nettopp for dette. De lar Dem skanne den aktive DI-containeren og reagere på metadata.

Merke leverandører med en dekorator

Mønsteret begynner med en egendefinert dekorator som legger metadata på en klasse. Vi bruker SetMetadata (eller Reflector.createDecorator), slik at skanneren senere kan filtrere etter den.

Her merker en @Plugin()-dekorator en klasse som et programtillegg som kan oppdages, og inneholder et name som registeret kan bruke som nøkkel.

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');
  }
}

Hva DiscoveryService gir Dem

DiscoveryService eksporteres av DiscoveryModule. Injiser den for å få to viktige metoder:

  • getProviders() — alle omslag for leverandørinstanser i applikasjonens container.
  • getControllers() — alle kontrolleromslag.

Hvert element er en InstanceWrapper med .instance (det aktive objektet), .metatype (klassen) og .name. De filtrerer disse omslagene ved å lese metadata fra 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 {}

Skanne leverandører under oppstart

Kjør oppdagelsen etter at containeren er ferdig bygget. Implementer OnModuleInit (eller OnApplicationBootstrap hvis De trenger at alle moduler er klare). Filtrer omslag der metatype inneholder metadata med PLUGIN_KEY.

Beskytt Dem mot null-omslag: Noen oppføringer (verdi-leverandører og plassholdere med request-scope) har ingen 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 for hooks på metodenivå

Noen ganger er programtilleggspunktet ikke klassen, men en metode — for eksempel @EventHandler('order.created') på enkeltmetoder. MetadataScanner går gjennom alle metodene på prototypen til en instans, slik at De kan lese metadata for hver metode.

Bruk getAllMethodNames(prototype) (det moderne API-et), og undersøk hver behandler 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`
  }
}

Dekoratoren på metodenivå

Kombiner koblingsmekanismen med en metodedekorator. Merk at det er en MethodDecorator — SetMetadata fester verdien til metodens descriptor.value, som er nøyaktig det reflector.get(EVENT_KEY, proto[method]) leser.

Dette holder koblingen deklarativ: En forfatter av et programtillegg legger til en annotasjon, og kjernen kobler den — uten manuelle kall til 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
  }
}

Modellere oppdagelse i ren TypeScript

Hvis vi fjerner NestJS, er kjerneideen enkel: Et register kobler en nøkkel til en instans som oppdages i en liste, og ruter deretter kall etter nøkkel. Denne frittstående modellen gjengir registersemantikken som De kobler til DiscoveryService.

Det samme Map-baserte oppslaget driver det virkelige registeret — bare kilden til instansene er forskjellig.

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));

Tidspunkt for oppdagelse og livssyklus

Tidspunktet er viktig. DI-containeren er bare komplett i bestemte livssyklusfaser:

  • onModuleInit — kjøres per modul etter at modulens egne leverandører er løst. Det fungerer fint hvis alle programtillegg ligger i én modul.
  • onApplicationBootstrap — kjøres én gang etter at alle moduler er initialisert. Dette er tryggest ved skanning av programtillegg på tvers av moduler.

Hvis De skanner for tidlig, får De en tom eller ufullstendig leverandørliste. Foretrekk onApplicationBootstrap når programtillegg kan leveres i feature-moduler som lastes senere.

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);
    }
  }
}

Fallgruver med scope: REQUEST og TRANSIENT

Oppdagelse fungerer problemfritt med singletoner. Vær oppmerksom på scopes som ikke er standard:

  • Leverandører med Scope.REQUEST / Scope.TRANSIENT kan ha wrapper.instance === null ved oppstart — det finnes ingen enkeltinstans som kan bufres.
  • En oppdaget instans med request-scope ville blitt foreldet og lekket tilstand fra én forespørsel dersom De bufret den.

Tommelfingerregel: La programtillegg ha singleton-scope. Hvis et programtillegg faktisk trenger forespørselsdata, oppdag klassen og løs en ny instans per forespørsel via ModuleRef.resolve() i stedet for å bufre 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 });
  }
}

Validere og sikre registeret

Et programtilleggssystem som i stillhet ignorerer duplikater eller manglende kontrakter, er et mareritt å feilsøke. Legg til kontroller under oppdagelsen:

  • Dupliserte navn — kast en feil, ikke overskriv, slik at to programtillegg ikke kan kollidere på samme nøkkel.
  • Kontraktskontroll — bekreft at instansen har forventet metodisk struktur før De stoler på den.

Ved å feile raskt under oppstart blir en feil i et programtillegg ved kjøring til en tydelig oppstartsfeil.

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);
}

Hvorfor dette passer med sekskantet design

Registrering basert på oppdagelse er kjøretidslimet i porter og adaptere:

  • Kjernen definerer en port (et grensesnitt) og et register med nøkkel basert på funksjonalitet.
  • Hver adapter/programmtillegg deklarerer seg selv med en dekorator — den avhenger av kjernens kontrakt, aldri omvendt.
  • Å legge til en funksjon betyr å legge inn en ny annotert leverandør; ingen endringer i kjernens kobling.

Dette snur avhengighetsretningen (prinsippet om avhengighetsinversjon) og holder kjernen lukket for endringer, men åpen for utvidelser — selve kjernen i en programtilleggsarkitektur.

Rask kontroll

De bygger et programtilleggsregister som bufrer hver oppdagede leverandørs .instance i en Map under onApplicationBootstrap. Ett programtillegg er deklarert med Scope.REQUEST. Hva går galt, og hva er den riktige løsningen?

Oppsummering

De bygget et kjøretidsbasert programtilleggssystem ved hjelp av NestJS-primitiver for oppdagelse:

  • Merk programtillegg med en metadatadekorator (SetMetadata + en PLUGIN_KEY), på klassenivå for hele programtillegg og på metodenivå for behandlere.
  • Skann containeren med DiscoveryService.getProviders(), og filtrer omslag med Reflector; bruk MetadataScanner.getAllMethodNames() for hooks på metodenivå.
  • Velg riktig tidspunkt med onApplicationBootstrap for sikkerhet på tvers av moduler, og hopp over omslag uten instance/metatype.
  • Sikre mot dupliserte nøkler og kontraktbrudd; la programtillegg ha singleton-scope, og løs dem via ModuleRef bare når request-scope faktisk er nødvendig.

Gevinsten er en sekskantet kjerne som er lukket for endringer, men åpen for nye annoterte adaptere — ingen endringer i kjernen per programtillegg.

Gratis å komme i gang

Lær deg TypeScript med en AI-veileder – gratis

Skriv og kjør ekte kode i nettleseren, få umiddelbar hjelp fra en AI-veileder som er tilgjengelig døgnet rundt, og fortsett der du slapp – på nettet eller i appen.

Kurs
20
Leksjoner
76

Ofte stilte spørsmål

Er leksjonen «Dynamisk registrering av providere med DiscoveryService» gratis?

Ja – hele teksten i «Dynamisk registrering av providere med DiscoveryService» er gratis å lese her på nettet. For å øve interaktivt med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt, og for å låse opp resten av NestJS-API-er for virksomhetsbackend-kurset, kan du oppgradere til CoddyKit PRO. Kurset i NestJS-API-er for virksomhetsbackend inneholder totalt 4 leksjoner.

Hva lærer jeg i «Dynamisk registrering av providere med DiscoveryService»?

Skann og koble providere under kjøring med DiscoveryService og MetadataScanner for plugin-systemer. Du øver på NestJS-API-er for virksomhetsbackend med praktisk kode som du kjører direkte i nettleseren, mens en AI-veileder som er tilgjengelig døgnet rundt, svarer på spørsmålene dine mens du jobber deg gjennom leksjonen.

Trenger jeg erfaring for å begynne med NestJS-API-er for virksomhetsbackend?

Ingen tidligere erfaring er nødvendig. NestJS-API-er for virksomhetsbackend på CoddyKit er lagt opp for både nybegynnere og viderekomne, så De kan begynne her eller helt fra start og lære i Deres eget tempo. Dette er leksjon 2 av 4.

Hvor lang tid tar leksjonen «Dynamisk registrering av providere med DiscoveryService»?

De fleste CoddyKit-leksjoner tar omtrent 5–10 minutter. Hver leksjon er kort og interaktiv, slik at De gjør jevne fremskritt og kan fortsette akkurat der De slapp – både på nettet og i appen.

Kan jeg skrive og kjøre kode i denne NestJS-API-er for virksomhetsbackend-leksjonen?

Ja. Alle NestJS-API-er for virksomhetsbackend-leksjoner har en innebygd kodeeditor, slik at De kan skrive og kjøre ekte kode direkte i nettleseren og få umiddelbar tilbakemelding fra AI – uten lokal konfigurering.

Alle leksjonene i dette kurset

  1. Porter og adaptere for domeneisolasjon
  2. Dynamisk registrering av providere med DiscoveryService
  3. Moduler lastet inn ved behov og funksjonsbrytere
  4. Utvidelsespunkter med Module Reference API
← Tilbake til NestJS-API-er for virksomhetsbackend