在这篇文章中,我将展示如何通过InversifyJS库在你的扩展中使用依赖注入。

InversifyJS 是一个轻量级的(4KB)控制反转(IoC)容器,用于 TypeScript 和 JavaScript 应用程序

听起来不错,我们开始吧。

首先我们需要创建我们的扩展。你可以阅读关于VSCode官网文档了解如何创建扩展。我已经选择了 vscode-di 扩展名,所以以后我将使用这个名字。在扩展创建后,在VSCode中打开它。

创建好扩展后,需要安装InversifyJS本身和一个叫做reflect-metadata的附加包,通过这个控制台命令来完成:

npm install inversify reflect-metadata --save

这里要注意:包是作为依赖项而不是作为 devDependencies 安装的,这是非常重要的,如果你把这些包作为 devDependencies 安装,它在开发时仍然可以正常工作,但是如果你的扩展将来从扩展市场安装,它将崩溃。

下一步需要tsconfig.json  文件中启用一些编译项。 experimentalDecorators  和  emitDecoratorMetadata  选项必须启用。打开  tsconfig.json  文件并添加所需的更改。下面是我更改后的  tsconfig.json  文件:

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es6",
        "outDir": "out",
        "lib": ["es6"],
        "sourceMap": true,
        "rootDir": "src",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "strict": true,
        "noUnusedLocals": true
    },
    "exclude": [
        "node_modules",
        ".vscode-test"
    ]
}

到这里,项目初始化的工作就完成了,现在我们开始编码实现具体功能。

让我们从定义接口开始,在本教程中,我们将做一些非常基础的事情,以展示DI在Inversify中的工作原理。

export interface Command {
    id: string;
    execute(...args: any[]): any;
}

这个  Command  接口将用于描述我们将在 VSCode 中注册的命令。

export interface Printer {
    print(message: string): void;
}

这个  Printer  接口定义了消息输出的抽象点。稍后我们将把这个  Printer  注入到我们的命令中。

对于本文,我将  Command  接口放入  commands  文件夹中,将  Printer  接口放入  utils  文件夹中。

另外,为了正确的注入,我们需要用 symbols. Symbol 定义一个标识符,这个标识符稍后将用于注册和解析依赖关系。我将 symbols 定义放在  src  文件夹的根目录中,这个文件包含:

const TYPES = {
    Command: Symbol("Command"),
    Printer: Symbol("Printer")
};

export default TYPES;

在所有这些更改之后,你的  src  文件夹看起来应该是这样的:

.
├── commands
│   ├── command.ts
├── extension.ts
├── test
│   ├── extension.test.ts
│   └── index.ts
├── types.ts
└── utils
    └── printer.ts

下一步将为我们的接口定义实现。

我们将有2个命令和1个打印。命令将是 AddCommand 和 RemoveCommand ,打印将非常简单 - 通过 console.log 调用在控制台中显示消息。

在  commands  文件夹中创建新文件  add-command.ts ,内容如下:

import { injectable, inject } from 'inversify';

import TYPES from '../types';

import { Command } from './command';
import { Printer } from '../utils/printer';

@injectable()
export class AddCommand implements Command {
    constructor(
        @inject(TYPES.Printer) private printer: Printer
    ) {}

    get id() {
        return 'extension.add';
    }

    execute(...args: any[]) {
        this.printer.print('AddCommand');
    }
}

这里有两点需要注意:

  • 在类上添加@injectable 装饰器;

  • 构造函数参数的@inject 装饰器。

@injectable  装饰器表示这个类将被注入,它是在 DI 容器中注册类的强制装饰器。

参数的@inject 装饰器告诉DI容器,它应该解析提供的类型并将其传递到这里。

第二个命令看起来很像 AddCommand ,它叫做 RemoveCommand 。在 commands 文件夹中创建新文件 remove-command.ts ,内容如下:

import { injectable, inject } from 'inversify';

import TYPES from '../types';

import { Command } from './command';
import { Printer } from '../utils/printer';

@injectable()
export class RemoveCommand implements Command {
    constructor(
        @inject(TYPES.Printer) private printer: Printer
    ) {}

    get id() {
        return 'extension.remove';
    }

    execute(...args: any[]) {
        this.printer.print('RemoveCommand');
    }
}

这两个命令的区别基本上是类名、命令的id以及这个命令通过 Printer 打印的内容。

