Abhängigkeitsinjektions-, JavaScript- und ES6-Module

Eine weitere Implementierung von Dependency Injection in JavaScript sind ES6-Module mit der Möglichkeit, denselben Code in einem Browser und in nodejs zu verwenden und keine Transpiler zu verwenden.


Bild


Unter dem Strich ist meine Sicht auf DI, seinen Platz in modernen Webanwendungen, die grundlegende Implementierung eines DI-Containers, mit dem Objekte sowohl vorne als auch hinten erstellt werden können, sowie eine Erklärung, was Michael Jackson damit zu tun hat.


Ich bitte diejenigen, die es in dem Artikel trivial finden, nicht, sich selbst zu vergewaltigen und nicht bis zum Ende zu lesen, damit sie später, wenn sie enttäuscht sind, kein "Minus" setzen. Ich bin nicht gegen die "Minuspunkte" - aber nur wenn das Minus von einem Kommentar begleitet wird, was genau in der Publikation eine negative Reaktion hervorrief. Dies ist ein technischer Artikel. Versuchen Sie also, sich dem Präsentationsstil herablassen zu können, und kritisieren Sie genau die technische Komponente des oben genannten. Vielen Dank.


Objekte in der Anwendung


Ich respektiere die funktionale Programmierung sehr, aber ich habe den größten Teil meiner beruflichen Tätigkeit der Erstellung von Anwendungen gewidmet, die aus Objekten bestehen. JavaScript beeindruckt mich dadurch, dass die darin enthaltenen Funktionen auch Objekte sind. Beim Erstellen von Anwendungen denke ich an Objekte, dies ist meine berufliche Verformung.


Je nach Lebensdauer können Objekte in der Anwendung in die folgenden Kategorien unterteilt werden:


  • dauerhaft - entstehen zu einem bestimmten Zeitpunkt des Antrags und werden erst nach Abschluss des Antrags vernichtet;
  • vorübergehend - entstehen, wenn eine Operation ausgeführt werden muss, und werden zerstört, wenn diese Operation abgeschlossen ist;

In dieser Hinsicht gibt es bei der Programmierung solche Entwurfsmuster wie:



Das heißt, aus meiner Sicht besteht die Anwendung aus permanent vorhandenen Einzelgängern, die entweder die erforderlichen Operationen selbst ausführen oder temporäre Objekte generieren, um sie auszuführen.


Objektcontainer


Die Abhängigkeitsinjektion ist ein Ansatz, mit dem Objekte in einer Anwendung einfach erstellt werden können. Das heißt, in der Anwendung gibt es ein spezielles Objekt, das „weiß“, wie alle anderen Objekte erstellt werden. Ein solches Objekt wird als Objektcontainer (manchmal als Objektmanager) bezeichnet.


Der Objektcontainer ist kein göttliches Objekt , weil Ihre Aufgabe besteht nur darin, wichtige Objekte der Anwendung zu erstellen und ihnen Zugriff auf andere Objekte zu gewähren. Die überwiegende Mehrheit der Anwendungsobjekte, die vom Container generiert werden und sich darin befinden, hat keine Ahnung vom Container selbst. Sie können in jeder anderen Umgebung platziert werden, die mit den erforderlichen Abhängigkeiten ausgestattet ist, und sie funktionieren dort auch bemerkenswert gut (Tester wissen, was ich meine).


Ort der Umsetzung


Im Großen und Ganzen gibt es zwei Möglichkeiten , Abhängigkeiten in ein Objekt einzufügen:


  • durch den Konstruktor;
  • durch eine Eigenschaft (oder ihren Zugang);

Ich habe im Grunde den ersten Ansatz verwendet, daher werde ich die Beschreibung unter dem Gesichtspunkt der Abhängigkeitsinjektion durch den Konstruktor fortsetzen.


Angenommen, wir haben eine Anwendung, die aus drei Objekten besteht:


Bild


In PHP (diese Sprache mit langjährigen DI-Traditionen, ich habe derzeit aktives Gepäck, ich werde etwas später zu JS übergehen) könnte sich eine ähnliche Situation folgendermaßen widerspiegeln:


class Config { public function __construct() { } } class Service { private $config; public function __construct(Config $config) { $this->config = $config; } } class Application { private $config; private $service; public function __construct(Config $config, Service $service) { $this->config = $config; $this->service = $service; } } 

Diese Informationen sollten ausreichen, damit ein DI-Container (z. B. Liga / Container ) bei entsprechender Konfiguration auf Anforderung zum Erstellen eines Application dessen Abhängigkeiten Service und Config erstellen und diese Parameter an den Konstruktor des Application kann.


