The browser extension framework
View the Project on GitHub exteranto/exteranto.github.io
Getting StartedThe IOC container is the heart of Exteranto. It provides an easy and convenient way to handle your services and their dependencies. When using TypeScript, Exteranto delivers a set of TypeScript decorators that let you bind and resolve dependencies from the container on a single line while retaining flexibility.
// MyService.ts
import { Singleton } from '@exteranto/core'
@Singleton
export class MyService {
// Service implementation...
}
// DependentService.ts
import { MyService } from './MyService'
import { Autowired } from '@exteranto/core'
export class DependentService {
@Autowired
private service: MyService
}
The example defines a MyService
dependency to the container in the singleton
scope and the later in the DependentService
, it is resolved using the
@Autowired
decorator. No arguments are required for the decorator as it looks
at the class field type while resolving a dependency.
There are three main ways to bind dependencies to the container. We shall discuss all three in this chapter.
The most straightforward way to define dependencies to the container is to use
the Container
class itself. It provides the bind<T>
method that accepts the
dependency constructor and returns a Dependency<T>
instance. Further
configuration can be attached to the dependency using the Dependency<T>
interface.
See the whole
Dependency<T>
interface in Exteranto API Reference.
import { MyService } from './MyService'
import { Container } from '@exteranto/core'
Container
.getInstance()
.bind<MyService>(MyService)
.toSelf()
To avoid the Container.getInstance()
call, the package provides a @Self
property decorator that resolves the current container instance and assigns it
to the decorated class property.
import { MyService } from './MyService'
import { Container } from '@exteranto/core'
export class Provider {
@Self
private container: Container
public bindMyService () : void {
this.container.bind<MyService>(MyService).toSelf()
}
}
The standard way to define dependencies (this is especially useful when building a separate package) is to use a service provider. Service providers are classes that are specified in the application configuration. Exteranto executes these classes on application boot and provides essential services like the container instance to the provider methods.
Read more on Service Providers and Application Configuration.
import { MyService } from './MyService'
import { Provider } from '@exteranto/core'
export class AppProvider extends Provider {
public boot () : void {
// The boot method usually binds dependencies to
// the container. Note that the container instance
// is automatically available on the provider class.
this.container.bind<MyService>(MyService).toSelf()
}
}
Exteranto provides a set of class decorators that make binding dependencies to
the container a piece of cake. There are two annotations, and the only
difference is the scope that the dependency is bound in. Either @Singleton
to
bind a service in a singleton scope or @Binding
.
Note that, however, these decorators are convenient to use, it is discouraged to use them in standalone packages to prevent container pollution. Packages should define their functionalities in service providers.
import { Singleton } from '@exteranto/core'
@Singleton
export class MyService {
//
}
The Container
class itself provides a resolve<T>
method that tries to
resolve a dependency from the container. It takes in the service constructor and
returns an instance.
Note that while this approach is perfectly okay, property decorators should be your go-to option for resolving dependencies. Not only do they provide a clean syntax, but they also support good practices and object-oriented code structure.
import { MyService } from './MyService'
import { Container } from '@exteranto/core'
const service: MyService = Container
.getInstance()
.resolve<MyService>(MyService)
console.assert(service instanceof MyService)
Property decorators should be the go-to way to resolve dependencies in Exteranto. There are two basic decorators.
@Autowired
decorator that resolves the dependency automatically based on the
type of the decorated class property.@With<T>
this decorator also resolve the dependency based on type but it
also accepts an array of arguments to supply to the dependency constructor.import {
With,
Autowired,
Singleton,
} from '@exteranto/core'
@Singleton
class MyService {
//
}
@Singleton
class MyServiceWithConstructor {
constructor (private type: string) {
//
}
}
class Dependant {
@Autowired
private mySerivce: MySerivce
@With<MyServiceWithConstructor>(['normal'])
private myOtherService: MyServiceWithConstructor
}
Exteranto also allows you to bind parameters to the container. This is achieved
either via the application configuration file (which is also the prefered way)
or via the Container
’s bindParam
method.
These parameters can then be resolved using the Container.resolveParam<T>
method or the @Param<T>
property decorator.
Read more on Application Configuration.
import { Container } from '@exteranto/core'
Container
.getInstance()
.bindParam('version', '1.0.0')
// Resolve via `resolveParam<T>`.
const version: string = Container
.getInstance()
.resolveParam<string>('version')
// Resolve via the `@Param<T>` decorator.
class MyService {
@Param<string>('version')
private version: string
}
Although the basic funcionality is often sufficient, some edge cases require the
container to be more flexible. That’s why the container has various advanced
features like binding to a superclass, service tagging, and resolving
dependencies as an Optional<T>
.
Exteranto’s IOC Container allows you to bind dependencies to a superclass. This allows to switch implementations in the application if the new implementation extends the same superclass. Consider the following example:
Note that you cannot bind a dependency to an interface in TypeScript as all interfaces are removed at compilation.
import { Container } from '@exteranto/core'
abstract class Storage {
abstract get<T> (key: string) : T
abstract set<T> (key: string, value: T) : void
}
class LocalStorage extends Storage {
get<T> (key: string) : T {
// Implemetation...
}
set<T> (key: string, value: T) : void {
// Implemetation...
}
}
// Now we can bind the concrete implementation
// to the abstract superclass.
Container.bind<Storage>(LocalStorage).to(Storage)
console.assert(
Container.resolve<Storage>(Storage) instanceof Storage
)
Note that subsequent bindings to the same superclass override the previous binding. If you want to be able to swap implementations dynamically, consider tagging the bindings.
As previously noted, binding a new implementations to the same superclass overrides the previously bound implementation. To avoid this behaviour, one has to differentiate the implementations by either tagging each one of them or by binding them to a specific browser. In the following example, we have a service that behaves differently in each browser. Exteranto automatically resolves the current instance based on the environment.
import { Container, Browser } from '@exteranto/core'
// Three different implementations of the
// `Service` superclass.
import {
Service,
ChromeService,
SafariService,
FirefoxSerivice,
} from '...'
// Now we can bind the implementations to the
// abstract superclass based on the browser
// environment.
Container.bind<Service>(ChromeService)
.to(Service).for(Browser.CHROME)
Container.bind<Service>(SafariService)
.to(Service).for(Browser.SAFARI)
Container.bind<Service>(FirefoxSerivice)
.to(Service).for(Browser.EXTENSIONS)
Exteranto provides a convenient way of resolving dependencies that might not be
present in the container via the resolveOptional
container method. This method
always resolves and instance of Optional<T>
. If the binding is present, the
optional is Some<T>
, otherwise it is None<T>
. No exceptions are thrown in
the process.
import { Service } from '...'
import { Container, Optional } from '@exteranto/core'
const service: Optional<Service> = Container
.resolveOptional<Service>(Service)
console.assert(
typeof service.isSome() === 'boolean'
)
Read more on the
Optional<T>
interface in the Exteranto API Reference.
If the desired dependency is not present in the container, the resolve
method
throws a DependencyNotFoundException
. Similarly, the resolveParam
method
throws a ParameterNotFoundException
. If you do not desire to throw an error
when resolving a non-existent dependency, consider reading up on
Resolving Optional Dependencies.