使用TypeScript,依赖注入和Discord Bot

本文概述

  • 设置你的Node.js项目
  • 在Discord Apps资讯主页中建立新的应用程式
  • 将Discord Bot添加到服务器
  • 创建.env文件
  • 编译TypeScript
  • 创建Bot类
  • 配置依赖项注入容器
  • 在Discord Bot App中使用容器
  • 实施业务逻辑
  • 创建单元测试
  • 创建集成测试
  • TypeScript和依赖注入:不仅用于不和谐的Bot开发
类型和可测试的代码是避免错误的两种最有效的方法, 尤其是随着时间的推移代码的变化。我们可以分别利用TypeScript和依赖项注入(DI)设计模式将这两种技术应用于JavaScript开发。
在本TypeScript教程中, 除编译外, 我们将不直接介绍TypeScript基础。取而代之的是, 我们将逐步演示如何使用TypeScript最佳实践, 从头开始制作Discord机器人, 连接测试和DI以及创建示例服务。我们将使用:
  • Node.js
  • TypeScript
  • Discord.js, Discord API的包装器
  • InversifyJS, 一个依赖注入框架
  • 测试库:Mocha, Chai和ts-mockito
  • 奖金:Mongoose和MongoDB, 以便编写集成测试
设置你的Node.js项目 首先, 我们创建一个名为typescript-bot的新目录。然后, 输入它并通过运行以下命令创建一个新的Node.js项目:
npm init

注意:你也可以使用yarn, 但是为了简洁起见, 请坚持使用npm。
这将打开一个交互式向导, 该向导将设置package.json文件。你可以放心地按Enter输入所有问题(或根据需要提供一些信息)。然后, 让我们安装我们的依赖项和开发依赖项(那些仅在测试中需要的依赖项)。
npm i --save typescript discord.js inversify dotenv @types/node reflect-metadata npm i --save-dev chai mocha ts-mockito ts-node @types/chai @types/mocha

然后, 将package.json中生成的” 脚本” 部分替换为:
"scripts": { "start": "node src/index.js", "watch": "tsc -p tsconfig.json -w", "test": "mocha -r ts-node/register \"tests/**/*.spec.ts\"" },

要递归查找文件, 需要在tests / ** / *。spec.ts周围加上双引号。 (注意:语法可能会因使用Windows的开发人员而异。)
启动脚本将用于启动bot, 监视脚本将用于编译TypeScript代码, 并进行测试以运行测试。
现在, 我们的package.json文件应如下所示:
{ "name": "typescript-bot", "version": "1.0.0", "description": "", "main": "index.js", "dependencies": { "@types/node": "^11.9.4", "discord.js": "^11.4.2", "dotenv": "^6.2.0", "inversify": "^5.0.1", "reflect-metadata": "^0.1.13", "typescript": "^3.3.3" }, "devDependencies": { "@types/chai": "^4.1.7", "@types/mocha": "^5.2.6", "chai": "^4.2.0", "mocha": "^5.2.0", "ts-mockito": "^2.3.1", "ts-node": "^8.0.3" }, "scripts": { "start": "node src/index.js", "watch": "tsc -p tsconfig.json -w", "test": "mocha -r ts-node/register \"tests/**/*.spec.ts\"" }, "author": "", "license": "ISC" }

在Discord Apps资讯主页中建立新的应用程式 为了与Discord API进行交互, 我们需要一个令牌。要生成这样的令牌, 我们需要在Discord Developer仪表板中注册一个应用程序。为此, 你需要创建一个Discord帐户并转到https://discordapp.com/developers/applications/。然后, 单击” 新建应用程序” 按钮:
使用TypeScript,依赖注入和Discord Bot

文章图片
选择一个名称, 然后单击创建。然后, 单击Bot→添加Bot, 你已完成。让我们将机器人添加到服务器。但请不要关闭此页面, 我们需要尽快复制令牌。
将Discord Bot添加到服务器 为了测试我们的机器人, 我们需要一个Discord服务器。你可以使用现有服务器或创建新服务器。为此, 请复制机器人的CLIENT_ID(位于” 常规信息” 标签上), 并将其用作此特殊授权URL的一部分:
https://discordapp.com/oauth2/authorize?client_id=< CLIENT_ID> & scope=bot
当你在浏览器中点击此URL时, 将出现一个表单, 你可以在其中选择应将机器人添加到的服务器。
使用TypeScript,依赖注入和Discord Bot