Abhängigkeitskennungen


Wie versteht der Objektcontainer, dass der Konstruktor des Application zwei Config und Config benötigt? Durch Analyse des Objekts über die Reflection-API ( Java , PHP ) oder durch direkte Analyse des Objektcodes (Code-Annotationen). Das heißt, im allgemeinen Fall können wir die Namen der Variablen bestimmen, die der Objektkonstruktor an der Eingabe erwartet, und wenn die Sprache typisierbar ist, können wir auch die Typen dieser Variablen abrufen.


Somit kann der Container als Bezeichner von Objekten entweder mit den Namen der Eingabeparameter des Konstruktors oder mit den Arten von Eingabeparametern arbeiten.


Objekte erstellen


Das Objekt kann vom Programmierer explizit erstellt und im Container unter der entsprechenden Kennung abgelegt werden (z. B. "Konfiguration").


 /** @var \League\Container\Container $container */ $container->add("configuration", $config); 

und kann vom Container nach bestimmten Regeln erstellt werden. Diese Regeln bestehen im Großen und Ganzen darin, die Kennung des Objekts mit seinem Code abzugleichen. Regeln können explizit festgelegt werden (Zuordnung in Form von Code, XML, JSON, ...)


 [ ["object_id_1", "/path/to/source1.php"], ["object_id_2", "/path/to/source2.php"], ... ] 

oder in Form eines Algorithmus:


 public function getSource($id) {. return "/path/to/source/${id}.php"; } 

In PHP sind die Regeln für den Abgleich eines Klassennamens mit einer Datei mit ihrem Quellcode standardisiert ( PSR-4 ). In Java erfolgt der Abgleich auf der JVM-Konfigurationsebene ( Class Loader ). Wenn der Container beim Erstellen von Objekten eine automatische Suche nach Quellen bietet, sind die Klassennamen ausreichend für Bezeichner in einem solchen Container.


Namespaces


In der Regel werden in einem Projekt neben dem eigenen Code auch Module von Drittanbietern verwendet. Mit dem Aufkommen von Abhängigkeitsmanagern (Maven, Composer, npm) wurde die Verwendung von Modulen erheblich vereinfacht und die Anzahl der Module in Projekten hat stark zugenommen. Mit Namespaces können gleichnamige Codeelemente in einem einzelnen Projekt aus verschiedenen Modulen (Klassen, Funktionen, Konstanten) vorhanden sein.


Es gibt Sprachen, in denen der Namespace anfänglich eingebaut ist (Java):


 package vendor.project.module.folder; 

Es gibt Sprachen, in denen der Namespace während der Entwicklung der Sprache (PHP) hinzugefügt wurde:


 namespace Vendor\Project\Module\Folder; 

Mit einer guten Namespace-Implementierung können Sie jedes Element des Codes eindeutig ansprechen:


 \Doctrine\Common\Annotations\Annotation\Attribute::$name 

Der Namespace löst das Problem der Organisation vieler Softwareelemente in einem Projekt, und die Dateistruktur löst das Problem der Organisation von Dateien auf der Festplatte. Daher gibt es nicht nur viele Gemeinsamkeiten und manchmal auch viele Gemeinsamkeiten. In Java muss beispielsweise eine öffentliche Klasse im Namespace eindeutig an eine Datei mit dem Code dieser Klasse angehängt werden.


Die Verwendung des Bezeichners einer Objektklasse im Projektnamespace als Objektbezeichner im Container ist daher eine gute Idee und kann als Grundlage für die Erstellung von Regeln für die automatische Erkennung von Quellcodes beim Erstellen des gewünschten Objekts dienen.


 $container->add(\Vendor\Project\Module\ObjectType::class, $obj); 

Code-Start


In PHP composer Modul-Namespace im Moduldeskriptor composer dem Dateisystem innerhalb des Moduls zugeordnet:


 "autoload": { "psr-4": { "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" } } 

Die JS-Community könnte eine ähnliche Zuordnung in package.json wenn in JS Namespaces package.json wären.


JS-Abhängigkeitskennungen


Oben habe ich angegeben, dass der Container entweder die Namen der Eingabeparameter des Konstruktors oder die Arten von Eingabeparametern als Bezeichner verwenden kann. Das Problem ist, dass:


  1. JS ist eine Sprache mit dynamischer Typisierung und bietet keine Angabe von Typen beim Deklarieren einer Funktion.
  2. JS verwendet Minifizierer, die Eingabeparameter umbenennen können.

