Dependency Injection, JavaScript, and ES6 Modules

Another implementation of Dependency Injection in JavaScript is with ES6 modules, with the ability to use the same code in a browser and in nodejs and not use transpilers.







image







Under the cut is my view on DI, its place in modern web applications, the fundamental implementation of a DI container that can create objects both on the front and back, as well as an explanation of what Michael Jackson has to do with it.







I very strongly ask those who find it trivial in the article not to rape themselves and not read to the end, so that later, when they are disappointed, they donโ€™t put a โ€œminusโ€. I am not against the โ€œminusesโ€ - but only if the minus is accompanied by a comment, what exactly in the publication caused a negative reaction. This is a technical article, so try to be condescending to the style of presentation, and criticize precisely the technical component of the above. Thank.







Objects in the application



I really respect functional programming, but I have devoted most of my professional activity to creating applications consisting of objects. JavaScript impresses me with the fact that the functions in it are also objects. When creating applications, I think of objects, this is my professional deformation.







According to the lifetime, objects in the application can be divided into the following categories:









In this regard, in programming there are such design patterns as:









That is, from my point of view, the application consists of permanently existing loners who either perform the required operations themselves or generate temporary objects to execute them.







Object Container



Dependency injection is an approach that makes it easy to create objects in an application. That is, in the application there is a special object that โ€œknowsโ€ how to create all other objects. Such an object is called an Object Container (sometimes an Object Manager).







The Object Container is not a Divine Object , because its task is only to create significant objects of the application and providing access to other objects to them. The vast majority of application objects, being generated by the Container and located in it, have no idea about the Container itself. They can be placed in any other environment, provided with the necessary dependencies, and they will also function wonderfully there (testers know what I mean).







Place of implementation



By and large, there are two ways to inject dependencies into an object:









I basically used the first approach, so I will continue the description with the point of view of dependency injection through the constructor.







Let's say that we have an application consisting of three objects:







image







In PHP (this language with long-standing DI traditions, I currently have active luggage, I will move on to JS a bit later) a similar situation could be reflected in this way:







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





This information should be enough so that a DI container (for example, league / container ), if configured appropriately, could, upon request to create an Application



object, create its dependencies Service



and Config



and pass them parameters to the constructor of the Application



object.







Dependency IDs



How does the Object Container understand that the constructor of the Application



object requires two Config



and Service



objects? By analyzing the object through the Reflection API ( Java , PHP ) or by analyzing the object code directly (code annotations). That is, in the general case, we can determine the names of the variables that the constructor of the object expects to see at the input, and if the language is typable, we can also get the types of these variables.







Thus, as identifiers of objects, the Container can operate with either the names of the input parameters of the constructor or the types of input parameters.







Create Objects



The object can be explicitly created by the programmer and placed in the Container under the corresponding identifier (for example, "configuration")







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





and can be created by the Container according to certain specific rules. These rules, by and large, come down to matching the identifier of the object to its code. Rules can be set explicitly (mapping in the form of code, XML, JSON, ...)







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





or in the form of some algorithm:







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





In PHP, rules for comparing a class name to a file with its source code are standardized ( PSR-4 ); in Java, matching is done at the JVM ( class loader ) configuration level. If the Container provides an automatic search for the sources when creating objects, then the class names are good enough identifiers for objects in such a Container.







Namespaces



Usually in a project, in addition to its own code, third-party modules are also used. With the advent of dependency managers (maven, composer, npm), the use of modules has been greatly simplified, and the number of modules in projects has increased greatly. Namespaces allow code elements of the same name to exist in a single project from various modules (classes, functions, constants).







There are languages โ€‹โ€‹in which the namespace is built in initially (Java):







 package vendor.project.module.folder;
      
      





There are languages โ€‹โ€‹in which the namespace was added during the development of the language (PHP):







 namespace Vendor\Project\Module\Folder;
      
      





A good namespace implementation allows you to unambiguously address any element of the code:







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





The namespace solves the problem of organizing many software elements in a project, and the file structure solves the problem of organizing files on disk. Therefore, there is not only much in common between them, and sometimes very much - in Java, for example, a public class in the namespace must uniquely be attached to a file with the code of this class.







Thus, using the identifier of an object class in the project namespace as the object identifiers in the Container is a good idea and can serve as the basis for creating rules for the automatic detection of source codes when creating the desired object.







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





Code startup



In PHP composer



module namespace is mapped to the file system inside the module in the composer.json



module descriptor:







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





