Dependency Injection for JavaScript
I missed an IoC container in JavaScript that let me control the lifestyle and lifecycle of
components. I couldn't find a library that gave me such granular control, so
I rolled my own.
npm install --save ankh
ankh
or notComponents may be constructed by ankh
, or not. If they are then they will be managed
by ankh
as configured by their 'lifestyle'. Components created by ankh
are, by default,transient
; meaning you get a new instance for each resolution. ankh
can
guarantee a singleton
instance when you register using { lifestyle: 'singleton'}
.
Components which are managed outside the container, such as instance
or value
registrations,
will ignore any lifestyle directive.
var Ankh = require('ankh')
var ankh = new Ankh()
//register a singleton factory
HandlerFactory.inject = ['http']
function HandlerFactory(http) {
return function handle(command) {
return http(...)
}
}
ankh.factory('handlerFactory',HandlerFactory, {
lifestyle: 'singleton'
})
//register a prototype
Issue.inject = ['cfg']
function Issue(cfg) {
this.cfg = cfg
}
Issue.prototype.doIt = function(){}
ankh.ctor('issue',Issue, {
lifestyle: 'transient'
})
//note that this clones the value, so changes to the object are not reflected
//in the container
var cfg = { root: '/api'}
ankh.value('cfg',cfg)
ankh.instance('bluebird',require('bluebird'))
//first register your services
// with '@impl' as a special key for the instance you want to decorate
MyDecoratingService.inject = ['@impl']
function MyDecoratingService(impl){
//do things to the instance
//return whatever you want
}
ankh.factory('myTargetService',function(){})
ankh.factory('myDecoratingService',MyDecoratingService)
//register the decoration
ankh.decorate('myTargetService','myDecoratingService')
Typically, you won't manually resolve components, but of course during testing this
is useful. ankh
will accept a second parameter object matching the key to the dependency to
override resolution. This is also great for testing for pushing mocks and the like
into your instances.
MyService.inject = ['dep1','dep2']
function MyService(dep1,dep2) {
}
//register
ankh.value('dep1','FOO')
ankh.value('dep2','BAR')
ankh.ctor('svc',MyService)
//resolve
ankh.resolve('svc',{ dep2: 'BAZ'}) // -> use 'BAZ' for dep2 value
.then(function(it) {
//do things with 'svc'
})
ankh.resolve('svc') // -> use 'BAR' for dep2 value
.then(function(it) {
//do things with svc
})
ankh
exposes a start
method that can act as a bootstrapper for an application.
Components may participate in this lifecycle event by attaching an startable
property
on the entity being registered. ankh
will construct the instance (and resolve any
dependencies) and immediately invoke the method name provided by startable
.
Settings.startable = 'load'
function Settings() {
return {
load: function() {
//do stuff
}
}
}
ankh.factory('settings',Settings)
ankh.start().then(...)
Sometimes you don't want to resolve the same dependencies during this period for a component,
so you may provide an inject
array for the method in the same fashion as you
configure your component's dependencies.
Settings.startable = 'load'
function Settings() {
var spec = {
load: function(cache) {
//do stuff with cache
}
}
spec.load.inject = ['localStorage']
return spec
}
ankh.factory('settings',Settings)
ankh.start().then(...)
When ankh
resolves a dependency it will invoke the method name provided by the
special initializable
property on the registered entity. This happens each time
the component is resolved.
User.initializable = 'fetchUser'
function User(xhr) {
var spec = {
userName: undefined
, fetchUser: function(xhr) {
return xhr('profile')
.bind(this)
.tap(function(usr) {
this.userName = usr.name
})
}
}
spec.inject = ['xhr']
return spec
}
Settings.inject = ['user']
function Settings(user) {
return {
sayHi: function() {
console.log('hi',user.userName)
}
}
}
Sometimes a component needs to defer construction of a dependency. This can come up when:
In all these cases, I prefer being explicit about such needs and register that behavior as
a component itself. However sometimes that isn't warranted or possible.
To that end, ankh
exposes a deferrable
method that accepts the key
of the service
you want to make deferrable and an (optional) key to name the resulting component. If you don't
provide a name, it will use {serviceName}Deferred
by convention. Behind the scenesankh
is merely putting the service behind a function closure and forwarding arguments
to defer it's construction on-demand.
Settings.inject = ['localStorage']
function Settings(cache) {
}
ankh.factory('settings',Settings)
ankh.deferrable('settings')
Login.inject = ['settingsDeferred'] //the special sauce
function Login(settingsDeferred) {
return {
localCache: {}
, signin: function() {
//settingsDeferred is a function
//you can even provide your own overrides
return settingsDeferred({ cache: this.localCache })
.then(function(settings) {
//do stuff with new instance
})
}
}
}
ankh
tries hard to tell you if the container has cyclic dependencies.
Decorators can be difficult to detect this.
try {
var graph = ankh.validate() // if OK then an array of execution order is returned
} catch(err) {
//otherwise an error is throw trying to identify failure
}
Run make view-docs
to see pretty documentation
There is an example of creating and using the ankh
container in the /examples
folder.
ankh
uses karma.
You can make test
to runem.
The work here is heavily influenced from the IoC concepts found in Castle Project's
Windsor Inversion of Control Container.
The success of Angular demonstrated for me that this is one of those patterns that
belongs in dynamic language world just as much as it does in static lang.