Die Entwickler des awilix DI-Containers schlagen vor, das Objekt als einzigen Eingabeparameter für den Konstruktor und die Eigenschaften dieses Objekts als Abhängigkeiten zu verwenden:


 class UserController { constructor(opts) { this.userService = opts.userService } } 

Die Objekteigenschafts- ID in JS kann aus alphanumerischen Zeichen "_" und "$" bestehen und darf nicht mit einer Ziffer beginnen.


Da wir für das automatische Laden dem Pfad zu ihren Quellen im Dateisystem Abhängigkeitskennungen zuordnen müssen, ist es besser, die Verwendung von "$" aufzugeben und die Erfahrung von PHP zu nutzen. Bevor der namespace Operator in einigen Frameworks (z. B. in Zend 1) angezeigt wurde, wurden die folgenden Namen für Klassen verwendet:


 class Zend_Config_Writer_Json {...} 

So könnten wir unsere Anwendung von drei Objekten ( Application , Config , Service ) auf JS wie Config widerspiegeln:


 class Vendor_Project_Config { constructor() { } } class Vendor_Project_Service { constructor({Vendor_Project_Config}) { this.config = Vendor_Project_Config; } } class Vendor_Project_Application { constructor({Vendor_Project_Config, Vendor_Project_Service}) { this.config = Vendor_Project_Config; this.service = Vendor_Project_Service; } } 

Wenn wir den Code jeder Klasse veröffentlichen:


 export default class Vendor_Project_Application { constructor({Vendor_Project_Config, Vendor_Project_Service}) { this.config = Vendor_Project_Config; this.service = Vendor_Project_Service; } } 

in Ihrer Datei in unserem Projektmodul:


  • ./src/
    • ./Application.js
    • ./Config.js
    • ./Service.js

Dann können wir das Stammverzeichnis des Moduls mit dem Stamm- "Namespace" des Moduls in der Konfiguration des Containers verbinden:


 const ns = "Vendor_Project"; const path = path.join(module_root, "src"); container.addSourceMapping(ns, path); 

${module_root}/src/Config.js dann ausgehend von diesen Informationen den Pfad zu den entsprechenden Quellen ( ${module_root}/src/Config.js ) basierend auf der Abhängigkeitskennung ( ${module_root}/src/Config.js ).


ES6-Module


ES6 bietet ein allgemeines Design zum Laden von ES6-Modulen:


 import { something } from 'path/to/source/with/something'; 

Da wir ein Objekt (eine Klasse) an eine Datei anhängen müssen, ist es in der Quelle sinnvoll, diese Klasse standardmäßig zu exportieren:


 export default class Vendor_Project_Path_To_Source_With_Something {...} 

Im Prinzip ist es möglich, keinen so langen Namen für die Klasse zu schreiben, nur Something wird auch funktionieren, aber in Zend 1 haben sie geschrieben und nicht gebrochen, und die Eindeutigkeit des Klassennamens innerhalb des Projekts wirkt sich positiv auf die Funktionen der IDE aus (automatische Vervollständigung und kontextbezogene Eingabeaufforderungen) und beim Debuggen:


Bild


Das Importieren einer Klasse und das Erstellen eines Objekts sieht in diesem Fall folgendermaßen aus:


 import Something from 'path/to/source/with/something'; const something = new Something(); 

Front & Back Import


Der Import funktioniert sowohl im Browser als auch in NodeJS, es gibt jedoch Nuancen. Beispielsweise versteht der Browser den Import von NodeJS-Modulen nicht:


 import path from "path"; 

Wir erhalten eine Fehlermeldung im Browser:


 Failed to resolve module specifier "path". Relative references must start with either "/", "./", or "../". 

Das heißt, wenn unser Code sowohl im Browser als auch in den Knotenjs funktionieren soll, können wir keine Konstrukte verwenden, die der Browser oder die Knotenjs nicht verstehen. Ich konzentriere mich speziell darauf, weil eine solche Schlussfolgerung zu natürlich ist, um darüber nachzudenken. Wie man atmet.


DIs Platz in modernen Webanwendungen


Dies ist aufgrund meiner persönlichen Erfahrung, wie alles andere in dieser Veröffentlichung, nur meine persönliche Meinung.


