本文概述
- 设置你的Node.js项目
- 在Discord Apps资讯主页中建立新的应用程式
- 将Discord Bot添加到服务器
- 创建.env文件
- 编译TypeScript
- 创建Bot类
- 配置依赖项注入容器
- 在Discord Bot App中使用容器
- 实施业务逻辑
- 创建单元测试
- 创建集成测试
- TypeScript和依赖注入:不仅用于不和谐的Bot开发
在本TypeScript教程中, 除编译外, 我们将不直接介绍TypeScript基础。取而代之的是, 我们将逐步演示如何使用TypeScript最佳实践, 从头开始制作Discord机器人, 连接测试和DI以及创建示例服务。我们将使用:
- Node.js
- TypeScript
- Discord.js, Discord API的包装器
- InversifyJS, 一个依赖注入框架
- 测试库:Mocha, Chai和ts-mockito
- 奖金:Mongoose和MongoDB, 以便编写集成测试
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/。然后, 单击” 新建应用程序” 按钮:
文章图片
选择一个名称, 然后单击创建。然后, 单击Bot→添加Bot, 你已完成。让我们将机器人添加到服务器。但请不要关闭此页面, 我们需要尽快复制令牌。
将Discord Bot添加到服务器 为了测试我们的机器人, 我们需要一个Discord服务器。你可以使用现有服务器或创建新服务器。为此, 请复制机器人的CLIENT_ID(位于” 常规信息” 标签上), 并将其用作此特殊授权URL的一部分:
https://discordapp.com/oauth2/authorize?client_id=< CLIENT_ID> & scope=bot
当你在浏览器中点击此URL时, 将出现一个表单, 你可以在其中选择应将机器人添加到的服务器。
文章图片
将漫游器添加到服务器后, 你应该会看到类似以上的消息。
创建.env文件 我们需要某种方式将令牌保存在我们的应用程序中。为此, 我们将使用dotenv软件包。首先, 从Discord Application Dashboard获取令牌(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” 的每条消息:
文章图片
这是它在日志中的外观:
>
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, 单元测试和依赖项注入都使我们能够编写更具可读性, 松耦合和可维护的代码。
推荐阅读
- 使用CSS和JavaScript构建自定义的全页滑块
- 展望JavaScript的未来
- Rails 6的功能(新增功能和重要性)
- 如何进行现代WordPress开发(第1部分)
- JS框架会在2020年引发一场前端革命吗()
- Dart语言(Java和C#不够用时)
- 使用Bitbucket进行WordPress连续部署和版本控制
- 通过React集成测试提高代码可维护性
- CSS开发人员做什么以及为什么需要