The JS community could do a similar mapping in package.json



if there were namespaces in JS.







JS dependency identifiers



Above, I indicated that the Container can use either the names of the constructor's input parameters or the types of input parameters as identifiers. The problem is that:







  1. JS is a language with dynamic typing and does not provide for specifying types when declaring a function.
  2. JS uses minifiers that can rename input parameters.


The developers of the awilix DI container suggest using the object as the only input parameter to the constructor, and the properties of this object as dependencies:







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





The object property identifier in JS may consist of alphanumeric characters, "_" and "$", and may not begin with a digit.







Since we will need to map dependency identifiers to the path to their sources in the file system for autoload, it is better to abandon the use of "$" and use the experience of PHP. Before the namespace



operator appeared in some frameworks (for example, in Zend 1), such names for classes were used:







 class Zend_Config_Writer_Json {...}
      
      





Thus, we could reflect our application of three objects ( Application



, Config



, Service



) on JS something like this:







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





If we post the code of each class:







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





in your file inside our project module:









Then we can associate the root directory of the module with the root "namespace" of the module in the configuration of the Container:







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





and then, starting from this information, construct the path to the corresponding sources ( ${module_root}/src/Config.js



) based on the dependency identifier ( ${module_root}/src/Config.js



).







ES6 Modules



ES6 offers a general design for loading ES6 modules:







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





Since we need to attach one object (class) to one file, it makes sense in the source to export this class by default:







 export default class Vendor_Project_Path_To_Source_With_Something {...}
      
      





In principle, itโ€™s possible not to write such a long name for the class, just Something



will work too, but in Zend 1 they wrote and did not break, and the uniqueness of the class name within the project positively affects both the capabilities of the IDE (autocomplete and contextual prompts) and when debugging:







image







Importing a class and creating an object in this case looks like this:







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





Front & Back Import



Import works both in the browser and in nodejs, but there are nuances. For example, the browser does not understand the import of nodejs modules:







 import path from "path";
      
      





We get an error in the browser:







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





That is, if we want our code to work both in the browser and in nodejs, we cannot use constructs that the browser or nodejs does not understand. I specifically focus on this, because such a conclusion is too natural to think about it. How to breathe.







DI's place in modern web applications



This is purely my personal opinion, due to my personal experience, like everything else in this publication.







In web-based applications, JS takes almost no alternative its place at the front, in the browser. On the server side, Java, PHP, .Net, Ruby, python, densely dug in ... But with the advent of nodejs, JavaScript also penetrated the server. And the technologies used in other languages, including DI, began to penetrate server-side JS.







The development of JavaScript is due to the asynchronous operation of the code in the browser. Asynchrony is not an exclusive feature of JS, but rather innate. Now the presence of JS on both the server and the front does not surprise anyone, but rather encourages the use of the same approaches at both ends of the web application. And the same code. Of course, the front and the back are too different in essence and in the tasks to be solved to use the same code both there and there. But we can assume that in a more or less complex application there will be browser, server and general code.







DI is already in use at the front, in RequireJS :







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





True, here the identifiers of dependencies are written explicitly and immediately in the form of links to the sources (you can configure the mapping of identifiers in the bootloader config).







In modern web applications, DI exists not only on the server side, but also in the browser.







What does Michael Jackson have to do with it?



When ES-modules are enabled in nodejs (the --experimental-modules



flag), the engine identifies the contents of files with the *.mjs



as EcmaScript-modules (unlike Common-modules with the *.cjs



).







This approach is sometimes called the " Michael Jackson Solution ", and the scripts are called Michael Jackson Scripts ( *.mjs



).







I agree that the so-so intrigue with the KDPV was resolved, but ... the guysโ€™s camon , Michael Jackson ...







Yet Another DI Implementation



Well, as expected, your own bicycle DI module - @ teqfw / di







This is not a ready-to-fight solution, but rather a fundamental implementation. All dependencies should be ES-modules and use common features for the browser and nodejs.







To resolve dependencies, the module uses the awilix approach:







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





To run the back example:







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





on server:







 $ node --experimental-modules main.mjs
      
      





To run the front example ( 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>
      
      





you need to put the module on the server and open the example.html



page in the browser (or use the capabilities of the IDE). If you open example.html



directly, then the error in Chrom is:







 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.
      
      





If everything went well, then in the console (browser or nodejs) there will be something like this:







 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.
      
      





Summary



AMD, CommonJS, UMD?







ESM !








All Articles