Angular2 actual combat of the front end: detailed explanation and application of dependency injection

Time:2021-9-13

Dependency injection

dependency-injection-in-angular-2

Dependency injection is the biggest feature and selling point of angular. It allows different components in an application to call each other without explicitly establishing an association. However, there are still some problems with dependency injection in angular 1, which is why angular 2 completely reconstructs a dependency injection system. The main problems of the dependency injection system in angular 1 are as follows:

  • Internal cache: dependency is generally treated as a singleton. Any service should be created only once in the whole application life cycle.

  • Synchronous by default(asynchronous by default): service creation in angular 1 is not asynchronous.

  • Namespace collision(namespace conflict): in the application life cycle, the token of a “type” is unique. If we customize a service named car, and there is a car service with the same name in the introduced third-party framework, there will be a problem in the whole system.

  • Built into the framework(built in framework): dependency injection of angular 1 is built-in and cannot be used alone.

The dependency injection system of angular 2 is roughly as follows:

Several key concepts are explained as follows:

  • Injector: injector is similar to ApplicationContext in spring and provides a series of interfaces for creating dependent instances.

  • Binding: binding is used to tell the injector how to create an instance of a dependency. A binding needs to be mapped to a factory type method.

  • Dependency: a dependency is the type of object being created.

The simplest dependency injection method in angular 2 is as follows:

import { Injector } from 'angular2/di';

var injector = Injector.resolveAndCreate([
  Car,
  Engine,
  Tires,
  Doors
]);
          
var car = injector.get(Car);

Resolveandcreate is a static interface method that creates dependent instances based on a series of binding inputs. Then, you can use the injector. Get () method to get the object instance corresponding to a type / token. When using this dependency, you can use the inject built in angular 2:

import { Inject } from 'angular2/di';

class Car {
  constructor(
    @Inject(Engine) engine,
    @Inject(Tires) tires,
    @Inject(Doors) doors
  ) {
    ...
  }
}

The inject decorator will automatically bind metadata to the attributes of the car class, or it can be rewritten as typescript:

class Car {
  constructor(engine: Engine, tires: Tires, doors: Doors) {
    ...
  }
}

At this step, a class can declare its own dependencies, and di resolves all the dependencies of the class. However, the injector also needs to get the information about how to create these object instances from the binding. In the above, a series of type / tokens are directly passed in the resolveandcreate method. If the complete writing method is used, the toclass method should be used to explicitly map a type / token to an instance.

import { bind } from 'angular2/di';

var injector = Injector.resolveAndCreate([
  bind(Car).toClass(Car),
  bind(Engine).toClass(Engine),
  bind(Tires).toClass(Tires),
  bind(Doors).toClass(Doors)
]);

The token in the above method can be any type or a string, which is the concrete implementation of the so-called recipe mechanism. With the help of such a binding, not only does the injector know how to use these dependencies in the application process, but also configures how to create these dependencies.

Further, if the foo type has been determined in the application, why write an expression such as bind (foo). Toclass (foo) and directly introduce write dead in the program. The real charm of dependency injection lies in:

bind(Engine).toClass(OtherEngine)

This can dynamically bind a token to the dependency, and effectively solve the problem of namespace conflict. We can create a type similar to an interface and point it to a specific type. Like interface and implementation in Java.

Other binding instructions

Sometimes, we do not necessarily need to bind a token to a class, but to a string value or factory method.

  • Bind to value

bind(String).toValue('Hello World')
  • Bind to alias

bind(Engine).toClass(Engine)
bind(V8).toAlias(Engine)

The toalias method binds one token to another.

  • Bind to a factory method

bind(Engine).toFactory(() => {
  if (IS_V8) {
    return new V8Engine();
  } else {
    return new V6Engine();
  }
})

Of course, a factory method may also have its dependencies. Simply point the dependencies to the parameters and add them to the token list.

bind(Engine).toFactory((dep1, dep2) => {
  if (IS_V8) {
    return new V8Engine();
  } else {
    return new V6Engine();
  }
}, [Token1, Token2])

Transient dependencies and child injectors

The dependencies mentioned above are often singleton, but sometimes we need a short-term dependency, that is, non singleton dependency. Generally speaking, there are two ways:

  • Use factory mode

bind(Engine).toFactory(() => {
  return new Engine();
})
  • Subinjector

You can use the injector. Resolveandcreatechild() method to iteratively create a child injector. A child injector will inherit the dependencies declared in the parent injector. However, if a dependency is also declared in the child injector, the instance it creates is inconsistent with the instance of the same token created by the parent injector:

var injector = Injector.resolveAndCreate([Engine]);
var childInjector = injector.resolveAndCreateChild([Engine]);

injector.get(Engine) !== childInjector.get(Engine);

Component Dependence

@Component({
  selector: 'app'
})
@View({
  template: '<h1>Hello !</h1>'
})
class App {
  constructor() {
    this.name = 'World';
  }
}
          
bootstrap(App);

The above declared component directly writes the name in the code. If the part of obtaining the name is extracted as a separate service:

class NameService {
  constructor() {
    this.name = 'Pascal';
  }

  getName() {
    return this.name;
  }
}

If you need to use Nameservice, you need to use the @ inject decorator when declaring a component:

class App {
  constructor(@Inject(NameService) NameService) {
    this.name = NameService.getName();
  }
}

If it is a typescript, it needs to be written as follows:

class App {
  constructor(NameService: NameService) {
    this.name = NameService.getName();
  }
}

The injector and binding steps of Nameservice are actually implemented by the bootstrap method:

bootstrap(App, [NameService]);

Of course, you can also write more elegant:

@Component({
  selector: 'app',
  bindings: [NameService]
})
@View({
  template: '<h1>Hello !</h1>'
})
class App {
  ...
}