疯狂的技术宅 前端先锋 前端
每日前端夜话0x76
每日前端夜话,陪你聊前端。
天天晚上18:00准时推送。
正文共:3509 字
预计阅读时间: 10 分钟
翻译:疯狂的技术宅
来源:toptalnode
类型和可测试代码是避免错误的两种最有效方法,尤为是代码随会时间而变化。咱们能够分别经过利用 TypeScript 和依赖注入(DI)将这两种技术应用于JavaScript开发。git
在本 TypeScript 教程中,除编译之外,咱们不会直接介绍 TypeScript 的基础知识。相反,咱们将会演示 TypeScript 最佳实践,由于咱们将介绍如何从头开始制做 Discord bot、链接测试和 DI,以及建立示例服务。咱们将会使用:es6
首先,让咱们建立一个名为 typescript-bot 的新目录。而后输入并经过运行如下命令建立一个新的 Node.js 项目:typescript
1npm init
注意:你也能够用 yarn,但为了简洁起见,咱们用了 npm。npm
这将会打开一个交互式向导,对 package.json 文件进行配置。对于全部问题,你只需简单的按回车键(或者若是须要,能够提供一些信息)。而后,安装咱们的依赖项和 dev 依赖项(这些是测试所需的)。json
1npm i --save typescript discord.js inversify dotenv @types/node reflect-metadata 2npm i --save-dev chai mocha ts-mockito ts-node @types/chai @types/mocha
而后,将package.json中生成的 `scripts 部分替换为:后端
1"scripts": { 2 "start": "node src/index.js", 3 "watch": "tsc -p tsconfig.json -w", 4 "test": "mocha -r ts-node/register \"tests/**/*.spec.ts\"" 5},
为了可以递归地查找文件,须要在tests/*/.spec.ts周围加上双引号。 (注意:在 Windows 下的语法可能会有所不一样。)promise
start 脚本将用于启动机器人,watch 脚本用于编译 TypeScript 代码,test用于运行测试。浏览器
如今,咱们的 package.json 文件应以下所示:
1{ 2 "name": "typescript-bot", 3 "version": "1.0.0", 4 "description": "", 5 "main": "index.js", 6 "dependencies": { 7 "@types/node": "^11.9.4", 8 "discord.js": "^11.4.2", 9 "dotenv": "^6.2.0", 10 "inversify": "^5.0.1", 11 "reflect-metadata": "^0.1.13", 12 "typescript": "^3.3.3" 13 }, 14 "devDependencies": { 15 "@types/chai": "^4.1.7", 16 "@types/mocha": "^5.2.6", 17 "chai": "^4.2.0", 18 "mocha": "^5.2.0", 19 "ts-mockito": "^2.3.1", 20 "ts-node": "^8.0.3" 21 }, 22 "scripts": { 23 "start": "node src/index.js", 24 "watch": "tsc -p tsconfig.json -w", 25 "test": "mocha -r ts-node/register \"tests/**/*.spec.ts\"" 26 }, 27 "author": "", 28 "license": "ISC" 29}
为了与 Discord API进 行交互,咱们须要一个令牌。要生成这样的令牌,须要在 Discord 开发面板中注册一个应用。为此,你须要建立一个 Discord 账户并转到 https://discordapp.com/developers/applications/。而后,单击 New Application 按钮:
Discord的 "New Application" 按钮
选择一个名称,而后单击建立。而后,单击 Bot → Add Bot,你就完成了。让咱们将机器人添加到服务器。可是不要关闭此页面,咱们须要尽快复制令牌。
为了测试咱们的机器人,须要一台Discord服务器。你可使用现有服务器或建立新服务器。复制机器人的 CLIENT_ID 并将其做为这个特殊受权URL (https://discordapp.com/developers/docs/topics/oauth2#bot-authorization-flow) 的一部分使用:
1https://discordapp.com/oauth2/authorize?client_id=<CLIENT_ID>&scope=bot
当你在浏览器中点击此URL时,会出现一个表单,你能够在其中选择应添加机器人的服务器。
标准Discord欢迎消息
将bot添加到服务器后,你应该会看到如上所示的消息。
咱们须要一种可以在本身的程序中保存令牌的方法。为了作到这一点,咱们将使用 dotenv 包。首先,从Discord Application Dashboard获取令牌(Bot → Click to Reveal Token):
“Click to Reveal Token”连接
如今建立一个 .env 文件,而后在此处复制并粘贴令牌:
1TOKEN=paste.the.token.here
若是你使用了 Git,则该文件应标注在 .gitignore 中,以事令牌不会被泄露。另外,建立一个 .env.example 文件,提醒你 TOKEN 须要定义:
1TOKEN=
要编译 TypeScript,可使用 npm run watch 命令。或者,若是你用了其余 IDE,只需使用 TypeScript 插件中的文件监视器,让你的 IDE 去处理编译。让咱们经过建立一个带有内容的 src/index.ts 文件来测试本身设置:
1console.log('Hello')
另外,让咱们建立一个 tsconfig.json 文件,以下所示。 InversifyJS 须要experimentalDecorators,emitDecoratorMetadata,es6和reflect-metadata:
1{ 2 "compilerOptions": { 3 "module": "commonjs", 4 "moduleResolution": "node", 5 "target": "es2016", 6 "lib": [ 7 "es6", 8 "dom" 9 ], 10 "sourceMap": true, 11 "types": [ 12 // add node as an option 13 "node", 14 "reflect-metadata" 15 ], 16 "typeRoots": [ 17 // add path to @types 18 "node_modules/@types" 19 ], 20 "experimentalDecorators": true, 21 "emitDecoratorMetadata": true, 22 "resolveJsonModule": true 23 }, 24 "exclude": [ 25 "node_modules" 26 ] 27}
若是文件观监视器正常工做,它应该生成一个 src/index.js文件,并运行 npm start :
1> node src/index.js 2Hello
如今,咱们终于要开始使用 TypeScript 最有用的功能了:类型。继续建立如下 src/bot.ts 文件:
1import {Client, Message} from "discord.js"; 2export class Bot { 3 public listen(): Promise<string> { 4 let client = new Client(); 5 client.on('message', (message: Message) => {}); 6 return client.login('token should be here'); 7 } 8}
如今能够看到咱们须要的东西:一个 token!咱们是否是只须要将其复制粘贴到此处,或直接从环境中加载值就能够了呢?
都不是。相反,让咱们用依赖注入框架 InversifyJS 来注入令牌,这样能够编写更易于维护、可扩展和可测试的代码。
此外,咱们能够看到 Client 依赖项是硬编码的。咱们也将注入这个。
依赖注入容器是一个知道如何实例化其余对象的对象。一般咱们为每一个类定义依赖项,DI 容器负责解析它们。
InversifyJS 建议将依赖项放在 inversify.config.ts 文件中,因此让咱们在那里添加 DI 容器:
1import "reflect-metadata"; 2import {Container} from "inversify"; 3import {TYPES} from "./types"; 4import {Bot} from "./bot"; 5import {Client} from "discord.js"; 6 7let container = new Container(); 8 9container.bind<Bot>(TYPES.Bot).to(Bot).inSingletonScope(); 10container.bind<Client>(TYPES.Client).toConstantValue(new Client()); 11container.bind<string>(TYPES.Token).toConstantValue(process.env.TOKEN); 12 13export default container;
此外,InversifyJS文档推荐建立一个 types.ts文件,并连同相关的Symbol 列出咱们将要使用的每种类型。这很是不方便,但它确保了咱们的程序在扩展时不会发生命名冲突。每一个 Symbol 都是惟一的标识符,即便其描述参数相同(该参数仅用于调试目的)。
1export const TYPES = { 2 Bot: Symbol("Bot"), 3 Client: Symbol("Client"), 4 Token: Symbol("Token"), 5};
若是不使用 Symbol,将会发生如下命名冲突:
1Error: Ambiguous match found for serviceIdentifier: MessageResponder 2Registered bindings: 3 MessageResponder 4 MessageResponder
在这一点上,甚至更难以理清应该使用哪一个 MessageResponder,特别是当个人 DI 容器扩展到很大时。若是使用 Symbol 来处理这个问题,在有两个具备相同名称的类的状况下,就不会出现这些奇怪的文字。
如今,让咱们经过修改 Bot 类来使用容器。咱们须要添加 @injectable 和 @inject() 注释来作到这一点。这是新的 Bot 类:
1import {Client, Message} from "discord.js"; 2import {inject, injectable} from "inversify"; 3import {TYPES} from "./types"; 4import {MessageResponder} from "./services/message-responder"; 5 6@injectable() 7export class Bot { 8 private client: Client; 9 private readonly token: string; 10 11 constructor( 12 @inject(TYPES.Client) client: Client, 13 @inject(TYPES.Token) token: string 14 ) { 15 this.client = client; 16 this.token = token; 17 } 18 19 public listen(): Promise < string > { 20 this.client.on('message', (message: Message) => { 21 console.log("Message received! Contents: ", message.content); 22 }); 23 24 return this.client.login(this.token); 25 } 26}
最后,让咱们在 index.ts 文件中实例化 bot:
1require('dotenv').config(); // Recommended way of loading dotenv 2import container from "./inversify.config"; 3import {TYPES} from "./types"; 4import {Bot} from "./bot"; 5let bot = container.get<Bot>(TYPES.Bot); 6bot.listen().then(() => { 7 console.log('Logged in!') 8}).catch((error) => { 9 console.log('Oh no! ', error) 10});
如今,启动机器人并将其添加到你的服务器。若是你在服务器通道中输入消息,它应该出如今命令行的日志中,以下所示:
1> node src/index.js 2 3Logged in! 4Message received! Contents: Test
最后,咱们设置好了基础配置:TypeScript 类型和咱们的机器人内部的依赖注入容器。
让咱们直接介绍本文的核心内容:建立一个可测试的代码库。简而言之,咱们的代码应该实现最佳实践(如 SOLID ),不隐藏依赖项,不使用静态方法。
此外,它不该该在运行时引入反作用,而且很容易模拟。
为了简单起见,咱们的机器人只作一件事:它将扫描传入的消息,若是其中包含单词“ping”,咱们将用一个 Discord bot 命令让机器人对那个用户响应“pong! “。
为了展现如何将自定义对象注入 Bot 对象并对它们进行单元测试,咱们将建立两个类: PingFinder 和 MessageResponder。咱们将 MessageResponder 注入 Bot 类,将 PingFinder 注入 MessageResponder。
这是 src/services/ping-finder.ts 文件:
1import {injectable} from "inversify"; 2 3@injectable() 4export class PingFinder { 5 6 private regexp = 'ping'; 7 8 public isPing(stringToSearch: string): boolean { 9 return stringToSearch.search(this.regexp) >= 0; 10 } 11}
而后咱们将该类注入 src/services/message-responder.ts 文件:
1import {Message} from "discord.js"; 2import {PingFinder} from "./ping-finder"; 3import {inject, injectable} from "inversify"; 4import {TYPES} from "../types"; 5 6@injectable() 7export class MessageResponder { 8 private pingFinder: PingFinder; 9 10 constructor( 11 @inject(TYPES.PingFinder) pingFinder: PingFinder 12 ) { 13 this.pingFinder = pingFinder; 14 } 15 16 handle(message: Message): Promise<Message | Message[]> { 17 if (this.pingFinder.isPing(message.content)) { 18 return message.reply('pong!'); 19 } 20 21 return Promise.reject(); 22 } 23}
最后,这是一个修改过的 Bot 类,它使用 MessageResponder 类:
1import {Client, Message} from "discord.js"; 2import {inject, injectable} from "inversify"; 3import {TYPES} from "./types"; 4import {MessageResponder} from "./services/message-responder"; 5 6@injectable() 7export class Bot { 8 private client: Client; 9 private readonly token: string; 10 private messageResponder: MessageResponder; 11 12 constructor( 13 @inject(TYPES.Client) client: Client, 14 @inject(TYPES.Token) token: string, 15 @inject(TYPES.MessageResponder) messageResponder: MessageResponder) { 16 this.client = client; 17 this.token = token; 18 this.messageResponder = messageResponder; 19 } 20 21 public listen(): Promise<string> { 22 this.client.on('message', (message: Message) => { 23 if (message.author.bot) { 24 console.log('Ignoring bot message!') 25 return; 26 } 27 28 console.log("Message received! Contents: ", message.content); 29 30 this.messageResponder.handle(message).then(() => { 31 console.log("Response sent!"); 32 }).catch(() => { 33 console.log("Response not sent.") 34 }) 35 }); 36 37 return this.client.login(this.token); 38 } 39}
在当前状态下,程序还没法运行,由于没有 MessageResponder 和 PingFinder 类的定义。让咱们将如下内容添加到 inversify.config.ts 文件中:
1container.bind<MessageResponder>(TYPES.MessageResponder).to(MessageResponder).inSingletonScope(); 2container.bind<PingFinder>(TYPES.PingFinder).to(PingFinder).inSingletonScope();
另外,咱们将向 types.ts 添加类型符号:
1MessageResponder: Symbol("MessageResponder"), 2PingFinder: Symbol("PingFinder"),
如今,在从新启动程序后,机器人应该响应包含 “ping” 的每条消息:
机器人响应包含“ping”一词的消息
这是它在日志中的样子:
1> node src/index.js 2 3Logged in! 4Message received! Contents: some message 5Response not sent. 6Message received! Contents: message with ping 7Ignoring bot message! 8Response sent!
如今咱们已经正确地注入了依赖项,编写单元测试很容易。咱们将使用 Chai 和 ts-mockito。不过你也可使用其余测试器和模拟库。
ts-mockito 中的模拟语法很是冗长,但也很容易理解。如下是如何设置 MessageResponder 服务并将 PingFinder mock 注入其中:
1let mockedPingFinderClass = mock(PingFinder); 2let mockedPingFinderInstance = instance(mockedPingFinderClass); 3 4letservice=newMessageResponder(mockedPingFinderInstance);
如今咱们已经设置好了mocks ,咱们能够定义 isPing() 调用的结果应该是什么,并验证 reply() 调用。在单元测试中的关键是定义 isPing():true 或 false 的结果。消息内容是什么并不重要,因此在测试中咱们只使用 "Non-empty string"。
1when(mockedPingFinderClass.isPing("Non-empty string")).thenReturn(true); 2await service.handle(mockedMessageInstance) 3verify(mockedMessageClass.reply('pong!')).once();
如下是整个测试代码:
1import "reflect-metadata"; 2import 'mocha'; 3import {expect} from 'chai'; 4import {PingFinder} from "../../../src/services/ping-finder"; 5import {MessageResponder} from "../../../src/services/message-responder"; 6import {instance, mock, verify, when} from "ts-mockito"; 7import {Message} from "discord.js"; 8 9describe('MessageResponder', () => { 10 let mockedPingFinderClass: PingFinder; 11 let mockedPingFinderInstance: PingFinder; 12 let mockedMessageClass: Message; 13 let mockedMessageInstance: Message; 14 15 let service: MessageResponder; 16 17 beforeEach(() => { 18 mockedPingFinderClass = mock(PingFinder); 19 mockedPingFinderInstance = instance(mockedPingFinderClass); 20 mockedMessageClass = mock(Message); 21 mockedMessageInstance = instance(mockedMessageClass); 22 setMessageContents(); 23 24 service = new MessageResponder(mockedPingFinderInstance); 25 }) 26 27 it('should reply', async () => { 28 whenIsPingThenReturn(true); 29 30 await service.handle(mockedMessageInstance); 31 32 verify(mockedMessageClass.reply('pong!')).once(); 33 }) 34 35 it('should not reply', async () => { 36 whenIsPingThenReturn(false); 37 38 await service.handle(mockedMessageInstance).then(() => { 39 // Successful promise is unexpected, so we fail the test 40 expect.fail('Unexpected promise'); 41 }).catch(() => { 42 // Rejected promise is expected, so nothing happens here 43 }); 44 45 verify(mockedMessageClass.reply('pong!')).never(); 46 }) 47 48 function setMessageContents() { 49 mockedMessageInstance.content = "Non-empty string"; 50 } 51 52 function whenIsPingThenReturn(result: boolean) { 53 when(mockedPingFinderClass.isPing("Non-empty string")).thenReturn(result); 54 } 55});
“PingFinder” 的测试很是简单,由于没有依赖项被mock。这是一个测试用例的例子:
1describe('PingFinder', () => { 2 let service: PingFinder; 3 beforeEach(() => { 4 service = new PingFinder(); 5 }) 6 7 it('should find "ping" in the string', () => { 8 expect(service.isPing("ping")).to.be.true 9 }) 10});
除了单元测试,咱们还能够编写集成测试。主要区别在于这些测试中的依赖关系不会被模拟。可是,有些依赖项不该该像外部 API 链接那样进行测试。在这种状况下,咱们能够建立模拟并将它们 rebind 到容器中,以便替换注入模拟。这是一个例子:
1import container from "../../inversify.config"; 2import {TYPES} from "../../src/types"; 3// ... 4 5describe('Bot', () => { 6 let discordMock: Client; 7 let discordInstance: Client; 8 let bot: Bot; 9 10 beforeEach(() => { 11 discordMock = mock(Client); 12 discordInstance = instance(discordMock); 13 container.rebind<Client>(TYPES.Client) 14 .toConstantValue(discordInstance); 15 bot = container.get<Bot>(TYPES.Bot); 16 }); 17 18 // Test cases here 19 20});
到这里咱们的 Discord bot 教程就结束了。恭喜你干净利落地用 TypeScript 和 DI 完成了它!这里的 TypeScript 依赖项注入示例是一种模式,你能够将其添加到你的知识库中一遍在其余项目中使用。
不管咱们是处理前端仍是后端代码,将 TypeScript 的面向对象引入 JavaScript 都是一个很大的改进。仅仅使用类型就能够避免许多错误。在 TypeScript 中进行依赖注入会将更多面向对象的最佳实践推向基于 JavaScript 的开发。
固然因为语言的局限性,它永远不会像静态类型语言那样容易和天然。但有一件事是确定的:TypeScript、单元测试和依赖注入容许咱们编写更易读、松散耦合和可维护的代码 —— 不管咱们正在开发什么类型的应用。
原文:https://www.toptal.com/typescript/dependency-injection-discord-bot-tutorial