In Webanwendungen nimmt JS praktisch ohne Alternative seinen Platz im Browser ein. Auf der Serverseite haben sich Java, PHP, .Net, Ruby, Python dicht eingegraben ... Mit dem Aufkommen von nodejs drang auch JavaScript in den Server ein. Und die in anderen Sprachen verwendeten Technologien, einschließlich DI, begannen, serverseitiges JS zu durchdringen.


Die Entwicklung von JavaScript ist auf das asynchrone Verhalten des Codes im Browser zurückzuführen. Asynchronität ist kein außergewöhnliches Merkmal von JS, sondern angeboren. Das Vorhandensein von JS sowohl auf dem Server als auch auf der Vorderseite überrascht niemanden, sondern fördert die Verwendung derselben Ansätze an beiden Enden der Webanwendung. Und der gleiche Code. Natürlich sind Vorder- und Rückseite im Wesentlichen und in den zu lösenden Aufgaben zu unterschiedlich, um dort und dort denselben Code zu verwenden. Wir können jedoch davon ausgehen, dass es in einer mehr oder weniger komplexen Anwendung Browser, Server und allgemeinen Code gibt.


DI wird an der Vorderseite bereits in RequireJS verwendet :


 define( ["./config", "./service"], function App(Config, Service) {} ); 

Richtig, hier werden die Bezeichner von Abhängigkeiten explizit und sofort in Form von Links zu den Quellen geschrieben (Sie können die Zuordnung von Bezeichnern in der Bootloader-Konfiguration konfigurieren).


In modernen Webanwendungen existiert DI nicht nur auf der Serverseite, sondern auch im Browser.


Was hat Michael Jackson damit zu tun?


Wenn ES-Module in nodejs aktiviert sind (das --experimental-modules ), identifiziert die Engine den Inhalt von Dateien mit der *.mjs als EcmaScript-Module (im Gegensatz zu Common-Modulen mit der *.cjs ).


Dieser Ansatz wird manchmal als " Michael Jackson-Lösung " bezeichnet, und die Skripte werden als Michael Jackson-Skripte ( *.mjs ) bezeichnet.


Ich bin damit einverstanden, dass die mittelmäßige Intrige mit dem KDPV gelöst wurde, aber ... der Camon der Jungs, Michael Jackson ...


Noch eine DI-Implementierung


Nun, wie erwartet, deine eigene Fahrrad DI-Modul - @ teqfw / di


Dies ist keine kampfbereite Lösung, sondern eine grundlegende Implementierung. Alle Abhängigkeiten sollten ES-Module sein und gemeinsame Funktionen für den Browser und die Knoten verwenden.


Um Abhängigkeiten aufzulösen, verwendet das Modul den awilix- Ansatz:


 constructor(spec) { /** @type {Vendor_Module_Config} */ const _config = spec.Vendor_Module_Config; /** @type {Vendor_Module_Service} */ const _service = spec.Vendor_Module_Service; } 

So führen Sie das hintere Beispiel aus:


 import Container from "./src/Container.mjs"; const container = new Container(); container.addSourceMapping("Vendor_Module", "../example"); container.get("Vendor_Module_App") .then((app) => { app.run(); }); 

auf dem Server:


 $ node --experimental-modules main.mjs 

So führen Sie das vordere Beispiel aus ( example.html ):


 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>DI in Browser</title> <script type="module" src="./main.mjs"></script> </head> <body> <p>Load main script './main.mjs', create new DI container, then get object by ID from container.</p> <p>Open browser console to see output.</p> </body> </html> 

Sie müssen das Modul auf den Server stellen und die Seite example.html im Browser öffnen (oder die Funktionen der IDE nutzen). Wenn Sie example.html direkt öffnen, lautet der Fehler in Chrom:


 Access to script at 'file:///home/alex/work/teqfw.di/main.mjs' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https. 

Wenn alles gut gegangen ist, gibt es in der Konsole (Browser oder NodeJS) ungefähr Folgendes:


 Create object with ID 'Vendor_Module_App'. Create object with ID 'Vendor_Module_Config'. There is no dependency with id 'Vendor_Module_Config' yet. 'Vendor_Module_Config' instance is created. Create object with ID 'Vendor_Module_Service'. There is no dependency with id 'Vendor_Module_Service' yet. 'Vendor_Module_Service' instance is created (deps: [Vendor_Module_Config]). 'Vendor_Module_App' instance is created (deps: [Vendor_Module_Config, Vendor_Module_Service]). Application 'Vendor_Module_Config' is running. 

Zusammenfassung


AMD, CommonJS, UMD?


ESM !

Source: https://habr.com/ru/post/de464347/


All Articles