Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

Elegant dependency injection container for vanilla JavaScript and TypeScript

License

NotificationsYou must be signed in to change notification settings

zheksoon/dioma

Repository files navigation

dioma

Elegant dependency injection container for vanilla JavaScript and TypeScript

NPM VersionNPM package gzipped sizeCodecov

Features

  • Just do it - no decorators, no annotations, no magic
  • Tokens for class, value, and factory injection
  • Async injection and dependency cycle detection
  • TypeScript support
  • No dependencies
  • Tiny size

Installation

npm install --save diomayarn add dioma

Usage

To start injecting dependencies, you just need to add thestatic scope property to your class and use theinject function to get the instance of it. By default,inject makes classes "stick" to the container where they were first injected (more details in theClass registration section).

Here's an example of using it forSingleton andTransient scopes:

import{inject,Scopes}from"dioma";classGarage{open(){console.log("garage opened");}// Single instance of the class for the entire applicationstaticscope=Scopes.Singleton();}classCar{// injects instance of Garageconstructor(privategarage=inject(Garage)){}park(){this.garage.open();console.log("car parked");}// New instance of the class on every injectionstaticscope=Scopes.Transient();}// Creates a new Car and injects Garageconstcar=inject(Car);car.park();

Scopes

Dioma supports the following scopes:

  • Scopes.Singleton() - creates a single instance of the class
  • Scopes.Transient() - creates a new instance of the class on every injection
  • Scopes.Container() - creates a single instance of the class per container
  • Scopes.Resolution() - creates a new instance of the class every time, but the instance is the same for the entire resolution
  • Scopes.Scoped() is the same asScopes.Container()

Singleton scope

Singleton scope creates a single instance of the class for the entire application.The instances are stored in the global container, so anyone can access them.If you want to isolate the class to a specific container, use theContainer scope.

A simple example you can see in theUsage section.

Multiple singletons can be cross-referenced with each other usingasync injection.

Transient scope

Transient scope creates a new instance of the class on every injection:

import{inject,Scopes}from"dioma";classEngine{start(){console.log("Engine started");}staticscope=Scopes.Singleton();}classVehicle{constructor(privateengine=inject(Engine)){}drive(){this.engine.start();console.log("Vehicle driving");}staticscope=Scopes.Transient();}// New vehicle every timeconstvehicle=inject(Vehicle);vehicle.drive();

Generally, transient scope instances can't be cross-referenced by theasync injection with some exceptions.

Container scope

Container scope creates a single instance of the class per container. It's the same as the singleton, but relative to the custom container.

The usage is the same as for the singleton scope, but you need to create a container first and usecontainer.inject instead ofinject:

import{Container,Scopes}from"dioma";constcontainer=newContainer();classGarage{open(){console.log("garage opened");}// Single instance of the class for the containerstaticscope=Scopes.Container();}// Register Garage on the containercontainer.register({class:Garage});classCar{// Use inject method of the container for Garageconstructor(privategarage=container.inject(Garage)){}park(){this.garage.open();console.log("car parked");}// New instance on every injectionstaticscope=Scopes.Transient();}constcar=container.inject(Car);car.park();

Container-scoped classes usually areregistered in the container first. Without it, the class will "stick" to the container it's used in.

Resolution scope

Resolution scope creates a new instance of the class every time, but the instance is the same for the entire resolution:

import{inject,Scopes}from"dioma";classQuery{staticscope=Scopes.Resolution();}classRequestHandler{constructor(publicquery=inject(Query)){}staticscope=Scopes.Resolution();}classRequestUser{constructor(publicrequest=inject(RequestHandler),publicquery=inject(Query)){}staticscope=Scopes.Transient();}constrequestUser=inject(RequestUser);// The same instance of Query is used for each of themrequestUser.query===requestUser.request.query;

Resolution scope instances can be cross-referenced by theasync injection without any issues.

Injection with arguments

You can pass arguments to the constructor when injecting a class:

import{inject,Scopes}from"dioma";classOwner{staticscope=Scopes.Singleton();petSomebody(pet:Pet){console.log(`${pet.name} petted`);}}classPet{constructor(publicname:string,publicowner=inject(Owner)){}pet(){this.owner.petSomebody(this);}staticscope=Scopes.Transient();}constpet=inject(Pet,"Fluffy");pet.pet();// Fluffy petted

Only transient and resolution scopes support argument injection.Resolution scope instances are cached for the entire resolution, so the arguments are passed only once.

Class registration

By default,Scopes.Container class injection is "sticky" - the class sticks to the container where it was first injected.

If you want to make a class save its instance in some specific parent container (seeChild containers), you can use class registration:

constcontainer=newContainer();constchild=container.childContainer();classFooBar{staticscope=Scopes.Container();}// Register the Foo class in the parent containercontainer.register({class:FooBar});// Returns and cache the instance on parent containerconstfoo=container.inject(FooBar);// Returns the FooBar instance from the parent containerconstbar=child.inject(FooBar);foo===bar;// true

You can override the scope of the registered class:

container.register({class:FooBar,scope:Scopes.Transient()});

To unregister a class, use theunregister method:

container.unregister(FooBar);

After that, the class will be removed from the container and all its child containers, and the next injection will return a new instance.

Injection with tokens

Instead of passing a class to theinject, you can usetokens instead.The token injection can be used forclass, value, and factory injection.Here's detailed information about each type.

Class tokens

Class tokens are useful to inject an abstract class or interface that has multiple implementations:

Here is an example of injecting an abstract interface
import{Token,Scopes,globalContainer}from"dioma";constwild=globalContainer.childContainer("Wild");constzoo=wild.childContainer("Zoo");interfaceIAnimal{speak():void;}classDogimplementsIAnimal{speak(){console.log("Woof");}staticscope=Scopes.Container();}classCatimplementsIAnimal{speak(){console.log("Meow");}staticscope=Scopes.Container();}constanimalToken=newToken<IAnimal>("Animal");// Register Dog class with the tokenwild.register({token:animalToken,class:Dog});// Register Cat class with the tokenzoo.register({token:animalToken,class:Cat});// Returns Dog instanceconstwildAnimal=wild.inject(animalToken);// Returns Cat instanceconstzooAnimal=zoo.inject(animalToken);

The class token registration can also override the scope of the class:

wild.register({token:animalToken,class:Dog,scope:Scopes.Transient()});

Value tokens

Value tokens are useful to inject a constant value:

import{Token}from"dioma";consttoken=newToken<string>("Value token");container.register({ token,value:"Value"});constvalue=container.inject(token);console.log(value);// Value

Factory tokens

Factory tokens are useful to inject a factory function.The factory takes the current container as the first argument and returns a value:

import{Token}from"dioma";consttoken=newToken<string>("Factory token");container.register({ token,factory:(container)=>"Value"});constvalue=container.inject(token);console.log(value);// Value

Factory function can also take additional arguments:

consttoken=newToken<string>("Factory token");container.register({  token,factory:(container,a:string,b):string=>a+b,});constvalue=container.inject(token,"Hello, ","world!");console.log(value);// Hello, world!

As a usual function, a factory can contain any additional logic, conditions, or dependencies.

Child containers

You can create child containers to isolate the scope of the classes.Child containers have a hierarchical structure, so Dioma searches instances top-down from the current container to the root container.If the instance is not found, Dioma will create a new instance in the current container, or in the container where the class was registered.

Here's an example:

import{Container,Scopes}from"dioma";constcontainer=newContainer(null,"Parent");constchild=container.childContainer("Child");classParentClass{staticscope=Scopes.Container();}classChildClass{staticscope=Scopes.Container();}container.register({class:ParentClass});child.register({class:ChildClass});// Returns ParentClass instance from the parent containerconstparentInstance=child.inject(ParentClass);// Returns ChildClass instance from the child containerconstchildInstance=child.inject(ChildClass);

Injection hooks

When registering a class, you can provide hooks that will be called before the instance is created or injected:

container.register({class:MyClass,beforeInject:(container,descriptor,args)=>{console.log("Before inject");},beforeCreate:(container,descriptor,args)=>{console.log("Before create");},});

Async injection and circular dependencies

When you have a circular dependency, there will be an errorCircular dependency detected. To solve this problem, you can use async injection.

Here is an example:
import{inject,injectAsync,Scopes}from"dioma";classA{constructor(privateinstanceB=inject(B)){}doWork(){console.log("doing work A");this.instanceB.help();}staticscope=Scopes.Singleton();}classB{privatedeclareinstanceA:A;// injectAsync returns a promise of the A instanceconstructor(privatepromiseA=injectAsync(A)){this.promiseA.then((instance)=>{this.instanceA=instance;});}help(){console.log("helping with work");}doAnotherWork(){console.log("doing work B");this.instanceA.doWork();}staticscope=Scopes.Singleton();}consta=awaitinjectAsync(A);constb=awaitinjectAsync(B);// Wait until all promises are resolvedawaitglobalContainer.waitAsync();a.doWork();b.doAnotherWork();

Async injection has an undefined behavior when there is a loop with transient dependencies. It may return an instance with an unexpected loop, or throw theCircular dependency detected in async resolution error, so it's better to avoid such cases.

As defined in the code above, you need to usecontainer.waitAsync() orwait for the next tick to get all instance promises resolved, even if you useawait injectAsync(...).

Generally, if you expect your dependency to have an async resolution, it's better to inject it withinjectAsync, as in the example above. But, you can also useinject for async injection as long as you wait for it as above.

Tokens also can be used for async injection as well:

import{Token,Scopes}from"dioma";consttoken=newToken<A>("A");classB{privatedeclareinstanceA:A;// token in used for async injectionconstructor(privatepromiseA=injectAsync(token)){this.promiseA.then((instance)=>{this.instanceA=instance;});}}

TypeScript

Dioma is written in TypeScript and provides type safety out of the box:

import{inject,Scopes,Injectable}from"dioma";// Injectable interface makes sure the static scope is definedclassDatabaseimplementsInjectable<typeofDatabase>{constructor(privateurl:string){}connect(){console.log(`Connected to${this.url}`);}staticscope=Scopes.Singleton();}// Error, scope is not specifiedclassRepositoryimplementsInjectable<typeofRepository>{constructor(privatedb=inject(Database)){}}inject(Repository);// Also type error, scope is not specified

Also, token and class injection infers the output types from the input types.If available, arguments are also checked and inferred.

API Reference

new Container(parent?, name?)

Creates a new container with the specified parent container and name.

new Token<T>(name?)

Creates a new token with the specified type and name.

container.inject(classOrToken, ...args)

Injects the instance of the class or token, and provides arguments to the constructor or factory function.

container.injectAsync(classOrToken, ...args)

Injects the promise of the instance of the class or token, and provides arguments to the constructor or factory function.

container.waitAsync()

Returns a promise that resolves when all current async injections are resolved.

container.register({ class, token?, scope? })

container.register({ token, value })

container.register({ token, factory })

Registers the class, value, or factory with the token in the container.

container.unregister(classOrToken)

Unregister the class or token from the container.

container.childContainer(name?)

Creates a new child container with the specified name.

Global exports

Global container:

  • globalContainer - the global container that is used by default for theinject function.
  • inject - the function to inject the instance of the class or token.
  • injectAsync - the function to inject the promise of the instance of the class or token.

Errors:

  • DependencyCycleError - thrown when a circular dependency is detected.
  • AsyncDependencyCycleError - thrown when a circular dependency is detected in async resolution.
  • ArgumentsError - thrown when the arguments are passed to unsupported scopes.
  • TokenNotRegisteredError - thrown when the token is not registered in the container.

Author

Eugene Daragan

License

MIT

Releases

No releases published

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp