preface

Recently, I used my spare time to read Theia source code. This article hopes to help you get familiar with Inversify and the application in Theia, so that you don’t want to give up Theia from the beginning because of Inversify.

Theia, as many of you know, is an open source IDE, similar to vscode, but with more customization capabilities (which haven’t been thoroughly studied yet), so many ides with strong customization requirements are redeveloped based on Theia.

While reading the source code of Theia, there is a difficult problem to get around is Inversify, behind which we often hear about IOC (Inversion of Control), including dependency injection (DI) and dependency query. There are more and more projects using IOC, from Angular to many server-side projects (nest.js, midwayjs). In fact, vscode itself is heavily dependent on IOC, but it implements its own injection and parsing, while Theia’s IOC is based on inversify.

The use of the inversify

In fact, the use of inversify can be introduced to the official website, here I simplify the official website demo to let you experience:

// Declare a few injectable classes
@injectable(a)class Katana {
  public hit() {
    return "cut!"; }}@injectable(a)class Ninja {
  @inject(Katana)
  public katana: Katana;
  
  public fight() { return this.katana.hit(); }}// Initialize a container to hold these injectable classes
const container = new Container();
// Inject the class into the container
container.bind<Katana>(Katana).to(Katana).inSingletonScope();
container.bind<Ninja>(Ninja).to(Ninja).inSingletonScope();
/ / class
const ninja = container.get(Ninja);
// The input result is cut!
console.log(ninja.fight());
Copy the code

A container manages all the injectable content, and the user can use the decorator syntax to retrieve the injectable content. In this example, the injectable content is a class and the injectable content is a class instance. In this way the consumer does not have to worry about how the class is instantiated and how to ensure a single instance.

Let’s look at some common uses (examples from Theia).

scope

What scope is, in fact, is to determine whether the instance you get is a singleton or something else. For specific information, please refer to the official document and summarize it as follows:

Scope supports the following three modes:

  • Transient: A new instance is fetched from the container every time (that is, every request)
  • Singleton: It is the same instance every time it is fetched from the container (that is, every request)
  • Request: Also known as Scoped, a new instance is retrieved on each Request. If the class is required multiple times in this Request, the same instance will still be returned
bind(MonacoCommandService).toSelf().inTransientScope(); // Default
bind(VSXEnvironment).toSelf().inRequestScope();
bind(VSXRegistryAPI).toSelf().inSingletonScope();
Copy the code

bind

Normally we use bind as follows:

container.bind<Katana>(Katana).to(Katana).inSingletonScope();
Copy the code

For large projects like Theia, where there are hundreds of binds, we’ll use the same method each time. It’s ok, but container modules are provided to manage complex bind(although I haven’t found much difference yet).

