-
Notifications
You must be signed in to change notification settings - Fork 17
Base Feature (En)
uioc provides 3 ways of dependency injection to satisfy needs of most cases, the following is introduced respectively:
It is used in case where dependencies are required when constructor executes, declarative constructor dependencies will be provided as parameters of constructor. In component configuration, use args property to declare constructor dependency:
class List {
constructor(entityName, model) {
this.entityName = entityName;
this.model = model;
}
}
let config = {
components: {
list: {
creator: List,
args: ['list', {$ref: 'listModel'}]
}
}
};
The above declares that list component’s constructor requires 2 dependencies:
- The first parameter dependency is a general object dependency, whose value is ‘list’.
- The second is a component reference dependency, the $ref keyword declares that the second parameter of constructor is a dependency referencing to a component of listModel.The types of dependencies will be described in detail later.
It is used in case where dependencies are needed after constructing instance. In component configuration, uioc uses properties property to configure property/interface dependencies:
class List {
constructor(entityName, model) {
this.entityName = entityName;
this.model = model;
}
setView(view) {
this.view = view
}
setCurrentContext(context) {
this.context = context;
}
}
let config = {
components: {
list: {
creator: List,
args: ['list', {$ref: 'listModel'}],
properties: {
name: 'list',
view: {$ref: 'listView'},
context: {
$ref: 'listContext',
$setter: 'setCurrentContext'
}
}
}
}
};
uioc will instantiate corresponding dependencies according to declaration, then inject dependencies into the instance:
-
If there is a keyword $setter in the property dependency declaration, uioc will invoke the interface declared by $setter, providing dependent instance as argument.
-
If not, uioc will search for the corresponding setter method (set${Name}) of property name, providing dependent instance as argument.
-
If instances do not include property setter, uioc will directly assign property to dependent instance:
instance: propertyName = propertyValue
Through above code, list component declares 3 property dependencies through properties, i.e. :name
, view
, context
.
name
property is general dependency, according to injection strategy, it will directly assign the instance by instance.name = 'list'
;view
property is component reference dependency, depends on listView component. According to injection strategy, it will search for the setView method on instance, if exist, it will invoke instance.setView(listView)
to inject listView component; if not, it will inject by instance.view = listView
.
context
property is component reference dependency, depends on listContext component. According to injection strategy, it declares $setter, and will inject listContext component by invoking instance.setCurrentContext(listView)
on instance.
In order to simplify manual dependency configuration, uioc provides auto searching and injecting functionality in property level, i.e. there is no need to explicitly declare each property/interface dependency in properties, auto injection will finally translated into property/interface injection.
You can set auto: true
property in component configuration to enable auto injection:
class List {
constructor(entityName, model) {
this.entityName = entityName;
this.model = model;
}
setView(view) {
this.view = view
}
setCurrentContext(context) {
this.context = context;
}
setName(name) {
this.name = name;
}
}
let config = {
components: {
list: {
creator: List,
auto: true,
args: ['list', {$ref: 'listModel'}],
properties: {
name: 'list'
}
},
name: {
creator() {}
}
view: {
creator: class View {}
}
}
};
In above code, the auto: true
switch in list component enables auto injection.
-
After the instance has been created, uioc will search for all the setter methods on the instance, and parse them to property dependency candidates. The above code includes three setters, i.e. setView, SetName and setCurrrentContext, their property dependency candidates are view, name and currentContext respectively.
-
According to the property dependency candidates from the first step, uioc will decide final property dependencies by filtering out those candidates registered in uioc container. The registered components in above code are list, name and view. Therefore name and view are the qualified.
-
Merge the dependencies generated from auto injection into explicitly declared property dependencies. If there is a property dependency configured explicitly with the same name of auto injection property, the explicitly configured one wins. In the above code, properties declare name dependency, therefore properties prevail in the final dependency configuration, and its value is ‘list’, not the register name component instance.
-
In case of property/interface injection according to the dependency declarations obtained in the third step, the above code will eventually invoke setView and setName methods injecting view and name dependency.
Note: Due to that JavaScript is a weakly typed language, auto injection is not currently available for constructor dependencies, but in the future when the decorator is mature, we can auto-inject declarations by decorator
General object dependency means the value can be determined in configuration phase without uioc management.
As follows, the first parameter dependency of list component constructor is a general dependency whose value is ‘list’. Property name is a general dependency whose value is ‘list’ too.
let config = {
components: {
list: {
creator: List,
args: ['list', {$ref: 'listModel'}],
properties: {
name: 'list',
view: {$ref: 'listView'}
}
}
}
};
Component reference dependency means the components registered in uioc, declared by the keyword $ref. uioc container will instantiate dependencies that the component needed when acquiring a component, then inject into the component according to the injection strategy. Component reference dependency satisfies most cases.
In the following code, list component declares two component reference dependencies, i.e. listModel and listView. listModel will be instantiated and passed as a parameter when invoking list constructor, while listView will be injected in property injection phase.
let config = {
components: {
list: {
creator: List,
args: ['list', {$ref: 'listModel'}],
properties: {
name: 'list',
view: {$ref: 'listView'}
}
}
}
};
Collection types dependency means that dependency is either an array or a dictionary, each element in the collection is a general object dependency or component reference dependency. It is suitable for case that the dependencies are collection.
Use keyword $list to declare an array dependency, the value of $list is an array, each element of the array is a specific dependency configuration:
class Service {
constructor(entityName, datasources) {
this.entityName = entityName;
this.datasources = datasources;
this.middleware = [];
}
setMiddleware(middleware) {
this.middleware = middleware;
}
}
let config = {
components: {
service: {
creator: Service,
args: [
'list',
{
$list: [{$ref: 'datasource'}, {id: 1}]
}
],
properties: {
entityName: 'list',
middleware: {
$list: [
{$ref: 'log'},
{
onRequest(query, next) {
query.timestamp = Date.now();
next();
}
}
]
}
}
}
}
};
As above, service component declares an array dependency passed as the second parameter during construction. Meanwhile, component property middleware is also an array dependency.
Each element in an array dependency is a dependency configuration. For example, in middleware array dependency, the first element is a component reference dependency, the second is a general object dependency.
After acquiring a service component, datasources and middleware of each instance are array, uioc will set value of each element according to responding array dependency configuration.
Use keyword $map to declare a Dictionary dependency, the value of $map is a JavaScript object with the form of key/value, each key is used as property name, whose corresponding value is a dependency configuration:
class ServiceFactory {
setServiceCollection(serviceCollection) {
this.services = serviceCollection;
}
}
let config = {
components: {
serviceFactory: {
creator: ServiceFactory,
properties: {
serviceCollection: {
$map: {
list: {$ref: 'listService'},
token: {$ref: 'tokenService'},
log(message) {
console.log(message);
}
}
}
}
}
}
};
As above, serviceFactory declares its serviceCollection property is a dictionary dependency, including 3 dependencies, i.e. list, token and log.
After acquiring a serviceFactory component, uioc will instantiate dependencies and assign to serviceCollection according to each dependency configuration of serviceCollection. Finally serviceCollection will be a JavaScript object passed to setServiceCollection. Eventually, we can operate on a serviceFactory instance as follows: serviceFactory.services.log(); serviceFactory.services.list()
.
Collection dependency is essentially syntactic sugar to simplify component configuration. Your can satisfy the needs for collection without using Collection dependency feature: Declaring a component which is an array or dictionary, each element of which is just the needed dependency.
Each element of collection can declare recursively arbitrary dependency types including Collection dependency, but recursiving too deep would affect performance, please use with caution.
uioc provides three modes of component instance management, i.e. transient, singleton and static to meet the requirements of most cases. Use scope keyword to identify the specified mode in component configuration.
Transient means every time when acquiring a component, uioc container will invoke creator in component configuration to create a new instance.
Set scope:'transient' to declare that a component is transient. However, you needn’t set scope to satisfy the case, because the default value of scope is just ‘transient’,
let config = {
components: {
list: {
creator: class List {},
scope: 'transient'
}
}
};
In above code, each time when invokes getComponent, the acquired list components are different.
Note: Different from server-side, transient cases are more often in front-end.
Singleton means that only in the first time of acquiring a component, uioc container will invoke creator in component configuration to create a new instance and then cache the instance, later just return the cached instance.
Set scope:'singleton' to declare a component is singleton in component configuration. When invoke the dispose method of a uioc container to destroy the container. The container will invoke the dispose method of each singleton component if declared. You can do some cleaning work or other operations here.
let config = {
components: {
list: {
creator: class List {},
scope: 'singleton'
}
}
};
In above code, each time when invokes getComponent, the acquired list components are the same.
Static means that the creator in component configuration is a static value, when acquiring a static component, uioc will return creator directly, rather than execute creator. It is commonly used in constant and configuration cases.
Set scope:'static' to declare a component is static in component configuration.
let config = {
components: {
constant: {
creator: {TYPE: 'pc'},
scope: 'static'
}
}
};
In above code, each time when invokes getComponent, the acquired constant components are {TYPE: 'pc'}.
uioc supports to instantiate with a constructor or factory method, instantiating with a constructor is the default way, and will instantiate with a factory method when isFactory is set to true.
Instantiation with a constructor will use operator new to invoke creator in the component configuration when acquiring component, and use its returned value is the component instance.
let config = {
components: {
list: {
creator: class List {},
isFactory: false // default
}
}
};
Instantiation with a factory method means that when acquiring a component, uioc will directly invoke creator in the component configuration and its returned value is the component instance.
let config = {
components: {
list: {
creator: function () {
return {name: 'list'};
},
isFactory: true
}
}
};
Loading Component Module Asynchronously means when declare a component configuration, the script containing the component module is not yet loaded. When acquiring a component, uioc will first load the component module with supportive loader in current environment, then instantiate the component. In the component configuration, set module value of the component module id to enable asynchronous loading.
let config = {
components: {
list: {
module: 'app/List'
}
}
};
Above code declares the module of list component, but does not set creator. When acquiring a list component, uioc will first invoke module loader of current environment (only AMD standard is supported by browser side currently) and pass ‘app/List’, then it will use acquired module as the creator of list, at last enter the instantiating process.
Note: currently, uioc can support module loader compliance with AMD and CommonJS, other loaders can be adapted with the customizing module loader feature which will be the followed introduction.
One of the biggest differences between browser side and other terminals is that it must download scripts in most cases. In large scale web app, we always have many optimize strategies to reduce the dimension of resource loading for initialization, one of which is excluding the uncommon business module from first loading scripts, only asynchronously loading it when needed.
Based on the asynchronous loading feature of uioc, we can easily do lazy loading modules, incremental updating and other similar optimization strategies in the browser.
In previous section, we talked about uioc loads a component module with the module loader supported by the current environment. Currently uioc uses require interface defined in AMD standard to load modules, meanwhile non-invasively adapts to AMD standard in NodeJS environment.
In practical cases, there are CMD, ES6 module and other cases besides AMD standard. In future, uioc will gradually adapt module specifications reached a unified consensus. To meet different kinds of module specifications, uioc allows users to customize the module loader, customizing the uioc module loader must conform require signature defined in AMD standard.
There are two ways to customize the module loader:
-
Configure loader in the uioc configuration.
-
Invoke setLoaderFunction of a uioc container instance.
let customLoader = (ids, cb) => {
console.log('custom loader':, ids);
window.require(ids, cb);
};
let config = {
loader: customLoader,
components: {
// ....
}
};
// or call uioc instance setLoaderFunction to configure
// ioc.setLoaderFunction(customLoader);
The above code demonstrates how to set and customize the loader. Each time when acquiring a component module asynchronously, uioc will print the id of modules to load.
Anonymous component configuration allows you to extend an existing component configuration to add or override configuration items in a simple way when declaring the component dependency (constructor/property). This feature can extremely reduce the need to declare a new component and partially reduce the configuration code.
Use $import keyword to declare an anonymous component configuration in dependency configuration, set the value of $import to the id of the component to extend:
let config = {
components: {
service: {
creator(url) {},
args: ['/api/entity'],
properties: {
method: 'get'
}
},
model: {
module: 'app/Model',
args: [{$ref: 'service'}]
},
listModel: {
module: 'app/ListModel',
args: [
{
$import: 'service',
args: ['/api/list']
}
]
}
}
};
In above code, the constructor of model depends on the service component, while listModel depends on an anonymous component whose configuration extends from service, but it overrides the configuration of constructor. It would be more complicated without the anonymous component configuration: you need to declare a new component, most configuration of which is the same as service component. Then declare a dependence on the new component in the constructor of listModel.