di1 Dependency Injection Container
![codecov.io](https://codecov.io/github/zxbodya/di1/coverage.svg?branch=master)
Dependency Injection Container for JavaScript, with following goals:
- minimalistic, and relatively simple to use
- possibility to create separate Container instance for specific context (user session for example)
- possibility to inject Container instance - useful when dealing with circular dependencies
- good TypeScript support, allowing inferring for most of the things
Installation
npm install di1
API
Service tokens
Services inside DI container, can be referenced using tokens.
Token for registering service or specifying dependency, can be created using createToken
function:
function createToken<ServiceType>(name?: string): Token;
name
argument here, is to be used for debug messages in cases like when having error about cyclic dependency.
There is also a special kind of which allows accessing DI container itself from a service.
Such a token can be created using containerToken
function:
function containerToken(...deps: Injectable[]): Token
Which as an argument optionally has a list of dependencies which should be available for the container,
this affects in which specific container in the hierarchy service using it is to be created.
This is useful for more dynamic cases when requesting dependencies known only at runtime time.
Also, this makes it possible to have cyclic dependencies, in limited cases(which btw, better to be avoided whenever possible).
Service declaration
Typically service should be defined using declareService
funtion:
function declareService(depsObject, factory): Declaration;
which creates a service declaration using object specifying dependencies(list of service tokens or other declarations),
and a factory function to be called with object of dependency instances(having same shape as depsObject
)
There is also a bit more low level version of this:
function declareServiceRaw(factory, ...deps): Declaration
the only difference is that dependencies would be injected as separate function arguments into factory function.
It is slightly closer to how things work internally, but most of the difference is in how it looks.
Supposedly declareService
should be more convenient in most of the cases, and suggested as preferred option.
DI Container
class Container
- Dependency injection container. Represents a registry of service declarations, and cache of already created instances.
Registering a service
To register declaration for given token, or to replace/override previously declared service register
method can be used:
container.register(tokenOrDeclaration, declaration)
Because it is allowed to have service declaration as a dependency - it might be useful to register it to be created on specific layer in the container hierarchy.
(by default it would be created in upper possible layer having all the dependencies)
For this case it is possible to register the declaration in the specific container (effectively limiting it to be created in it or its decedents)
container.register(declaration)
To create a service instance or to use previously created one - get
method is to be used:
container.get(tokenOrDeclaration)
There are cases when there is a need for separate context for services to be created,
while allowing to reuse some service instances from existing context, this can be done createChild
method:
container.createChild()
Usage example
import {
declareService,
declareServiceRaw,
Container,
createToken,
containerToken,
} from 'di1';
const svc1 = declareServiceRaw(() => 1);
const svc2 = declareServiceRaw(one => 1 + one, svc1);
const svc3 = declareService(
{
one: svc1,
two: svc2
},
({ one, two }) => {
return one + two;
}
);
svc3.name = 'svc3';
const svc4 = declareServiceRaw(function svc4(){ return 1 });
const rootContainer = new Container();
rootContainer.get(svc3);
const token1 = createToken('one');
rootContainer.register(token1, svc1);
rootContainer.get(token1);
const childContainer = rootContainer.createChild();
childContainer.get(svc3);
childContainer.register(svc2, declareServiceRaw(() => 0));
childContainer.get(svc3);
rootContainer.get(svc3);
const t1 = createToken('t1');
const t2 = createToken('t2');
const s1 = declareServiceRaw(s2 => {
return { s2 };
}, t2);
const s2 = declareServiceRaw(container => {
return () => {
const s1 = container.get(t1);
};
}, containerToken(t1));
rootContainer.register(t1, s1);
rootContainer.register(t2, s2);
const s1instance = rootContainer.get(t1);
Example use case for child container
This is cases when there is a need to create a child container providing context specific implementations
while reusing not specific whenever possible.
For example, imagine simple shopping app, having following services registered in root container:
products
- provides access to products dbuser
(depends on: session
)cart
(depends on: user
, products
)
But session
in request specific, and so implementation is to be registered in a child container(created per request),
the following will happen:
products
service would be created just once and will be reused across all requestscart
and user
services will be created for each session separately