-
Notifications
You must be signed in to change notification settings - Fork 14
Guide
- Getting started
- Module resolver
- Dependency Injection
- Module lifecycle
- Services
- Extend and Override Modules
Define a module:
/core/say.js
module.exports = {
sayHello: function() {
console.log("Hello scattered world!");
}
}
Add the particle descriptor:
/core/particle.json
{
"name": "helloComponent"
}
Initialize the Scatter container and register the new particle directory:
/app.js
var scatter = new Scatter();
scatter.registerParticle([
__dirname + '/core'
]);
//Load and use the module
scatter.load('hello').then(function(mod) {
mod.sayHello();
});
(Usually you will never need to manually load
a module, modules are normally wired together using dependency injection )
In Scatter, you don't need to manually register each module with the DI container (although you can), modules are automatically resolved from the particle directories specified during the container creation.
Module Naming
Each module is named after it's relative path from its particle directory + the name of the file (without the .js
extension). For example if we add a particle from the directory:
/project/lib/
and then define a module:
/project/lib/foo/bar.js
The module will be available in the DI container with the name:
foo/bar
Particles and subparticles
A Particle in Scatter is a container for a set of modules. To define a particle directory it is necessary to create a particle.json
file in the particle directory itself. The json file must contain at least a name
property, for example:
myparticledir/particle.json
:
{
"name": "<particle name>"
}
If particle.json
is not found, the directory will not be added as particle to the Scatter DI container.
A particle might define multiple subparticles by specifying the subparticles
property (containing relative paths to subparticles directories), for example:
myparticledir/particle.json
:
{
"name": "<particle name>",
"subparticles": [
"subDir1", "subDir2"
]
}
Note: Each Subparticle dir must define its own particle.json
file. When specifying subparticles the "parent" particle directory is not registered in the DI container, only subparticles will be.
Importing modules from the node_modules
directory
You can automatically register all the Scatter particles in the node_modules
directory by using the method scatter.setNodeModulesDir. This will also allow you to require standard npm modules from the DI container with the syntax npm!<module name>
Dependency injection is achieved by defining a __module
descriptor in your module. With it you can control how the module is instantiated, but more importantly, how it's wired with other modules.
The most intuitive type of dependency injection in Scatter is achieved by using factories.
module.exports = function(person) {
return {
sayHello: function() {
console.log("Hello " + person.name + "!");
}
};
};
module.exports.__module = {
args: ['models/person']
};
You can also use a constructor:
function Hello(person) {
this.person = person;
};
Hello.prototype = {};
Hello.prototype.sayHello: function() {
console.log("Hello " + person.name + "!");
}
module.exports = Hello;
module.exports.__module = {
args: ['models/person']
};
You can even inject properties directly into the module instance (injected after the module is instantiated)
var self = module.exports = {
sayHello: function() {
console.log("Hello " + self.person.name + "!");
}
};
module.exports.__module = {
properties: {
person: 'models/person'
}
};
A module in Scatter has 3 states:
- Resolved - The module is known to Scatter, it's kept in its internal data structures, but it's not ready yet to use.
-
Instantiated - The module is instantiated (e.g. the factory or constructor is invoked). At this point the module instance exists but it's not fully usable yet (the properties are not injected and
initialize
is not invoked). -
Initialized - The module is initialized, the
initialize
method was already invoked and all the dependencies are injected and initialized as well.
All modules are by default injected in an initialized state. Sometimes though you might have loops between dependencies, in this case you should know how the module lifecycle works to find workarounds.
For example, you will have a deadlock if you have two modules which try to inject each other at instantiation time (using args
with factory/constructor). To go around this, just inject one of the two modules with the properties
command being sure you require an instance only of that module using the dependency delayinit!<module name>
, otherwise you will have a deadlock at initialization time.
The same technique can be applied with deadlocks at initialization time (injected with properties
or initialize
).
PS: Don't worry, even with delayinit!<module name>
the module will be fully initialized as soon as the main application cycle starts.
Example
/core/foo.js
:
module.exports = function(bar) {
//bar is just the module instance, you can assign it,
//but be careful when using it at this point
var self = {
doSomething: function() {
console.log(bar.name);
}
};
return self;
}
module.exports.__module = {
args: ['delayinit!bar']
}
/core/bar.js
:
module.exports = function() {
var self = {
name: 'bar',
useFoo: function() {
self.foo.doSomething();
}
};
return self;
}
module.exports.__module = {
properties: {foo: 'foo'}
}
One of the most powerful features of Scatter is the services framework. You can use it to implement extension points, hooks or emit events.
To define a service, create a function in your module then declare it in your __module
descriptor, using the provides
property. The service name is in the form <namespace>/<method>
, where <namespace>
must be the module namespace (or a parent namespace) and method
is the service identifier.
To use a service inject a dependency in the format svc!<namespace>/<method>
, then simply invoke the resulting dependency as if it was a normal function. By default the services will be invoked in series (sequence
), but Scatter also allows you to invoke the service using other modes: any()
and pipeline()
. To explicitly specify the mode, pass it as an option to the dependency: svc|any!<namespace>/<service name>
Here is an example of how you can use it to register some routes in an express
application.
/components/home/routes/home.js
:
var self = module.exports = {
home: function(req, res) {
...
},
register: function(express) {
express.get('/', self.home);
}
};
self.__module = {
provides: ['routes/register']
}
/components/aPlugin/routes/person.js
:
var self = module.exports = {
view: function(req, res) {
...
},
register: function(express) {
express.get('/person', self.view);
}
};
self.__module = {
provides: ['routes/register']
}
Now somewhere else in your project you can register all your routes at once using the register
service:
/components/core/expressApp.js
:
...
var express = require('express');
module.exports = function(registerRoutes) {
var self = {
initializeApp: function() {
...
return registerRoutes(self.express);
}
}
return self;
};
module.exports.__module = {
args: ['svc!routes/register'],
provides: ['initializeApp']
}
Then the app entry point:
/app.js
:
var scatter = new Scatter();
scatter.registerParticle(__dirname + '/components/*');
scatter.load('svc|sequence!initializeApp').then(function(initializeApp) {
return initializeApp();
}).then(function() {
console.log('App initialized');
});
Notice how you can require a service exactly in the same way you require a module! The service becomes a dependency!
Another cool thing, is that the three modules do not know of the existence of each other, they are totally decoupled.
If you need a particular order of execution between your services, you can easily define it by specifying it in the __module
descriptor, for example:
/components/aPlugin/routes/person.js
:
...
module.exports.__module = {
...
provides: {
register: {
after: "routes/home"
}
}
}
The real power of Scatter resides in the fact that every module can be overridden or extended by another particle. This way it is possible to change the behavior of any module in any particle!
To declare that a particle is going to override the modules of another one it is necessary to add the property overrides
into the particle.json
descriptor, for example:
/components/EnhancedUser/particle.json
{
"name": "EnhancedUser",
"overrides": ["BasicUser"]
}
Now as let's see how it's possible to extend an hypothetical User
module:
/components/BasicUser/User.js
var self = module.exports = {
username: "Mario",
hello: function() {
console.log("Hello " + self.username);
}
}
Then to extend it we ca just do the following:
/components/EnhancedUser/User.js
module.exports = function(User) {
User.username = "Luigi";
return User;
}
module.exports.__module = {
args: ['User']
}
The module modifies the module User
by changing its username to Luigi
. This is just a basic change but thanks to the flexibility of JavaScript we can transform the parent module in many different ways, or even return a totally different object.
Notice the dependency User
that is injected into the factory. Since we specified that the particle EnhancedUser
overrides the particle BasicUser
, Scatter knows how to resolve the User
module from the dependency tree.
We can now initialize Scatter and load the User
module:
/app.js
:
var scatter = new Scatter();
scatter.registerParticle(__dirname + '/components/*');
scatter.load('User').then(function(user) {
user.hello();
});
What the code above will print?