habitjs
Version:
Modules and Dependency Injection for the browser and Node
476 lines (329 loc) • 10.3 kB
Markdown
Habit is a dependency injection framework for the browser and node,
and a bit of a work in progress. In its present form it is
intended to be used in conjunction with uglify or similar to
allow development across multiple source files and to arrange for and the inclusion
of source maps. (Not required for nodejs) In the future I would like to roll those features
into habit itself ~~along with making it compatible with Node.js~~ (done!).
Habit is heavily inspired by [Dependable](https://github.com/idottv/dependable)
but I decided to write my own version because I wanted it to work in a
browser.
Dependency Injection and Mocking. Modules are a side effect.
A module definition has the following pattern:
```JavaScript
supply(
'moduleName',
['Array', 'of', 'dependency', 'names'],
<module value>);
```
Where module value is either a value or a factory function.
For example...
```JavaScript
supply('word', [], 'bird');
```
... defines a module named word with the `String` value 'bird'.
```Javascript
supply(
'message',
['word'],
function (word) {
return word + ' is the word.';
});
```
... defines a second module named 'message' which uses a factory function to
return the string 'bird is the word.' The factory function will only be called once
(unless its value is mocked/unmocked or it is used inside a dependency injected closure. See below)
Finally a third module can be defined...
```JavaScript
supply(
'speak',
['message'],
function (message) {
return function () {
console.log(message);
};
});
```
... which returns a function which, when called, logs the message 'bird is the word' to the console.
As modules in habit are resolved lazily they can be defined out of order so...
```JavaScript
supply(
'sayHello',
['name'],
function (name) {
return function () {
alert('Hello, ' + name + '.');
};
});
```
... followed by...
```JavaScript
supply(
'name',
[],
'World'
);
```
... would work perfectly well because the dependencies of the 'sayHello' module are not
resolved until it itself is required somewhere else.
Modules can be resolved two ways. If you want to resolve several modules at once you can use a callback.
```JavaScript
need(['speak', 'sayHello'], function (speak, sayHello) {
speak();
sayHello();
});
```
Alternatively, as habit is synchronous, modules can be resolved and returned by the `need` function
```JavaScript
need('speak')();
```
Module names can be any string you like, but to aid in namespacing and grouping related modules, when path-like modules
names such as `path/to/my/awesome/module` are used, the module can declare its dependency using relative path names
so...
```JavaScript
supply(
'my/awesome/module',
[
'../really/special/function',
'./string'
],
function (fun, str) {
//Some specially awesome code here...
});
```
... would define a module with dependencies on the modules `my/really/special/function` and
`my/awesome/string`.
To aid in testing, modules can be temporarily mocked and unmocked using Habit.mock/Habit.unmock
Given a module:
```JavaScript
supply(
'name',
[],
'Dave'
);
```
And a second module:
```JavaScript
supply(
'married',
['name'],
function (name) {
return 'You\'re my wife now, ' + name;
});
```
I can mock the value of the name module with
```JavaScript
Habit.mock('name', 'Brian');
```
Mocking a module causes all modules which rely on it (directly or indirectly) to be re-resolved next time they are
required so
```JavaScript
console.log(need('married'));
```
Would output "You're my wife now, Brian", to the console.
The 'name' module can then be unmocked using:
```JavaScript
Habit.unmock('name');
```
Which again causes all the name modules dependences to be re-resolved so...
```JavaScript
console.log(need('married'));
```
... would once again log "[You're my wife now, Dave](http://en.wikipedia.org/wiki/Papa_Lazarou)" to the console
**An important point to note** is that mocked values are used as is. Functions supplied as mocks will be returned as
functions, not the results of said functions.
If you have a lot of modules mocked they can all be unmocked at the same time using `Habit.unmockAll()`
While habit by default has a single context, separate contexts can by created by passing a third parameter to the need
function.
Given modules:
```JavaScript
supply(
'name',
[]
'World',
);
supply(
'message',
['name'],
function (name) {
return 'Hello, ' + name + '!';
});
supply(
'tell',
['message'],
function (message) {
return function () {
console.log(message);
};
});
```
Then the following code...
```JavaScript
var inject = {
'name': 'Mum'
};
function callback (tell) {
tell();
}
need(['tell'], callback, inject);
```
... would write 'Hello, Mum!' to the console, whereas the code...
```JavaScript
var inject = {
'message': function (name) {
return 'Goodbye, cruel ' + name + '!';
}
};
function callback (tell) {
tell();
}
need(['tell'], callback, inject);
```
... would write 'Goodbye, cruel World!' to the console and the code...
```JavaScript
var inject = {
'tell': function (message) {
alert(message);
}
};
function callback (tell) {
tell();
}
need(['tell'], callback, inject);
```
... would display an alert with the message 'Hello, World!'
By default Habit has a single context. All modules are defined within the same space, but when you create a dependency
injection closure, the context is cloned. Within the closure all the modules are reset so
factory functions will be re-run when the modules are resolved. Any value passed in the inject object will replace the
value of the original module. In fact, since requirements are lazily resolved, module values can be provided in the
inject object which have never been defined in the main context.
The `supply` and `need` global functions and the `Habit.mock` and `Habit.unmock` functions always refer to the main
context. To enable the use of these functions inside a dependency injection closure a special module 'local' can
be required.
```Javascript
var context = {
}
var callback = function (local) {
local.mock('name', 'fred');
console.log(need('name'));
local.unmock('name');
console.log(need('name'));
}
need([], callback, context);
```
The 'local' module has mock, unmock, supply, need, unmockAll, disolve and disolveAll functions.
Being a synchronous framework, Habit does not have any good way to deal with circular dependencies at resolve time so
code such as...
```JavaScript
supply(
'parent',
['child'],
function (child) {
var name = 'Parent'
return {
name: name,
sayHello: function () {
return 'Hello, my name is ' + name + ' and I depend on ' + child.name;
};
}
});
supply(
'child',
['parent'],
function (parent) {
var name = 'Child';
return {
name: name,
sayHello: function () {
return 'Hello, my name is ' + name + ' and I depend on ' + parent.name;
}
};
});
need([parent], function (parent) {
//code in here will never run
});
```
... would throw an error when Habit tried to resolve the parent module. These sorts of dependencies are only
an issue at **resolve** time though. Refactoring the code as follows would solve the issue.
```JavaScript
supply(
'parent',
[],
function () {
var name = 'Parent'
return {
name: name,
sayHello: function () {
return 'Hello, my name is ' + name + ' and I depend on ' + need('child').name;
};
}
});
supply(
'child',
[],
function () {
var name = 'Child';
return {
name: name,
sayHello: function () {
return 'Hello, my name is ' + name + ' and I depend on ' + need('parent').name;
}
};
});
need(['parent', 'child'], function (parent) {
console.log(parent.sayHello(), child.sayHello());
});
```
The NPM Module for habit is called habitjs. Install it with
```bash
npm install habitjs
```
To save you from peppering your code with calls to require Habit will automatically attempt to load locally defined modules with the following caveats.
0. You must use path style module names.
0. The path style module names must reflect the paths in your project.
0. It will only load modules defined in your project and **not** modules loaded via npm.
The modules will be loaded relative to the path of your main module. If you want to change this then call Habit.setRequireRoot with the new location.
If you want to use habit to load external modules then simply wrap your externals in a Habit module. eg.
```JavaScript
//File: ext/fs.js
require('habitjs').supply('ext/fs', [], function () {
return require('fs');
});
```
This then allows you to mock out modules supplied by npm.
To prevent you having to type code like:
```JavaScript
habit = require('habitjs');
habit.Habit.mock('someModule', 'Hello');
```
the object returned from require('habitjs') has the following signature.
- supply: ...
- need: ...
- mock: bound to Habit.mock
- unmock: bound to Habit.unmock
- unmockAll: bound to Habit.unmockAll
- disolve: bound to Habit.disolve
- disolveAll: bound to Habit.disolveAll
- Habit: ...
### Utility Functions
To force a factory function to re-resolve you can use `Habit.disolve('moduleName')`
You can also force the re-resolution of all modules with `Habit.disolveAll()`
### Change Log
Only started at 0.5.0 (oops!)
v0.5.0 Added automatic requiring under node for mocked modules. Improved syntax for node.
v0.5.1 Fixed bug with DI closures under node.
v0.5.2 Actually fixed bug with DI closures under node.