65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (19投票s)

2015年2月1日

CPOL

6分钟阅读

viewsIcon

148248

在 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 字段是一个包含两个属性的对象;namemessage。尽管由于 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 项目中强制使用它。

希望您觉得本教程很有用。如果您有任何疑问,请随时与我联系!

© . All rights reserved.