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.