ditox package
Dependency injection for modular web applications
Please see the documentation at ditox.js.org
Installation
You can use the following command to install this package:
npm install --save ditox
The package can be used as UMD module. Use
jsdelivr.com CDN site to load
ditox:
<script src="//cdn.jsdelivr.net/npm/ditox/dist/umd/index.js" />
<script>
const container = Ditox.createContainer();
</script>
Using in Deno environment:
import {createContainer} from 'https://deno.land/x/ditox/mod.ts';
const container = createContainer();
General description
DI pattern in general allows to declare and construct a code graph of an
application. It can be described by following phases:
- Code declaration phase:
- Defining public API and types of business layer
- Creation of injection tokens
- Declaring DI modules
- Binding phase:
- Creation of a container
- Binding values, factories and modules to the container
- Runtime phase:
- Running the code
- Values are constructed by registered factories
Diagram:
Usage Example
import {createContainer, injectable, optional, token} from 'ditox';
function createStorage(config) {}
function createLogger(config) {}
class UserService {
constructor(storage, logger) {}
}
const STORAGE_TOKEN = token < UserService > 'Token description for debugging';
const LOGGER_TOKEN = token();
const USER_SERVICE_TOKEN = token();
const STORAGE_CONFIG_TOKEN = optional(token(), {name: 'default storage'});
const container = createContainer();
container.bindValue(STORAGE_CONFIG_TOKEN, {name: 'custom storage'});
container.bindFactory(
STORAGE_TOKEN,
injectable(createStorage, STORAGE_CONFIG_TOKEN),
);
container.bindFactory(LOGGER_TOKEN, createLogger, {scope: 'transient'});
container.bindFactory(
USER_SERVICE_TOKEN,
injectable(
(storage, logger) => new UserService(storage, logger),
STORAGE_TOKEN,
optional(LOGGER_TOKEN),
),
{
scope: 'scoped',
onRemoved: (userService) => userService.destroy(),
},
);
const logger = container.get(LOGGER_TOKEN);
const userService = container.resolve(userService);
container.remove(LOGGER_TOKEN);
container.removeAll();
Container Hierarchy
Ditox.js supports "parent-child" hierarchy. If the child container cannot to
resolve a token, it asks the parent container to resolve it:
import {creatContainer, token} from 'ditox';
const V1_TOKEN = token();
const V2_TOKEN = token();
const parent = createContainer();
parent.bindValue(V1_TOKEN, 10);
parent.bindValue(V2_TOKEN, 20);
const container = createContainer(parent);
container.bindValue(V2_TOKEN, 21);
container.resolve(V1_TOKEN);
container.resolve(V2_TOKEN);
Factory Lifetimes
Ditox.js supports managing the lifetime of values which are produced by
factories. There are the following types:
singleton
- This is the default. The value is created and cached by the
container which registered the factory.scoped
- The value is created and cached by the container which starts
resolving.transient
- The value is created every time it is resolved.
singleton
This is the default scope. "Singleton" allows to cache a produced value by a
parent container which registered the factory:
import {creatContainer, token} from 'ditox';
const TAG_TOKEN = token();
const LOGGER_TOKEN = token();
const createLogger = (tag) => (message) => console.log(`[${tag}] ${message}`);
const parent = createContainer();
parent.bindValue(TAG_TOKEN, 'parent');
parent.bindFactory(LOGGER_TOKEN, injectable(createLogger, TAG_TOKEN), {
scope: 'singleton',
});
const container1 = createContainer(parent);
container1.bindValue(TAG_TOKEN, 'container1');
const container2 = createContainer(parent);
container2.bindValue(TAG_TOKEN, 'container2');
parent.resolve(LOGGER_TOKEN)('xyz');
container1.resolve(LOGGER_TOKEN)('foo');
container2.resolve(LOGGER_TOKEN)('bar');
scoped
"Scoped" lifetime allows to have sub-containers with own instances of some
services which can be disposed. For example, a context during HTTP request
handling, or other unit of work:
import {creatContainer, token} from 'ditox';
const TAG_TOKEN = token();
const LOGGER_TOKEN = token();
const createLogger = (tag) => (message) => console.log(`[${tag}] ${message}`);
const parent = createContainer();
parent.bindFactory(LOGGER_TOKEN, injectable(createLogger, TAG_TOKEN), {
scope: 'scoped',
});
const container1 = createContainer(parent);
container1.bindValue(TAG_TOKEN, 'container1');
const container2 = createContainer(parent);
container2.bindValue(TAG_TOKEN, 'container2');
parent.resolve(LOGGER_TOKEN)('xyz');
container1.resolve(LOGGER_TOKEN)('foo');
container2.resolve(LOGGER_TOKEN)('bar');
container1.removeAll();
transient
"Transient" makes to a produce values by the factory for each resolving:
import {createContainer, token} from 'ditox';
const TAG_TOKEN = token();
const LOGGER_TOKEN = token();
const createLogger = (tag) => (message) => console.log(`[${tag}] ${message}`);
const parent = createContainer();
parent.bindValue(TAG_TOKEN, 'parent');
parent.bindFactory(LOGGER_TOKEN, injectable(createLogger, TAG_TOKEN), {
scope: 'transient',
});
const container1 = createContainer(parent);
container1.bindValue(TAG_TOKEN, 'container1');
const container2 = createContainer(parent);
container2.bindValue(TAG_TOKEN, 'container2');
parent.resolve(LOGGER_TOKEN)('xyz');
container1.resolve(LOGGER_TOKEN)('foo');
container2.resolve(LOGGER_TOKEN)('bar');
parent.bindValue(TAG_TOKEN, 'parent-rebind');
parent.resolve(LOGGER_TOKEN)('xyz');
Dependency Modules
Dependencies can be organized as modules in declarative way with
ModuleDeclaration
. It is useful for providing pieces of functionality from
libraries to an app which depends on them.
import {Module, ModuleDeclaration, token} from 'ditox';
import {TRANSPORT_TOKEN} from './transport';
export type Logger = {log: (message: string) => void};
export const LOGGER_TOKEN = token<Logger>();
type LoggerModule = Module<{logger: Logger}>;
const LOGGER_MODULE_TOKEN = token<LoggerModule>();
const LOGGER_MODULE: ModuleDeclaration<LoggerModule> = {
token: LOGGER_MODULE_TOKEN,
factory: (container) => {
const transport = container.resolve(TRANSPORT_TOKEN).open();
return {
logger: {log: (message) => transport.write(message)},
destroy: () => transport.close(),
};
},
exports: {
logger: LOGGER_TOKEN,
},
};
Later such module declarations can be bound to a container:
const container = createContainer();
bindModule(container, LOGGER_MODULE);
bindModules(container, [DATABASE_MODULE, CONFIG_MODULE, API_MODULE]);
Utility functions for module declarations:
declareModule()
– declare a module as ModuleDeclaration
however token
field can be optional for anonymous modules.declareModuleBindings()
– declares an anonymous module with imports. This
module binds the provided ones to a container.
Example for these functions:
const LOGGER_MODULE = declareModule<LoggerModule>({
factory: createLoggerModule,
exports: {
logger: LOGGER_TOKEN,
},
});
const APP_MODULE = declareModuleBinding([LOGGER_MODULE, DATABASE_MODULE]);
This project is licensed under the
MIT license.