Dependency Injection jenseits von NestJS – Ein tiefer Einblick in tsyringe und InversifyJS
Wenhao Wang
Dev Intern · Leapcell

Die Macht der Entkopplung: Dependency Injection in TypeScript-Projekten
In der sich entwickelnden Landschaft der modernen Softwareentwicklung sind der Aufbau skalierbarer, wartbarer und testbarer Anwendungen von größter Bedeutung. Ein Architekturmuster, das entscheidend zur Erreichung dieser Ziele beiträgt, ist Dependency Injection (DI). Während Frameworks wie NestJS DI nahtlos integrieren, könnten viele TypeScript-Projekte außerhalb dieses Ökosystems immer noch enorm von seinen Prinzipien profitieren. Dieser Artikel befasst sich damit, wie tsyringe
und InversifyJS
, zwei prominente DI-Container für TypeScript, Ihre unabhängigen Projekte stärken und Modularität fördern und enge Kopplungen reduzieren können. Wir werden ihre Kernmechanismen, Implementierungsdetails und praktischen Anwendungsfälle untersuchen, um Ihnen bei der Auswahl des richtigen Werkzeugs für Ihre Bedürfnisse zu helfen.
Verständnis der Kernkonzepte
Bevor wir uns mit den Besonderheiten von tsyringe
und InversifyJS
befassen, wollen wir unser Verständnis einiger grundlegender DI-Konzepte kurz festigen:
- Dependency Injection (DI): Ein Entwurfsmuster, bei dem Komponenten ihre Abhängigkeiten von einer externen Quelle erhalten, anstatt sie intern zu erstellen. Diese "Inversion of Control" fördert lose Kopplung, wodurch Komponenten einfacher zu testen, wiederzuverwenden und zu warten sind.
- IoC Container (Inversion of Control Container): Auch als DI-Container bekannt, ist er ein Framework oder eine Bibliothek, die die Instanziierung und den Lebenszyklus von Objekten und ihren Abhängigkeiten verwaltet. Er kümmert sich um das "Verdrahten" von Komponenten basierend auf Konfiguration oder Decorators.
- Binding/Registrierung: Der Prozess der Information des DI-Containers, wie eine Instanz einer bestimmten Abhängigkeit bereitgestellt werden soll, wenn sie angefordert wird. Dies beinhaltet oft die Abbildung einer Schnittstelle oder einer abstrakten Klasse auf eine konkrete Implementierung.
- Auflösung (Resolution): Die Handlung, eine Instanz einer Abhängigkeit vom DI-Container anzufordern. Der Container sucht dann nach seinen Bindungen, instanziiert die notwendigen Komponenten und injiziert sie.
- Decorator: Eine spezielle Art von Deklaration, die an Klassen, Methoden, Eigenschaften oder Parameter angehängt werden kann. Im Kontext von DI werden Decorators oft verwendet, um Klassen als injizierbar zu markieren, Injektionspunkte zu definieren oder Bindungen zu konfigurieren.
- Service Identifier: Ein eindeutiger Schlüssel (oft eine Zeichenkette, ein Symbol oder ein Klassenkonstruktor), der zur Identifizierung einer bestimmten Abhängigkeit innerhalb des IoC-Containers verwendet wird.
tsyringe: Ein leichtgewichtiger und decorator-gesteuerter Ansatz
tsyringe
, entwickelt von Microsoft, ist ein leichtgewichtiger und performanter Dependency Injection Container für TypeScript. Er nutzt experimentelle Decorators und Reflektionsfähigkeiten von TypeScript, um die Abhängigkeitsverwaltung erheblich zu vereinfachen. Seine API ist intuitiv und fühlt sich sehr "TypeScript-nativ" an.
Implementierung und Anwendung
Lassen Sie uns tsyringe
mit einem praktischen Beispiel veranschaulichen: ein einfacher Benachrichtigungsdienst.
Installieren Sie zuerst tsyringe
:
npm install tsyringe reflect-metadata
Und stellen Sie sicher, dass emitDecoratorMetadata
und experimentalDecorators
in Ihrer tsconfig.json
aktiviert sind:
{ "compilerOptions": { "emitDecoratorMetadata": true, "experimentalDecorators": true, // ...andere Optionen } }
Jetzt definieren wir unsere Dienste:
// services/notifier.ts import { injectable } from "tsyringe"; interface INotifier { send(message: string): void; } @injectable() class EmailNotifier implements INotifier { send(message: string): void { console.log(`Sending email: ${message}`); } } @injectable() class SMSNotifier implements INotifier { send(message: string): void { console.log(`Sending SMS: ${message}`); } } export { INotifier, EmailNotifier, SMSNotifier };
Als nächstes ein Dienst, der einen Benachrichtiger verwendet:
// services/user-service.ts import { injectable, inject } from "tsyringe"; import { INotifier } from "./notifier"; @injectable() class UserService { constructor(@inject("INotifier") private notifier: INotifier) {} registerUser(username: string): void { console.log(`User ${username} registered.`); this.notifier.send(`Welcome, ${username}!`); } } export { UserService };
Schließlich das Bootstrapping unserer Anwendung und das Auflösen von Abhängigkeiten:
// app.ts import "reflect-metadata"; // Muss einmal oben in Ihrer Einstiegsdatei importiert werden import { container } from "tsyringe"; import { INotifier, EmailNotifier, SMSNotifier } from "./services/notifier"; import { UserService } from "./services/user-service"; // Registrieren von Abhängigkeiten // Wir können eine Schnittstelle an eine konkrete Implementierung binden container.register<INotifier>("INotifier", { useClass: EmailNotifier }); // Oder wir können direkt nach Klassentyp registrieren, wenn keine Schnittstelle verwendet wird oder für mehrere Implementierungen // container.register(EmailNotifier); // container.register(SMSNotifier); // An diesem Punkt, wenn wir den Benachrichtiger ändern wollten, würden wir einfach die obige Registrierung ändern // Zum Beispiel, um SMSNotifier zu verwenden: // container.register<INotifier>("INotifier", { useClass: SMSNotifier }); // Auflösen des UserService (der transitiv INotifier auflöst) const userService = container.resolve(UserService); userService.registerUser("Alice"); // Wir können auch transiente Dienste haben (neue Instanz bei jeder Abfrage) im Vergleich zu Singletons (eine Instanz) // Standardmäßig sind tsyringe-Klassen transient, sofern nicht anders angegeben. // Um EmailNotifier zu einem Singleton zu machen: // container.registerSingleton<INotifier>("INotifier", EmailNotifier);
In diesem Beispiel markiert @injectable()
eine Klasse als für die Injektion verfügbar. @inject("INotifier")
weist tsyringe
an, eine Instanz zu injizieren, die durch die Zeichenkette "INotifier"
identifiziert wird. Die Methode container.register()
ist, wo wir den INotifier
-Identifikator an eine konkrete EmailNotifier
-Implementierung binden. Diese klare Trennung erleichtert den Austausch von Implementierungen (z. B. von EmailNotifier
zu SMSNotifier
), ohne UserService
ändern zu müssen.
tsyringe
ist hervorragend für seine Einfachheit und seine enge Integration mit dem Typsystem von TypeScript, was es zu einer guten Wahl für viele Projekte macht.
InversifyJS: Robust, flexibel und funktionsreich
InversifyJS
ist ein weiteres leistungsstarkes und weit verbreitetes Dependency Injection Framework für TypeScript. Es bietet einen umfangreicheren Satz von Funktionen im Vergleich zu tsyringe
, einschließlich erweiterter Bindungsoptionen, Lebenszyklusverwaltung und Middleware-Unterstützung. Es ist auf Erweiterbarkeit und Testbarkeit ausgelegt.
Implementierung und Anwendung
Lassen Sie uns unser Benachrichtigungsdienstbeispiel an InversifyJS
anpassen.
Installieren Sie zuerst InversifyJS
und reflect-metadata
:
npm install inversify reflect-metadata
Stellen Sie erneut sicher, dass emitDecoratorMetadata
und experimentalDecorators
in Ihrer tsconfig.json
aktiviert sind.
Wir werden Schnittstellen definieren und Symbole (Symbols) als Service-Identifikatoren für bessere Typsicherheit verwenden und Zeichenketten-basierte Magie-Literale vermeiden, was bei InversifyJS
eine gängige Praxis ist.
// services/notifier.ts import { injectable } from "inversify"; interface INotifier { send(message: string): void; } @injectable() class EmailNotifier implements INotifier { send(message: string): void { console.log(`[InversifyJS] Sending email: ${message}`); } } @injectable() class SMSNotifier implements INotifier { send(message: string): void { console.log(`[InversifyJS] Sending SMS: ${message}`); } } // Symbole für Service-Identifikatoren definieren const TYPES = { INotifier: Symbol.for("INotifier"), }; export { INotifier, EmailNotifier, SMSNotifier, TYPES };
Als nächstes wird unser UserService
@inject
mit dem Symbol verwenden:
// services/user-service.ts import { injectable, inject } from "inversify"; import { INotifier, TYPES } from "./notifier"; @injectable() class UserService { constructor(@inject(TYPES.INotifier) private notifier: INotifier) {} registerUser(username: string): void { console.log(`[InversifyJS] User ${username} registered.`); this.notifier.send(`Welcome, ${username}!`); } } export { UserService };
Schließlich das Bootstrapping der Anwendung mit InversifyJS
:
// app.ts import "reflect-metadata"; // Muss einmal oben in Ihrer Einstiegsdatei importiert werden import { Container } from "inversify"; import { INotifier, EmailNotifier, SMSNotifier, TYPES } from "./services/notifier"; import { UserService } from "./services/user-service"; // Einen neuen InversifyJS Container erstellen const inversifyContainer = new Container(); // Bindungen: // Wir binden das Symbol an unsere konkrete Implementierung inversifyContainer.bind<INotifier>(TYPES.INotifier).to(EmailNotifier); // Wir können es auch zu einem Singleton machen // inversifyContainer.bind<INotifier>(TYPES.INotifier).to(EmailNotifier).inSingletonScope(); // UserService direkt binden inversifyContainer.bind<UserService>(UserService).toSelf(); // Unseren Dienst auflösen const userService = inversifyContainer.get<UserService>(UserService); userService.registerUser("Bob"); // Um den Wechsel der Implementierung zu demonstrieren console.log("\nSwitching to SMS Notifier:"); inversifyContainer.unbind(TYPES.INotifier); // Die vorherige Aufhebung der Bindung inversifyContainer.bind<INotifier>(TYPES.INotifier).to(SMSNotifier); // Die neue binden const userServiceWithSMS = inversifyContainer.get<UserService>(UserService); userServiceWithSMS.registerUser("Charlie");
InversifyJS
verwendet ähnliche Decorators (@injectable
, @inject
), aber typischerweise gepaart mit Symbolen für Service-Identifikatoren, was eine stärkere Typenprüfung ermöglicht und potenzielle Namenskollisionen von Zeichenketten-Literalen vermeidet. Die Methode Container.bind()
bietet eine flüssige API zur Konfiguration von Bindungen, einschließlich der Angabe von Scopes (.inSingletonScope()
, .inTransientScope()
). Dies ermöglicht eine feingranulare Kontrolle über die Lebenszyklen der Instanzen. Seine Flexibilität macht es für größere, komplexere Anwendungen geeignet, bei denen eine feingranulare Kontrolle über den DI-Prozess von Vorteil ist.
Welches soll man wählen?
Sowohl tsyringe
als auch InversifyJS
sind ausgezeichnete Wahlmöglichkeiten für Dependency Injection in TypeScript-Projekten. Die Entscheidung hängt oft von den spezifischen Projektanforderungen und Präferenzen ab:
-
Wählen Sie
tsyringe
, wenn:- Sie eine leichtere Lösung mit minimaler API bevorzugen.
- Sie Einfachheit und einen direkten, decorator-gesteuerten Ansatz schätzen, der eng mit den TypeScript-Typen übereinstimmt.
- Ihr Projekt keine hoch entwickelten Bindungskonfigurationen oder umfangreichen Funktionen zur Lebenszyklusverwaltung benötigt.
- Sie kleine bis mittelgroße Anwendungen erstellen.
-
Wählen Sie
InversifyJS
, wenn:- Sie einen funktionsreicheren und robusteren DI-Container benötigen.
- Ihr Projekt erweiterte Bindungsoptionen erfordert (z. B. bedingte Bindungen, benutzerdefinierte Provider, Multi-Injections).
- Sie eine feingranulare Kontrolle über die Lebenszyklen der Instanzen wünschen (Singletons, Request-Scope usw.).
- Sie an größeren, unternehmensgerechten Anwendungen arbeiten, bei denen Erweiterbarkeit und Testbarkeit von größter Bedeutung sind, oder wenn Sie robuste Fehlerbehandlungs- und Debugging-Unterstützung benötigen.
- Sie die Verwendung von Symbolen für Service-Identifikatoren schätzen, die die Typsicherheit verbessern.
Fazit
Die Akzeptanz von Dependency Injection mit Frameworks wie tsyringe
oder InversifyJS
verändert grundlegend, wie Sie TypeScript-Anwendungen außerhalb der expliziten Grenzen von NestJS entwerfen und erstellen. Durch die Entkopplung von Komponenten und die Zentralisierung ihrer Erstellung und Verdrahtung erreichen Sie erhöhte Modularität, Testbarkeit und Wartbarkeit. Ob Sie sich für die optimierte Eleganz von tsyringe
oder die umfassende Leistung von InversifyJS
entscheiden, beide ermöglichen es Ihnen, saubereren, widerstandsfähigeren Code zu schreiben, was letztendlich zu robusteren und skalierbareren Softwaresystemen führt. Die Wahl zwischen ihnen hängt von den spezifischen Anforderungen und der Komplexität Ihres Projekts ab, aber die zugrunde liegenden Vorteile der DI sind universell wertvoll.