UNPKG

habitjs

Version:

Modules and Dependency Injection for the browser and Node

476 lines (329 loc) 10.3 kB
#Habit. Javascript Dependency Injection for the Browser and Node. ##Introduction 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. ##Why? Dependency Injection and Mocking. Modules are a side effect. ##Usage ### Defining Modules 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. #### Out of order definitions. 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. ### Using Modules 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 and relative paths. 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`. ### Mocking and Unmocking 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()` ### Dependency Injection Closures 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!' ####How do Dependency Injection closures work? 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. ####Creating, resolving, mocking and unmocking modules inside dependency injection closures. 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. ### Circular Dependencies 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()); }); ``` ### Usage with node.js 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. #### Node API 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.