在VSCode扩展中使用依赖注入
在这篇文章中,我将展示如何通过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的特性,可以参阅文档: