Awilix
Simple Inversion of Control (IoC) container for Node with dependency resolution support powered by ES6 Proxies. Make IoC great again!
Looking for v1? Click here.
Table of Contents
Installation
npm install awilix --save
Requires Node v6 or above
Usage
Awilix has a pretty simple API. At minimum, you need to do 3 things:
- Create a container
- Register some modules in it
- Resolve and use!
index.js
const awilix = require('awilix');
const container = awilix.createContainer();
class UserController {
constructor(opts) {
this.userService = opts.userService;
}
getUser(ctx) {
return this.userService.getUser(ctx.params.id);
}
}
container.registerClass({
userController: UserController
});
const makeUserService = ({ db }) => {
return {
getUser: (id) => {
return db.query(`select * from users where id=${id}`);
}
};
};
container.registerFunction({
userService: makeUserService
});
function Database({ connectionString }) {
this.conn = connectToYourDatabaseSomehow(connectionString);
}
Database.prototype.query = function(sql) {
return this.conn.rawSql(sql);
}
container.registerClass({
db: Database
})
container.registerValue({
connectionString: process.env.CONN_STR
});
router.get('/api/users/:id', container.resolve('userController').getUser);
router.get('/api/users/:id', container.cradle.userController.getUser);
That example looks big, but if you extract things to their proper files, it becomes rather elegant!
Check out a working Koa example!
Lifetime management
Awilix supports managing the lifetime of registrations. This means that you can control whether objects are resolved and used once, or cached for the lifetime of the process.
There are 3 lifetime types available.
TRANSIENT
: This is the default. The registration is resolved every time it is needed. This means if you resolve a class more than once, you will get back a new instance every time.SCOPED
: The registration is scoped to the container - that means that the resolved value will be reused when resolved from the same scope.SINGLETON
: The registration is always reused no matter what.
They are exposed on the awilix.Lifetime
object.
const Lifetime = awilix.Lifetime;
To register a module with a specific lifetime:
class MailService {}
container.registerClass({
mailService: [MailService, { lifetime: Lifetime.SINGLETON }]
});
const { asClass, asFunction, asValue } = awilix;
container.register({
mailService: asClass(MailService).lifetime(Lifetime.SINGLETON)
});
container.register({
mailService: asClass(MailService).singleton()
});
container.register({
mailService: asClass(MailService, { lifetime: Lifetime.SINGLETON })
});
container.registerClass('mailService', MailService, { lifetime: SINGLETON });
Scoped lifetime
In web applications, managing state without depending too much on the web framework can get difficult. Having to pass tons of information into every function just to make the right choices based on the authenticated user.
Scoped lifetime in Awilix makes this simple - and fun!
const { createContainer, asClass, asValue } = awilix;
const container = createContainer();
class MessageService {
constructor({ currentUser }) {
this.user = currentUser;
}
getMessages() {
const id = this.user.id;
}
}
container.register({
messageService: asClass(MessageService).scoped()
});
app.use((req, res, next) => {
req.scope = container.createScope();
req.scope.register({
currentUser: asValue(req.user)
});
next();
});
app.get('/messages', (req,res) => {
const messageService = req.scope.resolve('messageService');
messageService.getMessages().then(messages => {
res.send(200, messages)
});
});
IMPORTANT! If a singleton is resolved, and it depends on a scoped or transient registration, those will remain in the singleton for it's lifetime!
const makePrintTime = ({ time }) => () => {
console.log('Time:', time);
};
const getTime = () => new Date().toString();
container.register({
printTime: asFunction(makePrintTime).singleton(),
time: asFunction(getTime).transient()
});
container.resolve('time')
container.resolve('time')
container.resolve('printTime')();
container.resolve('printTime')();
Read the documentation for container.createScope()
for more examples.
Auto-loading modules
When you have created your container, registering 100's of classes can get boring. You can automate this by using loadModules
.
Imagine this app structure:
app
services
UserService.js
- exports an ES6 class UserService {}
emailService.js
- exports a factory function function makeEmailService() {}
repositories
UserRepository.js
- exports an ES6 class UserRepository {}
index.js
- our main script
In our main script we would do the following
const awilix = require('awilix');
const container = awilix.createContainer();
container.loadModules([
'services/**/*.js',
'repositories/**/*.js'
], {
formatName: 'camelCase',
registrationOptions: {
lifetime: Lifetime.SINGLETON
}
});
container.resolve('userService').getUser(1);
API
The awilix
object
When importing awilix
, you get the following top-level API:
createContainer
listModules
AwilixResolutionError
asValue
asFunction
asClass
Lifetime
- this one is documented above.
These are documented below.
createContainer()
Creates a new Awilix container. The container stuff is documented further down.
Args:
options
: Options object. Optional.
options.require
: The function to use when requiring modules. Defaults to require
. Useful when using something like require-stack
. Optional.
listModules()
Returns an array 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: 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;
const result = listModules([
'services/*.js'
]);
console.log(result);
AwilixResolutionError
This is a special error thrown when Awilix is unable to resolve all dependencies (due to missing or cyclic dependencies). You can catch this error and use err instanceof AwilixResolutionError
if you wish. It will tell you what dependencies it could not find or which ones caused a cycle.
The AwilixContainer
object
The container returned from createContainer
has some methods and properties.
container.cradle
Behold! This is where the magic happens! The cradle
is a proxy, and all getters will trigger a container.resolve
. The cradle
is actually being
passed to the constructor/factory function, which is how everything gets wired up.
container.registrations
A read-only getter that returns the internal registrations. When invoked on a scope, will show registrations for it's parent, and it's parent's parent, and so on.
Not really useful for public use.
container.cache
An object used internally for caching resolutions. It's a plain object.
Not meant for public use but if you find it useful, go ahead but tread carefully.
Each scope has it's own cache, and checks the cache of it's parents.
let counter = 1;
container.register({
count: asFunction(() => counter++).singleton()
});
container.cradle.count === 1
container.cradle.count === 1
delete container.cache.count;
container.cradle.count === 2
container.resolve()
Resolves the registration with the given name. Used by the cradle.
container.registerFunction({
leet: () => 1337
});
container.resolve('leet') === 1337
container.cradle.leet === 1337
container.register()
Registers modules with the container. This function is used by the registerValue
, registerFunction
and registerClass
functions.
Awilix needs to know how to resolve the modules, so let's pull out the
registration functions:
const awilix = require('awilix');
const { asValue, asFunction, asClass } = awilix;
asValue
: Resolves the given value as-is.asFunction
: Resolve by invoking the function with the container cradle as the first and only argument.asClass
: Like asFunction
but uses new
.
Now we need to use them. There are multiple syntaxes for the register
function, pick the one you like the most - or use all of them, I don't really care! :sunglasses:
Both styles supports chaining! register
returns the container!
container.register('connectionString', asValue('localhost:1433;user=...'));
container.register('mailService', asFunction(makeMailService), { lifetime: Lifetime.SINGLETON });
container.register('context', asClass(SessionContext), { lifetime: Lifetime.SCOPED });
container.register({
connectionString: asValue('localhost:1433;user=...'),
mailService: asFunction(makeMailService, { lifetime: Lifetime.SINGLETON }),
context: asClass(SessionContext, { lifetime: Lifetime.SCOPED })
})
container.register('mailService', asFunction(makeMailService).setLifetime(Lifetime.SINGLETON))
container.register('context', asClass(SessionContext).singleton())
container.register('context', asClass(SessionContext).transient())
container.register('context', asClass(SessionContext).scoped())
The object syntax, key-value syntax and chaining are valid for all register
calls!
container.registerValue()
Registers a constant value in the container. Can be anything.
container.registerValue({
someName: 'some value',
db: myDatabaseObject
});
container.registerValue('someName', 'some value');
container.registerValue('db', myDatabaseObject);
container
.registerValue('someName', 'some value')
.registerValue('db', myDatabaseObject);
container.registerFunction()
Registers a standard function to be called whenever being resolved. The factory function can return anything it wants, and whatever it returns is what is passed to dependents.
By default all registrations are TRANSIENT
, meaning resolutions will not be cached. This is configurable on a per-registration level.
The array syntax for values means [value, options]
. This is also valid for registerClass
.
const myFactoryFunction = ({ someName }) => (
`${new Date().toISOString()}: Hello, this is ${someName}`
);
container.registerFunction({ fullString: myFactoryFunction });
console.log(container.cradle.fullString);
setTimeout(() => {
console.log(container.cradle.fullString);
}, 2000);
const Lifetime = awilix.Lifetime;
container.registerFunction({
fullString: [myFactoryFunction, { lifetime: Lifetime.SINGLETON }]
});
console.log(container.cradle.fullString);
setTimeout(() => {
console.log(container.cradle.fullString);
}, 2000);
container.registerClass()
Same as registerFunction
, except it will use new
.
By default all registrations are TRANSIENT
, meaning resolutions will not be cached. This is configurable on a per-registration level.
class Exclaimer {
constructor({ fullString }) {
this.fullString = fullString;
}
exclaim() {
return this.fullString + '!!!!!';
}
}
container.registerClass({
exclaimer: Exclaimer
});
container.registerClass({
exclaimer: [Exclaimer, { lifetime: Lifetime.SINGLETON }]
});
container.cradle.exclaimer.exclaim();
container.createScope()
Creates a new scope. All registrations with a Lifetime.SCOPED
will be cached inside a scope. A scope is basically a "child" container.
let counter = 1;
container.register({
counterValue: asFunction(() => counter++).scoped()
});
const scope1 = container.createScope();
const scope2 = container.createScope();
const scope1Child = scope1.createScope();
scope1.cradle.counterValue === 1
scope1.cradle.counterValue === 1
scope2.cradle.counterValue === 2
scope2.cradle.counterValue === 2
scope1Child.cradle.counterValue === 1
Be careful! If a scope's parent has already resolved a scoped value, that value will be returned.
let counter = 1;
container.register({
counterValue: asFunction(() => counter++).scoped()
});
const scope1 = container.createScope();
const scope2 = container.createScope();
container.cradle.counterValue === 1
scope1.cradle.counterValue === 1
scope1.cradle.counterValue === 1
scope2.cradle.counterValue === 1
scope2.cradle.counterValue === 1
A scope may also register additional stuff - they will only be available within that scope and it's children.
container.register({
scopedValue: asFunction((cradle) => 'Hello ' + cradle.someValue)
});
const scope = container.createScope();
scope.register({
someValue: asValue('scope')
});
scope.cradle.scopedValue === 'Hello scope';
container.cradle.someValue
Things registered in the scope take precedence over it's parent.
const scope = container.createScope();
container.register({
value: asValue('root'),
usedValue: asFunction((cradle) => cradle.value)
});
scope.register({
value: asValue('scope')
});
container.cradle.usedValue === 'root'
scope.cradle.usedValue === 'scope'
container.loadModules()
Given an array of globs, returns a (useless) object with info about the loaded modules.
Awilix will use require
on the loaded modules, and register the default-exported function or class as the name of the file.
This will not work for constructor functions (function Database{} ...
), because there is no way to determine when to use new
. Internally, Awilix uses is-class
which only works for ES6 classes.
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. opts.formatName
: Can be either 'camelCase'
, or a function that takes the current name as the first parameter and returns the new name. Default is to pass the name through as-is.registrationOptions
: An object
passed to the registrations. Used to configure the lifetime of the loaded modules.
Example:
container.loadModules([
'services/*.js',
'repositories/*.js',
'db/db.js'
])
container.cradle.userService.getUser(123);
container.loadModules([
'services/*.js',
'repositories/*.js',
'db/db.js'
], {
registrationOptions: {
lifetime: Lifetime.SINGLETON
}
});
container.cradle.userService.getUser(123);
container.loadModules([
['services/*.js', Lifetime.SCOPED],
'repositories/*.js',
'db/db.js'
], {
registrationOptions: {
lifetime: Lifetime.SINGLETON
}
});
container.cradle.userService.getUser(123);
The ['glob', Lifetime.SCOPED]
syntax is a shorthand for passing in registration options like so: ['glob', { lifetime: Lifetime.SCOPED }]
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 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