使用 Node.js、TypeScript 和 WebSockets 编写聊天服务器






4.89/5 (19投票s)
在 Node.js 的背景下,掌握 TypeScript 及其编译器
引言
JavaScript 如果使用得当,可以使开发人员编写出清晰、模块化的代码。尽管如此,该语言也包含一些可能令初学者感到困惑的怪癖,例如 函数调用模式;开发人员最终会习惯它的运作方式,但在此过程中会遇到挫折。
这时就轮到 TypeScript 了。它由 C# 的首席架构师 Anders Hejlsberg 开发,是 JavaScript 的一个超集,引入了静态类型和泛型等功能,以及 ECMAScript 6 和 7 的提案,例如 lambda 表达式。编译器自然会提供类型检查,从而增强开发人员对代码的信心。
项目
在本教程中,我们将使用 Node.js 和 WebSockets 来编写一个非常简单的聊天服务器,WebSockets 支持通过 TCP 进行实时双向通信。当客户端向服务器发送消息时,消息将被验证并广播给所有已连接的客户端。
您可以在 GitHub 上查看我们将要完成的代码。
虽然您可以使用 Visual Studio 来开发 TypeScript 项目,但我编写本教程的方式是平台无关的,因此您可以在 Windows、Linux 和 OS X 上构建和运行服务器。
设置
如果您还没有安装,请安装 Node.js。这将同时安装 npm,我们将使用此包管理器来安装服务器的依赖项以及 TypeScript 编译器。准备好后,通过在命令行中运行 npm install -g typescript
来安装上述编译器(请注意,在 Windows 上,有一个配置好的 Node.js 命令提示符供您使用)。您可能需要 root 或管理员用户才能全局安装 Node 模块。
接下来,为项目创建一个目录。从命令行进入该目录并运行 npm init
。
cd <your_project_directory>
npm init
这将启动一个提示,最终会创建一个 package.json 文件,其中可以包含依赖项信息和其他各种元数据。
我们项目中的唯一依赖项是 ws 包。通过运行 npm install --save ws
来安装它。这将将其添加到 package.json 文件中,以便之后可以通过简单地运行 npm install
来解析依赖项。
注意:要构建 ws 所需的依赖项之一,您需要安装 Python 2。Python 不是我们项目的生产依赖项。
在编写任何代码之前,最好先完善我们项目的目录结构。在项目根目录下,创建以下文件夹:
- build - 包含我们编译后的 JavaScript
- declarations - 包含任何必需的 TypeScript 声明文件(稍后将详细介绍)
- src - 包含我们的 TypeScript 源代码
声明文件
“现在可以写代码了吗?”
还不可以。
在使用第三方框架或库时,TypeScript 编译器需要了解其 public
API 的结构;否则,我们的构建将会失败。TypeScript 通过 声明文件 提供了一种机制。
编写这些文件可能会是一项枯燥的任务,而且在我看来,这是该语言的一个缺点。幸运的是,出色的 DefinitelyTyped 项目托管了大量流行 JavaScript 技术的声明文件。
在我们的例子中,我们需要两个声明文件;分别用于 Node.js 和 ws。它们可以在 这里 和 这里 找到。将它们保存到 declarations 文件夹中。
我们差不多可以开始编写代码了,但有个小提示;ws 的声明文件依赖于 Node 的声明,但指向了错误的路径。打开 ws.d.ts 文件并找到以下行:
/// <reference path="../node/node.d.ts" />
将其替换为:
/// <reference path="node.d.ts" />
这些 引用注释 用于编译器,以根据预期的公共契约验证第三方代码。
现在我们可以写代码了!
在编写服务器代码之前,让我们先编写数据模型。在 src 目录中创建一个名为 models.ts 的文件,并编写以下接口:
'use strict';
interface Message {
name: string;
message: string;
}
如果您想知道看似任意的 'use strict';
字符串字面量的含义,请阅读 此处。
现在,在同一个文件中编写以下类,该类实现此接口:
export class UserMessage implements Message {
private data: { name: string; message: string };
constructor(payload: string) {
var data = JSON.parse(payload);
if (!data.name || !data.message) {
throw new Error('Invalid message payload received: ' + payload);
}
this.data = data;
}
get name(): string {
return this.data.name;
}
get message(): string {
return this.data.message;
}
}
这段代码中可能引起您注意的几点:
export
关键字 - 公开地将一个对象暴露给其他 TypeScript 模块。其底层机制取决于使用的底层技术。在 Node.js 的上下文中,它将一个适当的属性附加到其全局exports
对象上。private data: { [...] }
- 这利用了 TypeScript 的类型检查机制来确定 data 字段是一个包含两个属性的对象;name
和message
。尽管由于 WebSocket 数据的动态性,构造函数中进行了一些手动验证,但这仍然为预期的契约提供了一些清晰度。get name(): string { ...
- 就像在 C# 中一样,我们可以编写访问器来实现数据的封装!
现在进入激动人心的一步;编写我们的 WebSocket
服务器!在 src 文件夹中,创建一个名为 server.ts 的文件,并编写以下代码:
/// <reference path='../declarations/node.d.ts' />
/// <reference path='../declarations/ws.d.ts' />
'use strict';
import WebSocket = require('ws');
import models = require('./models');
var port: number = process.env.PORT || 3000;
var WebSocketServer = WebSocket.Server;
var server = new WebSocketServer({ port: port });
server.on('connection', ws => {
ws.on('message', message => {
try {
var userMessage: models.UserMessage = new models.UserMessage(message);
broadcast(JSON.stringify(userMessage));
} catch (e) {
console.error(e.message);
}
});
});
function broadcast(data: string): void {
server.clients.forEach(client => {
client.send(data);
});
};
console.log('Server is running on port', port);
关于这段代码的一些关注点:
import
关键字 - 我们可以像在纯 Node.js 服务器中那样使用变量声明,但此关键字的优点是编译器会自动构建相关的依赖项(不包括 Node 模块)。在我们的例子中,编译 server.ts 也会编译 models.ts。var port: number
- 这是 TypeScript 静态类型的又一个演示。- Lambda 表达式!有趣的是,这些已经被提议为 ECMAScript 6 的一部分。
现在我们可以编译我们的代码了。在命令行中,确保您位于项目根目录下并运行编译器:
tsc --removeComments --module commonjs --target ES5 --outDir build src/server.ts
让我们分解 tsc 的参数:
--removeComments
- 在编译后的生产代码中确实不需要注释。--module commonjs
- 这使得可以通过 Node 的 CommonJS 模块系统加载依赖项。没有它,编译器就无法构建我们的 models 脚本。--target ES5
- 编译为符合 ECMAScript 5 的 JavaScript。在我们的例子中,我们需要它来使用属性访问器,这些访问器在 JavaScript 中使用Object.defineProperty
实现;这在 ECMAScript 5.1 中已标准化。--outDir build
- 指定我们的构建目录。src/server.ts
- 我们要编译的脚本。请记住,这也会编译我们的 models 脚本。
代码编译完成后,您可以使用 node build/server
来运行它。
试试看!
在实际场景中,前端团队会编写一个客户端应用程序来使用此连接。为了简单起见,我们可以通过在浏览器中创建 WebSocket
实例来演示我们的服务器是否正常工作。Chrome、Firefox 和 IE10+ 都支持它们。
打开浏览器的开发者控制台,并写入以下内容来创建多个 WebSocket
连接:
var socket = new WebSocket('ws://:3000');
socket.onmessage = function (message) {
console.log('Connection 1', message.data);
};
var socket2 = new WebSocket('ws://:3000');
socket2.onmessage = function (message) {
console.log('Connection 2', message.data);
};
var socket3 = new WebSocket('ws://:3000');
socket3.onmessage = function (message) {
console.log('Connection 3', message.data);
};
要发送消息,请调用 socket.send(JSON.stringify({ name: 'Bob', message: 'Hello' }));
。您应该会看到所有三个连接都收到了数据。
我对 TypeScript 的看法
作为一名 JavaScript 开发人员,我非常熟悉该语言的大多数怪癖和特性,因此没有根本的动力在我的所有项目中使用 TypeScript。然而,作为一名 C# 开发人员,我喜欢 TypeScript 带来的功能;静态类型、接口、泛型、lambda 以及所有其他优点。就我个人而言,我会在需要对数据进行精细验证的浏览器或 Node.js 项目中强制使用它。
希望您觉得本教程很有用。如果您有任何疑问,请随时与我联系!