Introduction: What is dependency Injection

Before we talk about DEPENDENCY injection, there is a concept called “inversion of Control” that often comes up together. The differences are:

  • Inversion of Control is a design idea
  • Dependency Injection is a programming technique that is an implementation of INVERSION of control

Dependency injection separates the details of the object’s initialization from the consumer.

To put it crudely in OO systems, instead of explicitly creating a dependency by new or other means, the user can directly use the externally created dependency instance by declaring it in the constructor.

Different frameworks implement DI in different ways, such as Java’s Spring framework on the back end, Node.js’s Nest.js server framework, and so on. For the front end, the best known is the dependency injection implementation in the Angular framework.

Concept of the term

Angular, for example, has several concepts in its DI system.

Basic concepts:

  • Injectable: An object that can be injected. In a business system, we want to declare injectable Services for use elsewhere. In Angular, injectable Service classes are required@InjectableDecoration. Other simple objects and constants can also be injected.
  • Di token: A token that is used to look up during di. It can be any primitive type or object, but generally a symbol or class is used to avoid conflicts.
  • Injector: Injector, a class of objects in DI system that can find dependencies based on the token and pass them to the consumer. There are different injector implementations in Angular, but I won’t go into detail.
  • The provider of the specific objects that the runtime relies on. Has the ability to write an implementation of a token to injector.

Implement a simple dependency injection system in TS/JS

A DI system consists of two important phases:

  • Depend on the collection
  • Dependency initialization

Depend on the collection

According to convenience, it can be divided into two types:

Manually specify

// file1
export const USER_SERVICE_SYMBOL = Symbol('UserService')

// file2
import {USER_SERVICE_SYMBOL} from 'file1'

@Inject({
  dependencies: [USER_SERVICE_SYMBOL],
})
class Component {
  constructor(private userService: UserService){}}Copy the code

Automatically collect

The elegance and ease with which dependencies can be collected automatically is one of the most exciting indicators of a DI system. If the dependency information for each class needs to be provided delicately by the user, it can be a little tiring to use.

Typescript has supported metadata since 1.5, Reflect.getmetadata (‘design:paramtypes’, target) is used to get the incoming parameters of the class decorated by the decorator. Such as:

import UserService from './UserService'

@Inject
class Component {
  constructor(private userService: UserService){}}Copy the code

TSC generates some code:

var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect= = ="object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};

let Component = class Component {
    constructor(userService) {
        this.userService = userService; }}; Component = __decorate([ Inject, __metadata("design:paramtypes", [UserService])
], Component);
Copy the code

The compiler parameter emitDecoratorMetadata is provided to enable TS to generate the following __metadata calls:

{
    "compilerOptions": {
        "experimentalDecorators": true."emitDecoratorMetadata": true,}}Copy the code

If the runtime environment does not support reflect. metadata, you can install and use the polyfill import ‘reflect-metadata’.

Dependency registration and initialization

Some of the points to note are as follows, as shown in the implementation in the next section

  • Injectable Service can be initialized when used. Declaring it as class naturally allows you to do this when you’re new. There are other ways to agree, depending on the framework.
  • In some cases we want only one instance of a service in the DI system, this can be done through one of injectorproviderInstanceMapCache.

Implement a DI system that is available in the React component

Sample code and Demo

You can run the demo here.

// Simple Injector + Provider
class Injection {
  private providerMap = new Map(a);/** Record provider instances to serve as cache */
  private providerInstanceMap = new Map(a);/** Record information about class constructor arguments */
  private typeInfoMap = new Map(a);registerParamTypes(token, paramTypes) {
    this.typeInfoMap.set(token, paramTypes);
  }

  registerProvider(token, provider) {
    this.providerMap.set(token, provider);
  }

  getProviderInstance(token) {
    let depInstance;
    if (this.providerInstanceMap.has(token)) {
      depInstance = this.providerInstanceMap.get(token);
    } else {
      depInstance = new (this.providerMap.get(token))();
      this.providerInstanceMap.set(token, depInstance);
    }
    returndepInstance; }}const injection = new Injection();

/** * Injectable class decorator */
function Injectable() {
  return function(target) {
    // Service may also depend on other services
    const shouldMakeSubClass = target.length > 0; // constructor needs to be injected

    let injectableToRegiter;
    if (shouldMakeSubClass) {
      class Injected extends (target as any) {
        constructor(. args) {
          const dependencyInstances = params.map(token= >
            injection.getProviderInstance(token)
          );
          super(... args, ... dependencyInstances); } } injectableToRegiter = Injected; }else {
      injectableToRegiter = target;
    }

    const params: any[] =
      Reflect.getMetadata("design:paramtypes", target) || [];
    injection.registerParamTypes(injectableToRegiter, params);

    / / registered provider
    injection.registerProvider(target, injectableToRegiter);
    if(target ! == injectableToRegiter) { injection.registerProvider(injectableToRegiter, injectableToRegiter); }return injectableToRegiter;
  };
}

/** * react.ponent class decorator */
function InjectComponent() {
  return function<T> (target: T) {
    // React Component constructor's first two arguments are fixed; injected services can only follow
    const params: any[] =
      Reflect.getMetadata("design:paramtypes", target).slice(2) | | [];const oldConstructor = target.constructor;

    class InjectedComponent extends (target as any) {
      static displayName = `${(oldConstructor as any).displayName ||
        oldConstructor.name}) `;
      constructor(. args) {
        const dependencyInstances = params.map(token= >
          injection.getProviderInstance(token)
        );
        super(... args, ... dependencyInstances); }}return (InjectedComponent as unknown) as T;
  };
}
Copy the code

Here’s an example of how to use it.

@Injectable(a)class UserService {
  getUsers() {
    return ["Zhang San"."Li Si"]; }}@Injectable(a)class CartService {
  constructor(private userService: UserService) {
    console.log("init Cart Service".this.userService);
  }

  inspect() {
    return this.userService.getUsers().map(name= > {
      return `${name} cart total value: The ${Math.random() * 1000}`;
    }).join('\n'); }}@InjectComponent(a)class SomeComponent extends React.Component {
  constructor(props, context, private cartService: CartService) {
    super(props, context);
  }

  render() {
    return React.createElement("div", {},this.cartService.inspect()]); }}// Start rendering
ReactDOM.render(React.createElement(SomeComponent, {}, []), document.body);
Copy the code

Unimplemented high-level parts

Scope, namespace or scope

In the example above, there is only one injection for the global. However, sometimes we want to have different instances of the Provider in different scenarios (namespaces). It is even possible to want the provider to have a lifecycle bound to a scope (in front-end projects, For example, each page counts as a scope? When you leave the scope, execute a destruction logic such as Provider.Dispose ().

Hierarchical Injector

This is Angular terminology, simply saying that injector can have multiple layers, each layer selectively overwriting parts of the previous layer’s implementation. A practical scenario is to replace a partial implementation starting with a node in the component tree. The actual lookup has a lookup process, which is very flexible. See the Angular ElementInjector specification for details.

Some recommended tool libraries

  • InversifyJS a powerful IoC container for JavaScript apps powered by TypeScript A common IoC container implementation, powerful.
  • RobinBuschmann/react. Di: Dependency injection for react based on upon inversify. There are many decorator implementations for the React system that you can learn about.

Do you still need this in the React system?

Have the Context ah

There is a concept called Context in React, which is a simple DI implementation that better satisfies the need to share state or services in the component tree.

However, when there are multiple contexts, context. Provider needs to be nested, which makes the code look bad.

Context is also deeply involved in the React Reconciliation process, so generally use Context to share some data and services such as Theme/I18n that have a direct impact on the view. Other forms of services thrown into the Context can complicate processing and affect performance.

Have a story!

Predictable State Container for JS Apps, while React-Redux reflected the separation of concerns in UI programming, using View as the display layer of consumption State only. It also tracks operations and changes to State.

But generally in complex front-end business systems, Redux is more as the storage of View Data.

What we call the “business logic” == code for how to interact with the back end, how the back end data model is transformed into the view layer model, and so on, is best left in a separate abstraction layer, separate from the selection of the view layer. Dependency injection, however, has a place in the reuse of business logic.

Refer to the article

  • Detail Angular dependency Injection – Zhihu
  • Angular – Hierarchical injectors
  • rbuckton/reflect-metadata: Prototype for a Metadata Reflection API for ECMAScript

About us

Febook-bytedance enterprise collaboration platform is a one-stop enterprise communication and collaboration platform integrating video conference, online documents, mobile office and collaboration software. At present, flying book business is developing rapidly, and there are R&D centers in Beijing, Shenzhen and other cities. There are enough HC for front-end, mobile, Rust, server, test, product and other positions. We look forward to your joining us to do challenging things together (please click the link: future.feishu.cn/recruit).

We also welcome the students and flying book technical issues together to exchange, interested students please click the flying book technical exchange group into the group exchange