用Node.js构建实时应用程序的入门指南
在一个时间价值稳步增长的世界里,构建用户可以实时互动的应用程序已经成为大多数开发者的常态。我们今天看到的大多数应用程序,无论是移动、桌面还是网络应用,都至少包含了一个实时功能。举例来说,实时消息和通知是应用程序中最常用的两个实时功能。
在本文中,我们将通过建立一个实时聊天,向你介绍使用Node.js开发实时应用程序的情况。尽管文章将关注这个特定的用例,但这里教授的概念也可以适用于其他场景。
事实上,由于Node的事件驱动和异步特性,它是构建实时应用程序的最佳编程语言之一。在一头扎进构建实时应用程序之前,我们将看看使用Node.js可以构建什么样的实时应用程序。
实时应用程序用在哪里?
正如我上面提到的,消息传递和通知交付是实时应用程序最常见的两个用例。但我们可以将实时应用程序用于其他无数的目的。让我们看看它们是什么。
实时消息传递
我们大多数人都熟悉实时消息应用程序的使用,特别是在移动设备中,以Whatsapp、Facebook Messenger和许多其他消息应用程序的形式。然而,实时消息的使用并不限于纯粹的消息应用。我们在按需打车应用、送货应用和协作平台中看到实时消息功能。
实时通知交付
在提高用户对应用程序的参与度方面,启用实时通知已被证明是一个游戏规则的改变。出于这个原因,你很难看到一个现代应用程序不向其用户实时提供通知。
实时流媒体
在社交媒体平台将直播视频流整合到他们的应用程序后,用户可以实时互动的直播流正变得越来越流行。Instagram和Facebook的视频直播功能是这方面的最好例子。
实时跟踪
随着Uber和Amazon等流行的出租车和快递应用的推出,实时跟踪用户乘坐出租车或送货的进度已成为一项基本要求。他们的实时进度更新增加了这些应用程序的可用性和可靠性。
物联网设备
实时功能对物联网设备至关重要。由放置在物联网设备中的传感器捕获的数据被传输、处理,并以最小的延迟显示给终端用户。由于这些设备捕获的大多数输入,如温度和照明,随着时间不断变化,与物联网设备一起工作的应用程序应该能够实时接收和发送数据。
我们怎样才能构建实时应用程序?
构建一个实时应用程序与构建一个普通的网络应用程序不同吗?答案是,是的。
想一想一个消息应用程序,用户可以实时发送消息。这些信息应该在信息发送后立即出现在其他用户的应用程序上。如果我们像普通的Web应用一样实现这个应用,只有客户端可以向服务器发起请求来接收数据,那么用户就必须定期刷新网页以看到最新的消息,或者客户端应该在短时间内向服务器发送AJAX请求来检索最新的消息。两者中的前者对用户不太友好,后者则是对应用资源的浪费。那么,很显然,我们必须有一种不同的方法来构建实时应用程序,使其更有意义。
WebSocket提供了我们需要的解决方案。WebSocket是一种通信协议,允许客户端和服务器发起通信。换句话说,通过WebSocket,服务器可以随时向客户端发送数据,而无需客户端先请求数据。在之前的消息传递应用中,我们可以使用WebSocket,通过服务器即时向所有用户发送消息。在构建应用程序时,我们可以使用WebSocket API来使用WebSockets进行通信。
Socket.io
然而,在使用Node实现实时应用程序时,我们不必直接使用WebSocket API。相反,Javascript和Node.js库Socket.io是WebSocket API的一个API,为我们提供了一个更简单的WebSockets实现。在本教程中,我们将使用Socket.io来创建和管理客户端和服务器之间的WebSocket连接。
用Node.js构建一个实时聊天室
现在我们已经介绍了实时应用程序的开发背景,我们可以开始创建自己的实时应用程序。在本教程中,我们将建立一个简单的聊天室,用户可以用它来与其他连接的用户交流。任何数量的用户都可以连接到聊天室,一个用户发送的信息对所有连接到聊天室的用户都是即时可见的。
我们的简单聊天室将有以下一组功能。
- 改变用户的用户名
- 发送消息
- 显示另一个用户是否正在输入信息
酷,现在我们有了自己的要求,让我们开始建立环境和设置结构吧
建立应用环境
首先,为应用程序创建一个新的目录。然后,运行the npm init 来设置package.json 文件。确保在这一步,你指定app.js 作为你的主脚本,如果你没有,不要担心,你可以在以后的时候在你的package.json 中改变它。
安装依赖项
在本教程中,我们使用Express、ejs、socket.io和nodemon包来构建应用程序。
- Ejs是一个流行的JS模板引擎
- 我们在前面讨论了socket.io的使用。
- Nodemon是一个包,在我们每次对应用程序代码进行修改时都会重新启动服务器。它消除了我们在每次做出改变时手动停止和启动服务器的需要。与其他软件包不同,我们将nodemon安装为开发依赖项,因为我们只将其用于开发目的。
使用以下命令安装Express、ejs和socket.io。
npm install express ejs socket.io --save
使用该命令将nodemon作为开发依赖项安装。
npm install nodemon --save-dev
为了用nodemon启动应用程序,我们应该在package.json文件中添加一个启动脚本。
"scripts": {
"start": "nodemon app.js",
},
然后,我们可以通过在命令行上运行以下命令来启动应用程序。
npm run start
如果失败了,不要担心,这基本上是因为我们还没有任何代码文件。
设置应用程序的结构
在安装了本项目所需的所有依赖项后,让我们来建立项目结构。为此,你将需要创建一些目录,现在,有一个文件叫做app.js 。让我们完成这些,使你的应用程序结构看起来如下。
|--app.js
|--views
|--node_modules
|--package.json
|--public
|--css
|--js
我想这个结构已经很清楚了,但让我们快速浏览一下。
app.js文件:我们将用它来存放我们的服务器端代码views: 包含视图(ejs)的文件夹node_modules: 我们安装依赖项的地方package.jsonnpm配置文件public存储资产的目录:我们将用来存储资产,如css文件、javascript文件(用于客户端)和图片。
构建服务器的第一个步骤
在我们考虑做实时连接之前,我们首先要做的是让express ,为此,让我们打开我们的app.js 文件并粘贴以下代码。
const express = require('express')
const socketio = require('socket.io')
const app = express()
app.set('view engine', 'ejs')
app.use(express.static('public'))
app.get('/', (req, res)=> {
res.render('index')
})
const server = app.listen(process.env.PORT || 3000, () => {
console.log("server is running")
})
一旦我们配置好express ,并使用ejs 作为模板系统,我们就可以开始进行sockets.io的初始化工作。为此,在你的app.js 文件的末尾添加以下代码。
//initialize socket for the server
const io = socketio(server)
io.on('connection', socket => {
console.log("New user connected")
})
这段代码非常直接,我们从我们的server 连接(express)初始化socket.io ,然后我们使用io.on() 设置一个偶数,这将在每次建立一个新的套接字连接时被触发。
如果你现在用npm start 来运行你的服务器,你将能够接收新的套接字连接。所以让我们开始构建我们的前端。
构建我们的前端
我们不会花太多时间让我们的前端看起来很好,但我们会解释与服务器的连接是如何工作的,如何emit 和capture socket 事件,我们会把所有这些应用到我们的聊天例子中。
让我们先在我们的视图文件夹中创建一个模板,为此创建一个index.ejs 文件并粘贴以下代码。
<!DOCTYPE html>
<head>
<title>Simple realtime chatroom</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="title">
<h3>Realtime Chat Room</h3>
</div>
<div class="card">
<div class="card-header">Anonymous</div>
<div class="card-body">
<div class="input-group">
<input type="text" class="form-control" id="username" placeholder="Change your username" >
<div class="input-group-append">
<button class="btn btn-warning" type="button" id="usernameBtn">Change</button>
</div>
</div>
</div>
<div class="message-box">
<ul class="list-group list-group-flush" id="message-list"></ul>
<div class="info"></div>
</div>
<div class="card-footer">
<div class="input-group">
<input type="text" class="form-control" id="message" placeholder="Send new message" >
<div class="input-group-append">
<button class="btn btn-success" type="button" id="messageBtn">Send</button>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.4/socket.io.js"></script>
<script src="/js/chatroom.js"></script>
</body>
</html>
注意我们是如何将客户端socket.io库的脚本和我们将在此代码中使用的自定义javascript文件包括在内的。
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.4/socket.io.js"></script>
<script src="/js/chatroom.js"></script>
我们还有一个ID为messageBtn 的按钮来发送一条新的消息,还有一个ID为usernameBtn 的按钮来提交一个新的用户名。用户名和消息输入的ID分别为username 和message 。所有的用户信息都应该出现在ID为message-list 的无序列表中。如果一个用户正在捆绑一条信息,该信息将出现在类别为info 的div内。
如果你打开我们的浏览器并前往http://localhost:3000/ ,你的应用程序将看起来像这样。
实时聊天室的布局
但它没有做任何事情,按钮不会工作,而且将是一个几乎静态的应用程序。所以接下来让我们开始把前端连接到服务器上。
为此,在公共目录的js文件夹中创建一个新的Javascript文件,名为chatroom.js (注意在上面的HTML中,我已经引用了这个文件)。在这个Javascript文件中,我们需要从前端连接到socket。我们可以这样做。
(function connect(){
let socket = io.connect('http://localhost:3000')
})()
再次访问你的网站,在你的终端(在服务器端)你会看到类似的东西。
终端日志示例
太棒了!你的应用程序已经开始工作了,尽管它没有做什么。接下来我们来建立功能
改变用户名
我们为每个连接使用的默认用户名是 "匿名"。我们让用户可以选择改变这个用户名。我们将设置后端在前端发出change_username 事件时改变用户名。回到你的服务器端代码(app.js),编辑你的connection 事件,添加新代码。
io.on('connection', socket => {
console.log("New user connected")
socket.username = "Anonymous"
socket.on('change_username', data => {
socket.username = data.username
})
})
接下来,我们需要调整我们的前端,以便当我们按下更改用户名的按钮时,它向服务器发出一个事件,名称为change_username 。看到我们是如何通过发射和捕获相同的事件名称来建立这个名称的吗?
在chatroom.js 里面,我们要给usernameBtn 添加一个事件监听器,当按钮被点击时发出一个change_username 事件。
(function connect(){
let socket = io.connect('http://localhost:3000')
let username = document.querySelector('#username')
let usernameBtn = document.querySelector('#usernameBtn')
let curUsername = document.querySelector('.card-header')
usernameBtn.addEventListener('click', e => {
console.log(username.value)
socket.emit('change_username', {username: username.value})
curUsername.textContent = username.value
username.value = ''
})
})()
现在,如果你重新加载网页并提交一个新的用户名,你会看到你当前的用户名变成了新的。接下来,让我们开始发送消息。
发送消息
我们要实现的下一个功能是发送消息。这里事情开始变得有点不同,到目前为止,我们说每次前端发出的消息服务器都会收到,然而在我们的新案例中,前端需要发出一个new_message 事件,然后需要发送给所有连接的客户端,这样他们就可以打印新消息了。
首先,我们将设置前端在有新消息提交时发出一个new_message 事件。由于客户端也应该被配置为接收其他用户从服务器发送的新消息,应用程序也应该在前端监听receive_message 事件,并在网页上适当地显示新消息。我们可以使用下面的代码来实现这两项任务,该代码位于chatroom.js 中的前一个connect 函数内。
let message = document.querySelector('#message')
let messageBtn = document.querySelector('#messageBtn')
let messageList = document.querySelector('#message-list')
messageBtn.addEventListener('click', e => {
console.log(message.value)
socket.emit('new_message', {message: message.value})
message.value = ''
})
socket.on('receive_message', data => {
console.log(data)
let listItem = document.createElement('li')
listItem.textContent = data.username + ': ' + data.message
listItem.classList.add('list-group-item')
messageList.appendChild(listItem)
})
每当receive_message 事件在客户端发生时,我们就会改变我们的DOM,将消息渲染到屏幕上。
在后端,当我们收到一个new_message 事件时,我们需要向所有客户端发出一个新的事件,为此我们使用io.sockets.emit() 函数。在你的app.js 文件中改变你的connection 事件,如下所示。
io.on('connection', socket => {
console.log("New user connected")
socket.username = "Anonymous"
socket.on('change_username', data => {
socket.username = data.username
})
//handle the new message event
socket.on('new_message', data => {
console.log("new message")
io.sockets.emit('receive_message', {message: data.message, username: socket.username})
})
})
在处理new_message 事件时,服务器本身会向连接的客户端发出一个receive_message 事件,其中包含关于新消息的数据。所有连接到服务器的用户都会收到这个事件,包括发送消息的用户,因此新消息会显示在他们的聊天室界面上。
如果你现在在浏览器中打开你的网络应用(你可以有多个实例),你就可以开始聊天了(和你自己? :p)
你可以用两个不同的浏览器连接到聊天室,玩玩发送消息的功能,看看一个用户发送的消息是如何即时出现在两个用户的应用界面上的。
实时聊天室 - 发送消息
我在打字....
在我们今天使用的大多数实时消息应用程序中,每当另一个用户正在输入信息时,我们会看到一个简单的文本,上面写着 "用户X正在输入..."。这给应用程序带来了更多的实时感,提高了用户体验。我们将在我们的应用程序中加入这一功能。
首先,让我们考虑前端的实现。我们给消息输入框添加一个新的事件监听器,每当有按键发生时,就会发出一个typing 事件。由于在消息输入框上的按键表示用户正在输入一条消息,typing 事件告诉服务器用户正在输入一条消息。客户端也会监听服务器发出的typing 事件,以了解另一个用户当前是否正在输入消息,并在用户界面上显示。
同样,在chatroom.js 的connect函数里面,我们添加了以下代码。
let info = document.querySelector('.info')
message.addEventListener('keypress', e => {
socket.emit('typing')
})
socket.on('typing', data => {
info.textContent = data.username + " is typing..."
setTimeout(() => {info.textContent=''}, 5000)
})
如果一个用户正在输入信息,其他用户将显示 "正在输入...... "的文字,持续5秒钟。
现在我们需要设置后端来处理打字事件。我们在这里使用的代码是这样的。
socket.on('typing', data => {
socket.broadcast.emit('typing', {username: socket.username})
})
在这里,socket.io使用broadcast 函数来通知连接的客户端。当我们使用broadcast ,除了正在打字的用户,每个用户都会收到来自服务器的打字事件。因此,除了正在输入信息的用户外,每个用户都会显示 "正在输入... "的文字。
同样,你可以从两个浏览器连接到聊天室,看看这是如何实时工作的。
实时聊天室 - 打字
真棒!
摘要
今天,在桌面、移动和网络应用中使用实时功能几乎已经成为一种必要。在这篇文章中,我们介绍了一些实时应用程序的应用,并学习了如何在Node.js和Socket.io的帮助下创建一个实时聊天室。从这里继续,你可以尝试改进这个聊天室,增加更多的功能,使用数据库来保存旧的信息,或者实现另一个有不同用例的实时应用程序。