Awilix
Simple Inversion of Control (IoC) container for Node with dependency resolution support. Make IoC great again!
Installation
npm install awilix --save
Requires Node v4 or above, and possibly npm v3.
Quick start
Awilix has a pretty simple API. At minimum, you need to do 2 things:
- Create a container
- Register some modules in it
index.js
const awilix = require('awilix');
const database = require('./db');
const container = awilix.createContainer();
container.register({
db: database
});
container.db.query('SELECT ...');
Awilix: a primer
So the above example is not very interesting though. A reasonably sized project has a few directories with modules that may (or may not) depend on one another.
You could just require
them all in the correct order, but where's the fun in that? Nah, let's have Awilix auto-load our modules and hook them up to the container.
For this mini-tutorial, we will use a todos API as an example.
services/todosService.js
function getTodos(container, searchText) {
const todos = container.todos;
return todos.getTodos({ searchText: searchText }).then(result => {
return result;
});
}
module.exports.getTodos = getTodos;
module.exports.default = (container) => {
container.register({
todosService: container.bindAll({
getTodos: getTodos
})
});
};
The first bit is pretty standard, apart from the container
argument. We basically just treat it as an object that has all the stuff we need - in this
case, a todos repository (exposed as todos
). How this came to be, we will find out... right now!
repositories/todosRepository.js
export class TodosRepository {
constructor(db) {
this.db = db;
}
getTodos(query) {
return this.db.sql(`select * from todos where text like '%${query.searchText}%'`).then(result => {
return result.map(todo => {...});
});
}
}
export default function (container) {
container.dependsOn(['db'], () => {
container.register({
todos: new TodosRepository(container.db)
})
});
}
This time we used a class (instead of functions that take the container as the first argument), and so we used container.dependsOn
to defer registration
until the db
module was ready.
For good measure, let's cover the database module as well.
db/db.js
const someDbProvider = require('some-db-provider');
export function connect() {
return someDbProvider.connect({
host: 'localhost'
});
}
export default function(container) {
return connect().then(database => {
container.register({
db: database
})
});
}
Okay! We have written our modules, now we need to connect them.
In our app's main script (where we imported Awilix), we are going to load our modules. For clarity, I've included everything here.
index.js
const awilix = require('awilix');
const container = awilix.createContainer();
container.loadModules([
'services/*.js',
'repositories/*.js',
'db/db.js'
]).then(() => {
const todosService = container.todosService;
todosService.getTodos('use awilix').then(todos => {
console.log(todos);
});
});
Note how in getTodos
, we did not specify the container as the first argument! The observant reader may have remembered that we used a little
function called bindAll
in todosService.js
.
That's it for the mini-guide. Be sure to read the short API docs below
so you know what's possible.
The Awilix Container Pattern (ACP)
So in the above example, you might have noticed a pattern:
module.exports = function(container) {
container.register({
someStuff: 'yay',
someFunction: container.bind(someFunction),
someObject: container.bindAll({
moreFunctions: moreFunctions
})
})
}
This is what I refer to as the Awilix Container Pattern (ACP), and is what the loadModules
API uses to let you register your stuff with the container in a "discovery-based" manner.
To make a module eligible for loading through loadModules
, it needs a default exported function that takes the container as the first parameter. The function is reponsible for registering things with the container using container.register
.
An ACP function MAY:
- return a
Promise
- use
container.dependsOn
to declare dependencies up-front (see corresponding section)
Example in ES5:
module.exports = function(container) {
}
module.exports.default = function(container) {
}
Example in ES6:
export default function(container) {
}
export default container => {
};
API
The awilix
object
When importing awilix
, you get the following stuff:
createContainer
listModules
AwilixResolutionError
These are documented below.
createContainer()
Creates a new Awilix container. The container stuff is documented further down.
listModules()
Returns a promise for a list of {name, path}
pairs,
where the name is the module name, and path is the actual
full path to the module.
This is used internally, but is useful for other things as well, e.g.
dynamically loading an api
folder.
Args:
globPatterns
: a glob pattern string, or an array of them.opts.cwd
: The current working directory passed to glob
. Defaults to process.cwd()
.- returns: a
Promise
for an array of objects with:
name
: The module name - e.g. db
path
: The path to the module relative to options.cwd
- e.g. lib/db.js
Example:
const listModules = require('awilix').listModules;
listModules([
'services/*.js'
]).then(result => {
console.log(result);
})
AwilixResolutionError
This is a special error thrown when Awilix is unable to resolve all dependencies (due to dependOn
). You can catch this error and use err instanceof AwilixResolutionError
if you wish. It will tell you what
dependencies it could not find.
The AwilixContainer
object
The container returned from createContainer
has some methods and properties.
container.registeredModules
A hash that contains all registered modules. Anything in there will also be present on the container
itself.
container.bind()
Creates a new function where the first parameter of the given function will always be the container it was bound to.
Args:
fn
: The function to bind.ctx
: The this
-context for the function.- returns: The bound function.
Example:
container.one = 1;
const myFunction = (container, arg1, arg2) => arg1 + arg2 + container.one;
const bound = container.bind(myFunction);
bound(2, 3);
container.bindAll()
Given an object, binds all it's functions to the container and
assigns it to the given object. The object is returned.
Args:
obj
: Object with functions to bind.- returns: The input
obj
Example:
const obj = {
method1: (container, arg1, arg2) => arg1 + arg2,
method2: (container, arg1, arg2) => arg1 - arg2
};
const boundObj = container.bindAll(obj);
boundObj === obj;
obj.method1(1, 2);
container.register()
Given an object, registers one or more things with the container. The values can be anything, and therefore are not bound automatically.
Args:
obj
: Object with things to register. Key is the what others will address the module as, value is the module itself.- returns: The
container
.
Example:
const addTodo = (container, text) => { };
const connect = container => awesomeDb.connect(container.DB_HOST);
container.register({
DB_HOST: 'localhost',
todoService: container.bindAll({
addTodo: addTodo
}),
log: function(text) {
console.log('AWILIX DEMO:', text);
},
connect: container.bind(connect)
});
container.todoService === container.registeredModules.todoService;
container.todoService.addTodo('follow and start awilix repo');
console.log(container.DB_HOST);
container.log('Hello!');
connect();
container.loadModules()
Given an array of globs, returns a Promise
when loading is done.
Awilix will use require
on the loaded modules, and call their default exported function (if it is a function, that is..) with the container as the first parameter (this is the Awilix Container Pattern (ACP)). This function then gets to do the registration of one or more modules.
Args:
globPatterns
: Array of glob patterns that match JS files to load.opts.cwd
: The cwd
being passed to glob
. Defaults to process.cwd()
.- returns: A
Promise
for when we're done. This won't be resolved until all modules are ready.
Example:
container.loadModules([
'services/*.js',
'repositories/*.js',
'db/db.js'
]).then(() => {
console.log('We are ready!');
container.todoService.addTodo('go to the pub');
});
container.dependsOn()
Used in conjunction with loadModules
, makes it easy to state up-front what
your module needs, and then get notified once it's ready. This is useful for doing constructor injection where you grab the dependencies off the container
at construction time rather than at function-call-time.
I recommend using the functional approach as it's less complex, but if you must, this method works perfectly fine as well. It's just a bit more verbose.
Args:
dependencies
: Array of strings that map to the modules being grabbed off the container - e.g. 'db'
when using container.db
.- returns: A dependency token (an internal thing, don't mind this).
Example:
class TodosRepository {
constructor(dependencies) {
this.db = dependencies.db;
}
}
module.exports = container => {
container.dependsOn(['db'], () => {
container.register({
todos: new TodosRepository(container)
})
});
}
Contributing
Clone repo, run npm i
to install all dependencies, and then npm run test-watch
+ npm run lint-watch
to start writing code.
For code coverage, run npm run coverage
.
If you submit a PR, please aim for 100% code coverage and no linting errors.
Travis will fail if there are linting errors. Thank you for your considering contributing. :)
What's in a name?
Awilix is the mayan goddess of the moon, and also my favorite character in the game SMITE.
Author
Jeff Hansen - @Jeffijoe