文章图片
将漫游器添加到服务器后, 你应该会看到类似以上的消息。
创建.env文件 我们需要某种方式将令牌保存在我们的应用程序中。为此, 我们将使用dotenv软件包。首先, 从Discord Application Dashboard获取令牌(Bot→单击以显示令牌):
使用TypeScript,依赖注入和Discord Bot

文章图片
现在, 创建一个.env文件, 然后将令牌复制并粘贴到此处:
TOKEN=paste.the.token.here

如果使用Git, 则应将此文件放置在.gitignore中, 以使令牌不会受到损害。另外, 创建一个.env.example文件, 以便知道令牌需要定义:
TOKEN=

编译TypeScript 为了编译TypeScript, 可以使用npm run watch命令。另外, 如果你使用PHPStorm(或其他IDE), 则只需使用其TypeScript插件中的文件监视程序, 然后让你的IDE处理编译即可。让我们通过创建包含以下内容的src / index.ts文件来测试设置:
console.log('Hello')

另外, 让我们创建一个tsconfig.json文件, 如下所示。 InversifyJS需要experimentalDecorators, emitDecoratorMetadata, es6和reflect-metadata:
{ "compilerOptions": { "module": "commonjs", "moduleResolution": "node", "target": "es2016", "lib": [ "es6", "dom" ], "sourceMap": true, "types": [ // add node as an option "node", "reflect-metadata" ], "typeRoots": [ // add path to @types "node_modules/@types" ], "experimentalDecorators": true, "emitDecoratorMetadata": true, "resolveJsonModule": true }, "exclude": [ "node_modules" ] }

如果文件监视程序正常运行, 则应生成一个src / index.js文件, 运行npm start会导致:
> node src/index.js Hello

创建Bot类 现在, 让我们最后开始使用TypeScript最有用的功能:类型。继续创建以下src / bot.ts文件:
import {Client, Message} from "discord.js"; export class Bot { public listen(): Promise< string> { let client = new Client(); client.on('message', (message: Message) => {}); return client.login('token should be here'); } }

现在, 我们可以在这里看到我们需要的东西:令牌!我们是要在此处复制粘贴, 还是直接从环境加载值?
都不行取而代之的是, 使用我们选择的依赖注入框架InversifyJS注入令牌, 从而编写出更具可维护性, 可扩展性和可测试性的代码。
另外, 我们可以看到Client依赖项是硬编码的。我们也将注入这一点。
配置依赖项注入容器 依赖项注入容器是一个知道如何实例化其他对象的对象。通常, 我们为每个类定义依赖项, 而DI容器负责解决它们。
InversifyJS建议将依赖项放入inversify.config.ts文件中, 因此让我们继续在此处添加我们的DI容器:
import "reflect-metadata"; import {Container} from "inversify"; import {TYPES} from "./types"; import {Bot} from "./bot"; import {Client} from "discord.js"; let container = new Container(); container.bind< Bot> (TYPES.Bot).to(Bot).inSingletonScope(); container.bind< Client> (TYPES.Client).toConstantValue(new Client()); container.bind< string> (TYPES.Token).toConstantValue(process.env.TOKEN); export default container;

另外, InversifyJS文档建议创建一个types.ts文件, 并列出我们将要使用的每种类型以及相关的Symbol。这是很不方便的, 但是可以确保在我们的应用程序增长时不会发生命名冲突。每个符号都是唯一的标识符, 即使其描述参数相同(该参数仅用于调试目的)也是如此。
export const TYPES = { Bot: Symbol("Bot"), Client: Symbol("Client"), Token: Symbol("Token"), };

在不使用符号的情况下, 发生命名冲突时的外观如下:
Error: Ambiguous match found for serviceIdentifier: MessageResponder Registered bindings: MessageResponder MessageResponder

在这一点上, 要选择应该使用哪种MessageResponder更加不便, 尤其是当我们的DI容器变大时。使用Symbols可以解决这一问题, 并且在具有两个相同名称的类的情况下, 我们不会想出奇怪的字符串文字。
在Discord Bot App中使用容器 现在, 让我们修改Bot类以使用容器。为此, 我们需要添加@injectable和@inject()批注。这是新的Bot类:
import {Client, Message} from "discord.js"; import {inject, injectable} from "inversify"; import {TYPES} from "./types"; import {MessageResponder} from "./services/message-responder"; @injectable() export class Bot { private client: Client; private readonly token: string; constructor( @inject(TYPES.Client) client: Client, @inject(TYPES.Token) token: string ) { this.client = client; this.token = token; }public listen(): Promise < string > { this.client.on('message', (message: Message) => { console.log("Message received! Contents: ", message.content); }); return this.client.login(this.token); } }

最后, 让我们在index.ts文件中实例化我们的机器人:
require('dotenv').config(); // Recommended way of loading dotenv import container from "./inversify.config"; import {TYPES} from "./types"; import {Bot} from "./bot"; let bot = container.get< Bot> (TYPES.Bot); bot.listen().then(() => { console.log('Logged in!') }).catch((error) => { console.log('Oh no! ', error) });

现在, 启动机器人并将其添加到你的服务器。然后, 如果你在服务器通道中键入消息, 则该消息应显示在命令行的日志中, 如下所示:
> node src/index.jsLogged in! Message received! Contents:Test

最后, 我们建立了基础:机器人内部的TypeScript类型和依赖项注入容器。
实施业务逻辑 让我们直接进入本文的核心:创建可测试的代码库。简而言之, 我们的代码应实现最佳实践(如SOLID), 而不是隐藏依赖项, 而不使用静态方法。
另外, 它在运行时不应引入副作用, 并且易于模拟。
为了简单起见, 我们的机器人只会做一件事:它将搜索传入的消息, 如果其中包含” ping” 一词, 我们将使用可用的Discord机器人命令之一使该机器人以” pong!” 来响应。 “ 给那个用户。
为了展示如何将自定义对象注入Bot对象并对其进行单元测试, 我们将创建两个类:PingFinder和MessageResponder。我们将MessageResponder注入Bot类, 并将PingFinder注入MessageResponder。
这是src / services / ping-finder.ts文件:
import {injectable} from "inversify"; @injectable() export class PingFinder {private regexp = 'ping'; public isPing(stringToSearch: string): boolean { return stringToSearch.search(this.regexp) > = 0; } }

然后, 将该类注入src / services / message-responder.ts文件:
import {Message} from "discord.js"; import {PingFinder} from "./ping-finder"; import {inject, injectable} from "inversify"; import {TYPES} from "../types"; @injectable() export class MessageResponder { private pingFinder: PingFinder; constructor( @inject(TYPES.PingFinder) pingFinder: PingFinder ) { this.pingFinder = pingFinder; }handle(message: Message): Promise< Message | Message[]> { if (this.pingFinder.isPing(message.content)) { return message.reply('pong!'); }return Promise.reject(); } }

最后, 这是一个修改后的Bot类, 它使用MessageResponder类:
import {Client, Message} from "discord.js"; import {inject, injectable} from "inversify"; import {TYPES} from "./types"; import {MessageResponder} from "./services/message-responder"; @injectable() export class Bot { private client: Client; private readonly token: string; private messageResponder: MessageResponder; constructor( @inject(TYPES.Client) client: Client, @inject(TYPES.Token) token: string, @inject(TYPES.MessageResponder) messageResponder: MessageResponder) { this.client = client; this.token = token; this.messageResponder = messageResponder; }public listen(): Promise< string> { this.client.on('message', (message: Message) => { if (message.author.bot) { console.log('Ignoring bot message!') return; }console.log("Message received! Contents: ", message.content); this.messageResponder.handle(message).then(() => { console.log("Response sent!"); }).catch(() => { console.log("Response not sent.") }) }); return this.client.login(this.token); } }

在这种状态下, 该应用程序将无法运行, 因为没有MessageResponder和PingFinder类的定义。让我们将以下内容添加到inversify.config.ts文件中:
container.bind< MessageResponder> (TYPES.MessageResponder).to(MessageResponder).inSingletonScope(); container.bind< PingFinder> (TYPES.PingFinder).to(PingFinder).inSingletonScope();

另外, 我们将向type.ts添加类型符号:
MessageResponder: Symbol("MessageResponder"), PingFinder: Symbol("PingFinder"),

现在, 重新启动我们的应用程序后, 机器人应响应包含” ping” 的每条消息:
使用TypeScript,依赖注入和Discord Bot

文章图片
这是它在日志中的外观:
> node src/index.jsLogged in! Message received! Contents:some message Response not sent. Message received! Contents:message with ping Ignoring bot message! Response sent!

创建单元测试 既然我们已经正确注入了依赖项, 那么编写单元测试就很容易了。我们将为此使用Chai和ts-mockito;但是, 你可以使用许多其他测试运行程序和模拟库。
ts-mockito中的模拟语法非常冗长, 但也易于理解。以下是设置MessageResponder服务并将PingFinder模拟注入其中的方法:
let mockedPingFinderClass = mock(PingFinder); let mockedPingFinderInstance = instance(mockedPingFinderClass); let service = new MessageResponder(mockedPingFinderInstance);

既然我们已经设置了模拟, 我们就可以定义isPing()调用的结果是什么, 并验证reply()调用。关键是在单元测试中, 我们定义了isPing()调用的结果:true或false。邮件的内容无关紧要, 因此在测试中, 我们只使用” 非空字符串” 。
when(mockedPingFinderClass.isPing("Non-empty string")).thenReturn(true); await service.handle(mockedMessageInstance) verify(mockedMessageClass.reply('pong!')).once();

整个测试套件如下所示:
import "reflect-metadata"; import 'mocha'; import {expect} from 'chai'; import {PingFinder} from "../../../src/services/ping-finder"; import {MessageResponder} from "../../../src/services/message-responder"; import {instance, mock, verify, when} from "ts-mockito"; import {Message} from "discord.js"; describe('MessageResponder', () => { let mockedPingFinderClass: PingFinder; let mockedPingFinderInstance: PingFinder; let mockedMessageClass: Message; let mockedMessageInstance: Message; let service: MessageResponder; beforeEach(() => { mockedPingFinderClass = mock(PingFinder); mockedPingFinderInstance = instance(mockedPingFinderClass); mockedMessageClass = mock(Message); mockedMessageInstance = instance(mockedMessageClass); setMessageContents(); service = new MessageResponder(mockedPingFinderInstance); })it('should reply', async () => { whenIsPingThenReturn(true); await service.handle(mockedMessageInstance); verify(mockedMessageClass.reply('pong!')).once(); })it('should not reply', async () => { whenIsPingThenReturn(false); await service.handle(mockedMessageInstance).then(() => { // Successful promise is unexpected, so we fail the test expect.fail('Unexpected promise'); }).catch(() => { // Rejected promise is expected, so nothing happens here }); verify(mockedMessageClass.reply('pong!')).never(); })function setMessageContents() { mockedMessageInstance.content = "Non-empty string"; }function whenIsPingThenReturn(result: boolean) { when(mockedPingFinderClass.isPing("Non-empty string")).thenReturn(result); } });

PingFinder的测试非常简单, 因为没有要模拟的依赖项。这是一个示例测试用例:
describe('PingFinder', () => { let service: PingFinder; beforeEach(() => { service = new PingFinder(); })it('should find "ping" in the string', () => { expect(service.isPing("ping")).to.be.true }) });

创建集成测试 除了单元测试之外, 我们还可以编写集成测试。主要区别在于这些测试中的依赖项没有被模拟。但是, 有些依赖项不应该进行测试, 例如外部API连接。在这种情况下, 我们可以创建模拟并将其重新绑定到容器, 以便注入模拟。这是有关此操作的示例:
import container from "../../inversify.config"; import {TYPES} from "../../src/types"; // ...describe('Bot', () => { let discordMock: Client; let discordInstance: Client; let bot: Bot; beforeEach(() => { discordMock = mock(Client); discordInstance = instance(discordMock); container.rebind< Client> (TYPES.Client) .toConstantValue(discordInstance); bot = container.get< Bot> (TYPES.Bot); }); // Test cases here});

这使我们结束了Discord机器人教程的结尾。恭喜, 你从一开始就使用TypeScript和DI进行了整洁的构建!此TypeScript依赖项注入示例是一种模式, 你可以将其添加到库中以用于任何项目。
TypeScript和依赖注入:不仅用于不和谐的Bot开发 无论我们在开发前端代码还是后端代码, 将TypeScript的面向对象的世界引入JavaScript都是一项巨大的增强。仅使用类型就可以避免很多错误。在TypeScript中进行依赖注入可以将更多面向对象的最佳实践推向基于JavaScript的开发。
【使用TypeScript,依赖注入和Discord Bot】当然, 由于语言的限制, 它永远不会像静态类型的语言那样简单自然。但是可以肯定的是:无论我们开发的是哪种应用程序, TypeScript, 单元测试和依赖项注入都使我们能够编写更具可读性, 松耦合和可维护的代码。

    推荐阅读