di-xxl
v1.2.2
Published
Dependency injection library
Downloads
1,357
Maintainers
Readme
Javascript Dependency Injection library
DI-XXL is a dependency injection library facilitating lazy initialization and loose coupling. It is generic, because it can inject everything into anything in multiple ways. Together with support for namespaces, decorators, factory functions, projections and lazy initialization it is a powerful tool ready for complex situations.
In its most basic form a registration of an entity like a class, object or function is done like this
import {DI} from 'di-xxl';
class Foo {}
const descriptor = {
name: 'foo',
ref: Foo
};
DI.set(descriptor)
Foo
gets registered here and will be accessible now by the name foo
. The descriptor
object needs at least
a name
and a ref
erence. Use get
to retrieve an instance of Foo
const foo = DI.get('foo'); // const foo = new Foo();
NOTE (Because I've made this mistake many times:) Don't use DI inside the constructor!!!!
class Bar {
@Inject('foo') foo;
constructor() {
this.foo.do(); // --> Error, this.foo is undefined
}
}
Dependencies are injected after the Bar instances is created (Below you can find more about @decorators)
Parameters
The example above showed the creation of an instance without parameters, so here is an exmaple where the instance is created using params
const descriptor = {
name 'foo',
ref: Foo,
params: [10, 20]
}
DI.set(descriptor);
These params
will be used when none are given when the instance of Foo is requested
DI.get('foo'); // -> new Foo(10, 20)
But when provided, the default parameters are ignored
DI.get<Foo>('foo', {params: [999]}); // -> Returns new Foo(999)
In the above example, DI.get
is typed, telling it what the returned value is, to make consuming the output easier and more obvious.
Injection
In theory you can inject anything into almost everything :) Circular dependencies do not exist, because it is not possible to inject into a constructor. Keep your constructors empty or as small as possible!
So, to inject Foo
into Bar
class Bar {}
const descriptor = {
name: 'bar',
ref: Bar,
inject: [{property: 'foo', name: 'foo'}]
};
DI.set(descriptor);
This will assign an instance of Foo
to the foo
property of Bar on first usage. Lazy initialization
is used here, meaning that Foo will be created when it is used for the first time. To disable this
feature set the lazy
option to false
const descriptor = {
name: 'bar',
ref: Bar,
inject: [{property: 'foo', name: 'foo', lazy: false}]
};
ACTIONS
Anything can be registered, like classes, objects, but also normal functions (not constructors). If a function is not a constructor you need to instruct DI-XXL what it should do, return the function or call it first and return the output
const descriptor = {
name: 'double',
ref: num => num * 2,
action: DI.ACTIONS.NONE
};
DI.set(descriptor);
const double = DI.get('double');
double(10); // -> 20
or
const descriptor = {
name: 'double',
ref: base => num => base + num * 2,
action: DI.ACTIONS.INVOKE
}
In the last example DI-XXL will invoke the reference using the provided parameters and return the output
const double = DI.get('double', {params: [10]});
double(2); // -> 14
Singletons
To turn a class into a singleton, add the singleton flag to the descriptor
const descriptor = {
name: 'foo',
ref: Foo,
singleton: true
}
This will also work with Objects. By default (if not specified) on object is a singleton
const descriptor = {
name: 'app',
ref: {}
}
DI.set(descriptor);
let app = DI.get('app')
app.count = 1
app = DI.get('app');
console.log(app.count); // -> 1
But if you set that flag to false, DI-XXL returns a new object, internally using Object.create
const descriptor = {
name: 'app',
ref: {},
singleton: false
}
DI.set(descriptor);
let app = DI.get('app')
app.count = 1
app = DI.get('app');
console.log(app.count); // -> undefined
Inherit
In case you have almost identical descriptors for two different entities, one can inherit the other
descriptor = {
name: 'Bar',
ref: Bar,
inherit: 'foo'
}
Factories
When a class produce instances of an other class
class Bar {
getFoo(input) {
return new Foo(input);
}
}
it can be rewritten with DI-XXL using Factories
class Bar {
getInstance() {
return this.creator();
}
}
descriptor = {
name: 'bar',
ref: Bar,
inject: [{property: 'creator', factory: 'foo'}] // Each entity has a factory!
}
The factory function, which produces instances of Foo
, is injected into the creator
property of bar
const bar = DI.get('bar');
const foo = bar.creator({params: [1,2]}); // new Foo(1,2)
Everything registered in DI-XXL has by default a factory. For example
class Bar { /* ... */ }
DI.set({name: 'xyz', ref: Bar});
const factory = DI.getFactory('xyz', { params: [1,2]});
let bar = factory() // -> new Bar(1,2)
bar = factory({params: [3,4]}) // -> new Bar(3,4)
Projections
Projections let you map an entity name to an other
DI.setProjection({'foo': 'bar'}); // Is the same:
const something = DI.get('foo');
results in something instanceof Bar
. Projections can be used, for example, to change the behaviour of you application dynamically, based on user action.
Namespaces
Namespaces help to structure your entities in a descriptive way. A namespace is a prefix of the entity name
user.overview.profile
with user.overview
being the namespace. Try to keep your entity names unique within the whole namespace. For example
user.profile
user.overview.profile
profile
is not unique!! As long as you know what your are doing this isn't a problem. The reason behind this is how namespaces are implemented, for example
class User { ... }
DI.set({
name: 'user.overview.profile',
ref: User,
inject: [
{property: 'list', name: 'user.widgets.list'},
{property: 'source', name: 'user.data.source'}]});
DI.get('user.overview.profile');
The list
and source
entities, although exactly specified, will be searched for within the namespace from the root up.
It means that DI-XXL will look for list
using the following entity names
list --> no
user.list --> no
user.widgets.list --> yes
This allows you to redefine entities without replacing the original
DI.set({ name: 'user.list', ....});
This time the search for list
looks like
list --> no
user.list --> yes
It will not find user.widgets.list
. This is the default lookup direction (DI.DIRECTIONS.PARENT_TO_CHILD
), but you can reverse the lookup
DI.get('user.overview.profile', {lookup: DI.DIRECTIONS.CHILD_TO_PARENT});
So far we have only talked about the entities from the inject
list, but this search pattern is also applied on the entity request, with one exception, the first attempt is always the exact name provided
// DI.DIRECTIONS.CHILD_TO_PARENT
user.overview.profile
user.profile
profile
// DI.DIRECTIONS.PARENT_TO_CHIDL
user.overview.profile
profile
user.profile
Roles
Each entity can have a role
and a reject
and accept
list of roles
const descriptor = {
name: 'service.user',
...
role: 'service'
accept: ['service'],
reject: ['component']
}
If you specify accept
all injected entities need to have a role present in the list. But if you define reject
everything can be injected except for the roles defined in the reject list.
@Decorators
As of this writing you have to use a couple of babel plugins to get @decorators
up and running, or if you're using typescript make sure to set the experimentalDecorators
option to true in tsconfig.json
.
With Decorators you can define all dependency related configuration inside the class itself
import {Injectable, Inject} from 'di-xxl';
@Injectable({name: 'foo'})
class Foo {
sum(a, b) { return a + b }
}
@Injectable({name: 'Bar'})
class Bar {
@Inject('foo')
addService
constructor(base = 0) {
this.total = base;
}
add(val) {
return this.addService(this.base, val);
}
}
Which is equivalent to
import {ID} from 'di-xxl';
DI.set({
name: 'foo',
ref: Foo
});
DI.set({
name: 'bar',
ref: Bar,
inject: [{property: 'addService', name: 'foo'}]
});
The @Inject
also accepts an object instead of just the name/string
@Inject({ name: 'foo', lazy: false }) addService;
Please note that this might not work out of the box when you're using Typescript. Read about di executable to the rescue
below to work around this issue!
The @Injectable
statements are directly executed, meaning that they are immediately available
import {ID} from 'di-xxl';
let bar = DI.get('bar', {params: [100]});
bar.add(1); // -> 101
Checkout the unit tests fixtures file for more advanced use cases
Inject into a constructor
Ok, if you really really really have to do this you can of course do it ... yourself :)
const params = [DI.get('foo'), 10];
const bar = DI.get('bar', {params});
di
executable to the rescue
Unfortunately you cannot use the @decorators in combination with Typescript out of the box. Typescript ignores files which are not used directly, for example
file: foo.ts
@Injectable({name: 'foo'})
class Foo {
sum(a, b) { return a + b }
}
file index.ts
import { DI } from 'di-xxl';
const foo = DI.get('foo'); // -> foo === undefined
Now, when Typescript compiles index.ts
it has no notion of Foo
, so it ignores that file,
meaning the @Injectable
is never executed. This can be fixed by using Foo
inside index.ts
import { DI } from 'di-xxl';
import { Foo } from './foo';
Foo;
const foo = DI.get('foo'); // -> foo === Foo instance
This is exactly what ./node_modules/.bin/di
does
$> di ./src/index.ts
What this does, it creates a file called ./src/index-id.ts
with all the files using @Injectable
injected
import { DI } from 'di-xxl';
import { Foo } from './foo';Foo;
const foo = DI.get('foo'); // -> foo === Foo instance
But it will also behave like ts-node
, because after it has created index-id.ts
it will run
ts-node ./src/index-di.ts
But if only compiling is what you need and not running the code with ts-node
you can
$> di -c tsc ./src/index.ts
And if you need, for example, to specifiy a custom configfile for tsc do
$> di -c tsc ./src/index.ts -- -p my-special-tsconfig.json
Everything after the --
will be used as arguments for the command you specifiy with -c
.
Below is a listing of options to further tune the behavior of this tool:
Options:
--help Show help [boolean]
--version Show version number [boolean]
--command, -c After injecting dependencies, it runs the command. Default argument is is `ts-node`
--base, -b Base path to the root of the source files [string]
--debug, -d Enable debug messages [boolean]
--entry, -e Entry filename [string]
--include, -i List of paths/files to include [string]
--pattern, -p Glob patterns specifying filenames with wildcard characters, defaults to **/*.ts [string]
--output, -o Output file relative to `base. Defaults to `<entryfile>`-di.ts` [string]
Examples:
$> di ./src/main.ts -- Run main.ts using ts-node
$> di -c ./src/main.ts -- This runs the code using `ts-node`
$> di -b ./src -e index.ts -p '**/*.ts' -o out.ts -- Run the code
$> di -b ./src index.ts -- --thread 10 -- Run `ts-node ./src/index-di.ts --thread 10`
$> di -c -b ./src -e index.ts -p '**/*.ts' -o out.ts -- Compiles all code with `tsc`
$> di -c 'yarn build' -b ./src -e index.ts -o out.ts -- Injects and runs `yarn build`
$> di -c yarn -b ./src -e index.ts -o out.ts -- build -- Same as above
Here is a more complex example
$> di -d -c tsc -b ./src -i 'frontend,shared' -e 'Foo,Bar' frontend/main.ts -- -p tsconfig-frontend.json
It might be useful to add the file created by di
to your .gitignore
file!
More information
A lot more advanced use-cases are available inside the unit test files.
Installation
Install this library with yarn
or npm
$> yarn add di-xxl
or
$> npm install di-xxl
Commands
Convert DI--XXL into an UMD and ES5 library + a minified version in ./dist
$> yarn build
Unit testing
$> yarn test
Linting
$> yarn lint
Generate documentation (jsdoc)
$> yarn doc
Run benchmarks on different aspects of DI-XXL
$> yarn bench
Run in the browser
There are a couple of ways to run this library in the browser. If you're project doesn't support
import
or require
use browserify
. For es2015 use babelify
$> ./node_modules/.bin/browserify index.js -o bundle.js -t [ babelify --presets [ env ] ]
and for es5 you only need to do
$> ./node_modules/.bin/browserify index.js -o bundle.js