I am new at JavaScript. I wonder how dependency injection is being implemented in JavaScript? I searched the internet but couldn\'t find anything.
Even though this is an old question I feel the urge. ;)
//dependency injection
class Thing1 {
constructor(aThing){
this.otherThing = aThing;
}
}
class Thing2 {}
const thing = new Thing1(new Thing2())
//dependency inversion
class Thing1 {
constructor({
read = null
} = {}){
if(typeof read !== 'function'){
//establish a simple contract
throw new TypeError(read + ' is not a function.');
}
this._read = read;
//Somewhere an instance of Thing1()
//will call this._read()
}
}
class Thing2 {
read(){
//read something
}
}
const thing2 = new Thing2();
const thing1 = new Thing1({
read(){
//Here is the equivalent to the so called "interface"
return thing2.read();
}
});
I am new at JavaScript. I wonder how dependency injection is being implemented in JavaScript? I searched the internet but couldn't find anything.
To be completely honest after working with JavaScript (mainly on the server side) and the whole ecosystem for a few years, I get the feeling that dependency injection (not to mention containers) hasn't really made it into a regular JS programmer's toolbox. That's probably the reason why there's not much information about it out there (it's getting better though).
As opposed to a language such as Java, you cannot rely on static types in JavaScript. This fact alone rules out the traditional way of declaring dependencies via interfaces. You can of course add types to JS (see Flow) but these get elided before the code gets executed. The same applies for TypeScript but I believe there's a way to preserve the types as non enforced metadata. Further, JavaScript doesn't support annotations (although there's a proposal for it).
People have been getting around the limitations in various ways. Some containers parse the function/class definition (as in they call .toString()
on the passed function/class and parse the resulting string) and look for dependencies based on the names, some require functions/classes to provide a property/static method to get the list of dependencies.
I've been working myself on a container called Ashley, which simply asks for the dependencies as part of the binding process. No further inspection required.
container.instance('Client', Client, ['DependencyA', 'DependencyB']);
container.instance('DependencyA', DependencyA, ['DependencyC']);
container.instance('DependencyB', DependencyB, ['DependencyC']);
container.instance('DependencyC', DependencyC, [], {
scope: 'Prototype', // Defaults to Singleton
initialize: true,
deinitialize: true
});
const client = await container.resolve('Client');
More examples on GitHub.
For me yusufaytas answer was exactly what I needed! The only missing features were:
I wanted to have the ability to do something like this:
Injector.register('someDependency', function () {
return new ConcreteDependency();
});
function SomeViewModel(userId, someDependency) {
this.userId = userId;
this.someDependency = someDependency;
}
var myVm = Injector.get(SomeViewModel, { "userId": "1234" });
So I ended up with the following code:
var Injector = {
factories = {},
singletons = {},
register: function (key, factory) {
this.factories[key] = factory;
},
registerSingle: function (key, instance) {
this.singletons[key] = instance;
},
get: function (CTor, params) {
var dependencies = this.resolveDependencies(CTor, params);
// a workaround to allow calling a constructor through .apply
// see https://stackoverflow.com/questions/1606797/use-of-apply-with-new-operator-is-this-possible
function MiddlemanCTor() {
CTor.apply(this, dependencies);
}
MiddlemanCTor.prototype = CTor.prototype;
return new MiddlemanCTor();
},
resolveDependencies: function(CTor, params) {
params = params || {};
var args = this.getArguments(CTor);
var dependencies = [];
for (var i = 0; i < args.length; i++) {
var paramName = args[i];
var factory = this.factories[paramName];
// resolve dependency using:
// 1. parameters supplied by caller
// 2. registered factories
// 3. registered singletons
var dependency = params[paramName] ||
(typeof factory === "function" ? factory() : undefined) ||
this.singletons[paramName];
dependencies.push(dependency);
}
return dependencies;
}
getArguments: func(func) {
// Regex from require.js
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var args = func.toString().match(FN_ARGS)[1].split(',').map(function (str) {
return str.trim();
});
return args;
}
};
I've been using this solution for a few years now. As I moved my code base to TypeScript the solution evolved with it to support both TypeScript and JavaScript. After quite a while that the code was running in production I recently (two days ago) published a library based on this solution. Feel free to check it out, open issues, etc.
peppermint-di
Injecting is a lightweight yet powerful DI container, it can well handle promise injection.
Injecting Homepage
Source Code only 100+ lines.
Test Cases to see its examples.
bubble-di is a lightweight DI container for Javascript and Typescript.
It enables you to register factory methods (callbacks) or instances. Below is a simple example (more examples).
npm install --save bubble-di
var {DiContainer} = require("bubble-di");
// import { DiContainer } from "bubble-di";
DiContainer.setContainer(new DiContainer());
class Bar { sayBar(){ console.log("bar"); } }
class Baz { sayBaz(){ console.log("baz"); } }
class Foo {
constructor (bar, baz)
{
bar.sayBar();
baz.sayBaz();
// ...
}
};
DiContainer.getContainer().registerInstance("bar", new Bar());
DiContainer.getContainer().registerInstance("baz", new Baz());
DiContainer.getContainer().register("foo", {
dependencies: ["bar", "baz"],
factoryMethod: (bar, baz) => new Foo(bar, baz) },
);
const foo = DiContainer.getContainer().resolve("foo"); // will print "bar" and "baz".
You can use AngularJS as an example. Whether it is a good thing, you have to decide for yourself. I wrote a week ago an article about demistifying dependency injection in AngularJS. Here you can read the code from the article:
// The following simplified code is partly taken from the AngularJS source code:
// https://github.com/angular/angular.js/blob/master/src/auto/injector.js#L63
function inject(fn, variablesToInject) {
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
if (typeof fn === 'function' && fn.length) {
var fnText = fn.toString(); // getting the source code of the function
fnText = fnText.replace(STRIP_COMMENTS, ''); // stripping comments like function(/*string*/ a) {}
var matches = fnText.match(FN_ARGS); // finding arguments
var argNames = matches[1].split(FN_ARG_SPLIT); // finding each argument name
var newArgs = [];
for (var i = 0, l = argNames.length; i < l; i++) {
var argName = argNames[i].trim();
if (!variablesToInject.hasOwnProperty(argName)) {
// the argument cannot be injected
throw new Error("Unknown argument: '" + argName + "'. This cannot be injected.");
}
newArgs.push(variablesToInject[argName]);
}
fn.apply(window, newArgs);
}
}
function sum(x, y) {
console.log(x + y);
}
inject(sum, {
x: 5,
y: 6
}); // should print 11
inject(sum, {
x: 13,
y: 45
}); // should print 58
inject(sum, {
x: 33,
z: 1 // we are missing 'y'
}); // should throw an error: Unknown argument: 'y'. This cannot be injected.