const frontendApplicationModule = new ContainerModule((bind, unbind, isBound, rebind) = > {
    bind(NoneIconTheme).toSelf().inSingletonScope();
    bind(LabelProviderContribution).toService(NoneIconTheme);
    bind(IconThemeService).toSelf().inSingletonScope();
    // ...
}
Copy the code

Symbols

In fact, the arguments to bind syntax support both strings and classes that we pass in

/ / class
container.bind<Katana>(Katana).to(Katana).inSingletonScope();
/ / string
container.bind('Katana').to(Katana).inSingletonScope();
Copy the code

If we use strings, we run into namespace problems, so we can use Symbols to resolve conflicts

const TYPES = {
    Katana: Symbol.for("Katana")}; container.bind(TYPES.Katana).to(Katana).inSingletonScope();Copy the code

toSelf

This is a grammar candy, see below

bind(WidgetManager).toSelf().inSingletonScope();
/ / equal to
bind(WidgetManager).to(WidgetManager).inSingletonScope();
Copy the code

toDynamicValue

Bound to a dynamic value, the corresponding function is executed when fetched

bind(WidgetFactory).toDynamicValue(context= > ({
    id: CALLHIERARCHY_ID,
    createWidget: () = > createHierarchyTreeWidget(context.container)
}));
Copy the code

toFactory

The factory function returns a higher order function that allows you to further customize the value

bind(LoggerFactory).toFactory(ctx= >
    (name: string) = > {
        const child = new Container({ defaultScope: 'Singleton' });
        child.parent = ctx.container;
        child.bind(ILogger).to(Logger).inTransientScope();
        child.bind(LoggerName).toConstantValue(name);
        returnchild.get(ILogger); });Copy the code

toService

Bind to a service and let it resolve to a different type of bind declared previously. This bind is special and does nothing else because it returns no value.

bind(FrontendApplicationContribution).toService(IconThemeApplicationContribution);

public toService(service: string | symbol | interfaces.Newable<T> | interfaces.Abstract<T>): void {
    this.toDynamicValue(
        (context) = > context.container.get<T>(service)
    );
}
Copy the code

Tagged bindings & Named bindings

If you do not know which class to fetch, you can use @tagged and @named to solve the problem. However, the distinction has not been studied in depth, and Theia only uses @named.

// packages/task/src/browser/task-service.ts
@injectable(a)export class TaskService implements TaskConfigurationClient {
    @inject(ILogger) @named('task')
    protected readonly logger: ILogger;
}

// packages/task/src/common/task-common-module.ts
function createCommonBindings(bind: interfaces.Bind) :void {
    bind(ILogger).toDynamicValue(ctx= > {
        const logger = ctx.container.get<ILogger>(ILogger);
        return logger.child('task');
    }).inSingletonScope().whenTargetNamed('task');
}
Copy the code

Application of InVersify in Theia

After looking at the common uses of Inversify above, there is no problem reading the relevant content in Theia code, but there are some relatively advanced uses to be encountered in the real world, so let’s start with a practical example.

// packages/core/src/browser/widget-manager.ts
export const WidgetFactory = Symbol('WidgetFactory');
export interface WidgetFactory {
    readonly id: string; createWidget(options? :any): MaybePromise<Widget>;
}

// packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts
bind(WidgetFactory).toDynamicValue(({ container }) = > ({
    id: PLUGIN_VIEW_DATA_FACTORY_ID,
    createWidget: (identifier: TreeViewWidgetIdentifier) = > {
       // ...
    }
})).inSingletonScope();
bind(WidgetFactory).toDynamicValue(({ container }) = > ({
    id: PLUGIN_VIEW_FACTORY_ID,
    createWidget: (identifier: PluginViewWidgetIdentifier) = > {
        // ...
    }
})).inSingletonScope();

bind(WidgetFactory).toDynamicValue(({ container }) = > ({
    id: PLUGIN_VIEW_CONTAINER_FACTORY_ID,
    createWidget: (identifier: ViewContainerIdentifier) = >
        container.get<ViewContainer.Factory>(ViewContainer.Factory)(identifier)
})).inSingletonScope();
Copy the code

Bind WidgetFactory id (bindings) {bind (WidgetFactory id) {bind (WidgetFactory ID); With this question in mind, let’s see how Theia solves it. First of all, we look at how to use the place, so as to find a breakthrough, the code is as follows:

// packages/core/src/browser/widget-manager.ts

/**
 * The {@link WidgetManager} is the common component responsible for creating and managing widgets. Additional widget factories
 * can be registered by using the {@link WidgetFactory} contribution point. To identify a widget, created by a factory, the factory id and
 * the creation options are used. This key is commonly referred to as `description` of the widget.
 */
@injectable(a)export class WidgetManager {
    @inject(ContributionProvider) @named(WidgetFactory)
    protected readonly factoryProvider: ContributionProvider<WidgetFactory>;
    
    protected get factories() :Map<string.WidgetFactory> {
        if (!this._cachedFactories) {
            this._cachedFactories = new Map(a);for (const factory of this.factoryProvider.getContributions()) {
                if (factory.id) {
                    this._cachedFactories.set(factory.id, factory);
                } else {
                    this.logger.error('Invalid ID for factory: ' + factory + ". ID was: '" + factory.id + "'."); }}}return this._cachedFactories; }}Copy the code

There are only factories using factoryProvider, so the core logic is in the GetGetFactory method. Then look for this method to implement:

// packages/core/src/common/contribution-provider.ts

// Just a definition
export const ContributionProvider = Symbol('ContributionProvider');

export interface ContributionProvider<T extendsobject> { getContributions(recursive? :boolean): T[]
}

// An actual implementation
class ContainerBasedContributionProvider<T extends object> implements ContributionProvider<T> {
    constructor(
        protected readonly serviceIdentifier: interfaces.ServiceIdentifier<T>,
        protected readonly container: interfaces.Container
    ){}}/ / here is the key, the use of the named bindings, so factoryProvider eventually get ContainerBasedContributionProvider instance
export function bindContributionProvider(bindable: Bindable, id: symbol) :void {
    const bindingToSyntax = (Bindable.isContainer(bindable) ? bindable.bind(ContributionProvider) : bindable(ContributionProvider));
    bindingToSyntax
        .toDynamicValue(ctx= > new ContainerBasedContributionProvider(id, ctx.container))
        .inSingletonScope().whenTargetNamed(id);
}

Copy the code

You can see that the ContributionProvider is an ID that uses named Bindings, The WidgetManager. FactoryProvider finally got was actually ContainerBasedContributionProvider instance, then look at getContributions implementation

class ContainerBasedContributionProvider<T extends object> implements ContributionProvider<T> {
    protected services: T[] | undefined; getContributions(recursive? :boolean): T[] {
        if (this.services === undefined) {
            const currentServices: T[] = [];
            let currentContainer: interfaces.Container | null = this.container;
            // eslint-disable-next-line no-null/no-null
            while(currentContainer ! = =null) {
                if (currentContainer.isBound(this.serviceIdentifier)) {
                    try{ currentServices.push(... currentContainer.getAll(this.serviceIdentifier));
                    } catch (error) {
                        console.error(error); }}// eslint-disable-next-line no-null/no-null
                currentContainer = recursive === true ? currentContainer.parent : null;
            }
            this.services = currentServices;
        }
        return this.services; }}Copy the code

We bind WidgetFactory several times. We can’t inject it directly, but we can use Container. GetAll to getAll the injected classes and save all the classes. Then according to the id stored in the Map, and finally use directly according to the id to take out the corresponding class to use is ok, specific can see WidgetManager. GetOrCreateWidget method.

Contributionproviders are used in many places, and the underlying thinking is similar.

conclusion

Theia’s use of Inversify is very deep and frequent, hopefully reading this article will help you read the source code of Theia.

Here also want to express some of their own views. Use IOC nature is good, but the individual feels sensics is there some overuse, basically all module is based on the IOC to implement, the benefits of this is to keep the consistency and expansibility, for example, plug-ins can bind to the main module some capacity or to some extend, but feeling is also very obvious, is that code is obscure, The process is not clear enough.

Some classes that really need singletons are better suited to use IOC, but some function modules or functions that also use IOC feel overqualified and increase the cost of understanding. You’re welcome to discuss this as well, and see if our code and architecture capabilities are inadequate, or if they’re just a little overused

reference

Js series – Inversify basics to learn