ContainerJS is a Dependency Injection Container for JavaScript Web Application.
- Dependency Resolution and Injection
- ContainerJS is responsible for the creation of container-managed objects, and the resolution and injection of its dependent components.
- You can specify a dependency in the component definition by JavaScript code, or can be defined declaratively in the class.
- Since there is no interface in JavaScript, dependency resolution is done in the name assigned to the component.
- By using the dependency injection container, you can automate the wiring.
- Because of the component is cached by the container, you can reduce the creation of unnecessary objects.
- Lazy Module Loading
- It loads the required modules lazily and asynchronously by working with the require.js.
- Until the component is actually used by the user's operation or the like, you can delay the loading and evaluation of the JavaScript source.
- Supports Aspect Oriented Programing
- You can weave a method interceptor to a container-managed component.
- You can aggregate a cross-class features like a performance measurement, into the interceptor.
ContainerJS is dependent on the following modules.
In addition, we use the following testing framework.
- jasmine ( MIT License )
- IE9+
- GoogleChrome
- Firefox4+
- IE7,8 with es5-shim ( https://github.com/kriskowal/es5-shim )
Here is an example of the "Hello World". Please also see 'samples/hello-world'.
file layout:
- index.html
- scripts/
- main.js
- require.js
- container.js
- app/
- model.js
- view.js
- utils/
- observable.js
scripts/app/model.js:
define(["utils/observable"], function(Observable){ "use strict"; /** * @class */ var Model = function() {}; Model.prototype = new Observable(); /** * @public */ Model.prototype.initialize = function() { this.fire( "updated", { property: "message", value :"hello world." }); }; return Model; }); scripts/app/view.js:
define(["container"], function( ContainerJS ){ "use strict"; /** * @class */ var View = function() { this.model = ContainerJS.Inject("app.Model"); }; /** * @public */ View.prototype.initialize = function() { this.model.addObserver("updated", function( ev ) { if ( ev.property != "message" ) return; var e = document.getElementById(this.elementId); e.innerHTML = ev.value; }.bind(this)); }; return View; }); scripts/app/main.js:
require.config({ baseUrl: "scripts", }); require(["container"], function( ContainerJS ) { var container = new ContainerJS.Container( function( binder ){ binder.bind("app.View").withProperties({ elementId : "console" }).onInitialize("initialize") .inScope(ContainerJS.Scope.EAGER_SINGLETON); binder.bind("app.Model"); }); container.onEagerSingletonConponentsInitialized.then(function() { container.get("app.Model").then(function( model ){ model.initialize(); }, function( error ) { alert( error.toString() ); }); }); }); index.html:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello World</title> <script type="text/javascript" data-main="scripts/main.js" src="scripts/require.js"></script> </head> <body> <div id="console"></div> </body> </html> Supports the binding of components by the following 5 ways.
- Class Binding
- Prototype Binding
- Object Binding
- Provider Binding
- Instance Binding
- Specifies a class (same as a constructor function) to a component.
- The object that is created by the
newoperator will be the component. - The constructor function is load asynchronously using the require.js's
require(). - You can specify an argument passed to the constructor function by using
withConstructorArgument().
Component Definition:
var container = new ContainerJS.Container( function( binder ){ binder.bind("app.Class"); binder.bind("anotherName").to("app.Class").withConstructorArgument({ foo:"foo", var:ContainerJS.Inject("app.Class") // Dependency injection can also }); }); app/class.js:
define(function(){ /** * @class */ var Class = function(arg) { this.foo = args.foo; this.var = args.var; }; return Class; }); - Specifies a prototype to a component.
- The object that is created by
Object#create(<prototype>)will be the component. - The prototype is load asynchronously using the require.js's
require().
Component Definition:
var container = new ContainerJS.Container( function( binder ){ binder.bind("app.Prototype").asPrototype(); binder.bind("anotherName").toPrototype("app.Prototype", { foo : { value: "foo" } // You can specify the arguments to be passed to `Object#Create()` in the second argument. }); }); app/prototype.js:
define(function(){ /** * @class */ var Prototype = { method : function( arg ) { return arg; } } return Prototype; }); - The object that loaded by requirejs's "require" will be a component.
Component Definition:
var container = new ContainerJS.Container( function( binder ){ binder.bind("app.Object").asObject(); binder.bind("anotherName").toObject("app.Object"); }); app/object.js:
define(function(){ var Obj = { method : function( arg ) { return arg; } } return Obj; }); - Specifies a function to generate the component.
- The function's return value will be the component.
Component Definition:
var container = new ContainerJS.Container( function( binder ){ binder.bind("name").toProvider(function(){ return "foo"; }); }); - Specifies the component itself.
Component Definition:
var container = new ContainerJS.Container( function( binder ){ binder.bind("name").toInstance("foo"); }); By setting the Packaging Policy, where you can control the loading of modules.
This Is the default policy. It assumes that the module are separated per class. A Component is loaded from <A class name "-" was separated>.js following the same path as the namespace.
file layout:
- app/
- foo/
- hoge-hoge.js
- fuga-fuga.js
- foo/
- main.js
app/foo/hoge-hoge.js:
define(function(){ /** * @class */ var HogeHoge = function(arg) {}; return HogeHoge; }); app/foo/fuga-fuga.js:
define(function(){ /** * @class */ var FugaFuga = function(arg) {}; return FugaFuga; }); main.js:
var container = new ContainerJS.Container( function( binder ){ binder.bind("app.foo.HogeHoge"); binder.bind("app.foo.FugaFuga"); }); It assumes that the module are separated per package.
file layout:
- app/
- foo.js
- main.js
app/foo.js:
define(function(){ /** * @class */ var HogeHoge = function(arg) {}; /** * @class */ var FugaFuga = function(arg) {}; return { HogeHoge : HogeHoge, FugaFuga : FugaFuga } }); main.js:
var container = new ContainerJS.Container( function( binder ){ binder.bind("app.foo.HogeHoge") .assign(ContainerJS.PackagingPolicy.MODULE_PER_PACKAGE); binder.bind("app.foo.FugaFuga") .assign(ContainerJS.PackagingPolicy.MODULE_PER_PACKAGE); }); It assumes that all of the classes in a namespace are defined into a single file.
file layout:
- app.js
- main.js
app.js:
define(function(){ /** * @class */ var HogeHoge = function(arg) {}; /** * @class */ var FugaFuga = function(arg) {}; return { foo : { HogeHoge : HogeHoge, FugaFuga : FugaFuga } } }); main.js:
var container = new ContainerJS.Container( function( binder ){ binder.bind("app.foo.HogeHoge") .assign(ContainerJS.PackagingPolicy.SINGLE_FILE); binder.bind("app.foo.FugaFuga") .assign(ContainerJS.PackagingPolicy.SINGLE_FILE); }); In addition to be specified in the component definition, The default packaging policy can also be specified in the constructor arguments of the container.
var container = new ContainerJS.Container( function( binder ){ binder.bind("app.foo.HogeHoge"); binder.bind("app.foo.FugaFuga"); }, ContainerJS.PackagingPolicy.SINGLE_FILE); // specified the default settings by the constructor arguments. Supports the "Singleton", "EagerSingleton", "Prototype". the "Singleton" is the default.
- Singleton
- Creates only one component.
- If you get the same component multiple times, the same component will return always.
- The Components are discarded by
Container#destroy().
- EagerSingleton
- Will return the single instance of like Singleton, an instance will be created when creating the container.
- You can create a component to be effective only to be registered into the container.
- Prototype
- Each time you get a component, and then re-create the component.
Configuration change is done in the inScope() .
var container = new ContainerJS.Container( function( binder ){ binder.bind("Foo").inScope( ContainerJS.Scope.SINGLETON ); // default binder.bind("Bar").inScope( ContainerJS.Scope.EAGER_SINGLETON ); binder.bind("Val").inScope( ContainerJS.Scope.PROTOTYPE ); }); container.onEagerSingletonConponentsInitialized.then(function(){ // called when all eager singleton conponents are initialized. }); If you set a ContainerJS.Inject to the property, the dependent module is searched and Injected by the container.
- Setting a
ContainerJS.Inject, a component will be searched by the property name. - Using
ContainerJS.Inject(name), You can explicitly specify the name of the component to search. - Setting a
ContainerJS.Inject.allorContainerJS.Inject.all(name), An array of components with the specified name will be injected. - Setting a
ContainerJS.Inject.lazily,ContainerJS.Inject.lazily(name),ContainerJS.Inject.all.lazily,ContainerJS.Inject.all.lazily(name), Component(s) to be injected will then be load lazily.- Instead the component, Deferred in order to get the component is injected.
Example:
define(["container"], function(ContainerJS){ /** * @class */ var Class = function() { this.a = ContainerJS.Inject; this.b = ContainerJS.Inject("foo.var"); this.c = ContainerJS.Inject.all; this.d = ContainerJS.Inject.all("foo.var"); this.e = ContainerJS.Inject.lazily; this.f = ContainerJS.Inject.all.lazily("foo.bal"); }; Class.prototype.method1 = { this.e.then( function(component) { // }, function(error) { // } ) }; return Class; }); You can also be injected at the time of the component definition.
var container = new ContainerJS.Container( function( binder ){ binder.bind("Foo").withProperties({ a : ContainerJS.Inject("foo.var") }).withConstructorArgument({ b : ContainerJS.Inject.all.lazily("foo.bal") }); }); You can register a function to be called when creating and destroying components.
- Initialization function is executed after when all of creation phases are completed.
- Destruct function is executed when
container.Container#destroy()is called if the following conditions are met.- the scope of the component is Singleton or EagerSingleton.
- the component is already created.
- You can specify the functions by the component method name or a function.
- If you specify a function, components and containers will be passed as an argument.
Example:
var c = new ContainerJS.Container( function( binder ) { // specifies the component method name. binder.bind( "Foo" ).onInitialize("initialize").onDestroy("dispose"); // specifies a function. binder.bind( "Bar" ).onInitialize( function( component, container ) { component.initialize(); }).onDestroy( function( component, container ) { component.dispose(); }); }); You can weave an interceptor to a method of the component.
- The interceptor would be specified in the function. an object that contains the method name and arguments is passed in the argument.
- you can specify a function to indicate the components and methods to be applied the interceptor in the second argument.
- If the second argument is not explicitly specified, the interceptor applies to all methods of all components.
Example:
var container = new ContainerJS.Container( function( binder ){ binder.bind("app.Component"); binder.bindInterceptor( function( jointpoint ) { jointpoint.methodName; jointpoint.self; jointpoint.arguments; // Arguments. Can be modified. jointpoint.context; // You can store the state that is shared between the invocation of this method. return jointpoint.proceed(); // Returns the result of calling the original method. }, function(binding, component, methodName) { if (binding.name !== "app.Component" ) return false; return methodName === "method1" || methodName === "method2"; } ); });