现在,让我们实现我们的打印功能。在  utils  文件夹中创建新文件  console-printer.ts ,内容如下:

import { injectable } from 'inversify';
import { Printer } from './printer';

@injectable()
export class ConsolePrinter implements Printer {
    print(message: string): void {
        console.log(message);
    }
}

正如您可能已经注意到的,它的实现非常简单。

在我们开始设置容器之前,让我们再创建一个东西,它将帮助我们在VSCode中注册命令。我将其命名为 CommandsManager ,并将其放置在我们定义的所有命令附近 - 在 commands 文件夹中。

import * as vscode from 'vscode';
import { multiInject, injectable } from 'inversify';
import TYPES from '../types';
import { Command } from './command';

@injectable()
export class CommandsManager {
    constructor(
        @multiInject(TYPES.Command) private commands: Command[]
    ) {}

    registerCommands(context: vscode.ExtensionContext) {
        for (const c of this.commands) {
            const cmd = vscode.commands.registerCommand(c.id, c.execute);
            context.subscriptions.push(cmd);
        }
    }
}

虽然这个类很小,但有一个与我们的命令实现不同的地方。你应该注意到构造函数参数中的 @multiInject 装饰器,参数类型是数组。@multiInject 装饰器告诉DI容器注入所有具有指定符号的实体(在我们的例子中是 TYPES.Command ).这基本上意味着 Command 接口的所有实现将作为数组传递在这里.

好了,这就是实现,现在让我们配置我们的 DI 容器并尝试使用它。

如果你查看Inversify的官方文档,你会发现它建议将容器放入 inversify.config.ts 文件中,让我们坚持这个建议,在 src 文件夹中创建相同的文件。

import 'reflect-metadata';

import { Container } from 'inversify';
import TYPES from './types';
import { Printer } from './utils/printer';
import { ConsolePrinter } from './utils/console-printer';
import { AddCommand } from './commands/add-command';
import { Command } from './commands/command';
import { RemoveCommand } from './commands/remove-command';
import { CommandsManager } from './commands/commands-manager';

const container = new Container();
container.bind<Printer>(TYPES.Printer).to(ConsolePrinter);
container.bind<Command>(TYPES.Command).to(AddCommand);
container.bind<Command>(TYPES.Command).to(RemoveCommand);
container.bind<CommandsManager>(TYPES.CommandManager).to(CommandsManager);

export default container;

如果你以前使用过任何DI容器,这看起来会很熟悉,但这里最重要的是第一行,没有它,什么都不能工作,因为这个库应该全局导入一次。

现在我们有了实体、符号并设置了容器,让我们做一些有用的事情,注册我们的命令,这样它们就能工作了。

打开文件 extension.ts ,在 activate 方法中写入以下代码:

const cmdManager = container.get<CommandsManager>(TYPES.CommandManager);
cmdManager.registerCommands(context);

另外,不要忘记在  package.json  文件中告诉你的扩展贡献了命令。下面是我的  contributes  部分的样子:

"contributes": {
    "commands": [
        {
            "command": "extension.add",
            "title": "DI: Add"
        },
        {
            "command": "extension.remove",
            "title": "DI: Remove"
        }
    ]
}

基本上就是这样了。现在你可以启动你的扩展,并尝试从命令面板调用命令。

我试图启动  add  命令,但失败了... :(

你可能会想知道“为什么会这样???”,如果你尝试在调试下启动你的扩展,并在任何命令的执行方法的断点处停止,你会注意到 this 是undefined。

解释很简单:当 execute 方法启动时, this 上下文没有设置到你的对象。你可以在VSCode中注册命令时提供正确的 this 上下文来修复这个问题。打开你的 CommandManager ,找到 registerCommand 调用行。这个函数接受第三个参数 thisArg?: any 。由于它是可选的,我们没有提供它,当VSCode调用 execute 方法时, this 是undefined。

将这一行改为:

const cmd = vscode.commands.registerCommand(c.id, c.execute, c);

这会在方法执行期间设置正确的  this  上下文。

再次启动扩展,你会注意到一切工作正常,你的命令在VSCode的调试控制台上显示消息。

希望你现在对如何将DI框架集成到你的扩展代码库中并开始使用它有了更好的理解。在本文中,我们只是触及了Inversify可以做什么的表面。如果你想了解更多关于Inversify的特性,可以参阅文档:

https://inversify.io