如何使用 Socket.IO、Angular 和 Node.js 创建实时应用程序

介绍

WebSocket 是一种允许服务器和客户端之间进行全双工通信的互联网协议。该协议超越了典型的 HTTP 请求和响应范式。通过 WebSocket,服务器可以向客户端发送数据,而无需客户端发起请求,因此可以实现一些非常有趣的应用程序。

在本教程中,您将构建一个实时文档协作应用程序(类似于 Google Docs)。我们将使用 Socket.IO Node.js 服务器框架和 Angular 7 来实现这一目标。

您可以在 GitHub 上找到此示例项目的完整源代码。

先决条件

要完成本教程,您需要:

  • 在本地安装 Node.js,您可以按照《如何安装 Node.js 并创建本地开发环境》中的步骤进行操作。
  • 一个支持 WebSocket 的现代 Web 浏览器。

本教程最初是在 Node.js v8.11.4、npm v6.4.1 和 Angular v7.0.4 的环境中编写的。

本教程已经验证通过了 Node v14.6.0、npm v6.14.7、Angular v10.0.5 和 Socket.IO v2.3.0。

步骤 1 — 设置项目目录并创建 Socket 服务器

首先,打开您的终端并创建一个新的项目目录,该目录将包含我们的服务器和客户端代码:

mkdir socket-example

接下来,切换到项目目录:

cd socket-example

然后,为服务器代码创建一个新的目录:

mkdir socket-server

接着,切换到服务器目录。

cd socket-server

然后,初始化一个新的 npm 项目:

npm init -y

现在,我们将安装我们的包依赖项:

npm install express@4.17.1 socket.io@2.3.0 @types/socket.io@2.1.10 --save

这些包包括 Express、Socket.IO 和 @types/socket.io

现在,您已经完成了项目的设置,可以继续编写服务器代码。

首先,创建一个新的 src 目录:

mkdir src

现在,在 src 目录中创建一个名为 app.js 的新文件,并使用您喜欢的文本编辑器打开它:

nano src/app.js

从 Express 和 Socket.IO 开始编写 app.js 文件的 require 语句:

const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);

正如您所看到的,我们使用 Express 和 Socket.IO 来设置我们的服务器。Socket.IO 提供了对原生 WebSocket 的抽象层。它带有一些很好的功能,例如对不支持 WebSocket 的旧版浏览器的回退机制,以及创建“房间”的能力。我们将在下一步中看到这一点。

对于我们的实时文档协作应用程序,我们将需要一种存储 documents 的方式。在生产环境中,您可能希望使用数据库,但在本教程的范围内,我们将使用一个存储 documents 的内存存储:

const documents = {};

现在,让我们定义我们希望我们的 socket 服务器实际执行的操作:

io.on("connection", socket => {// ...
});

让我们来分解一下。.on('...') 是一个事件监听器。第一个参数是事件的名称,第二个参数通常是在事件触发时执行的回调函数,带有事件负载。

我们首先看到的示例是当客户端连接到 socket 服务器时(connection 是 Socket.IO 中的保留事件类型)。

我们获得一个 socket 变量,以便将其传递给我们的回调函数,以便与该 socket 或多个 socket(即广播)进行通信。

safeJoin

我们将设置一个本地函数(safeJoin),用于处理加入和离开“房间”:

io.on("connection", socket => {let previousId;const safeJoin = currentId => {socket.leave(previousId);socket.join(currentId, () => console.log(`Socket ${socket.id} joined room ${currentId}`));previousId = currentId;};// ...
});

在这种情况下,当客户端加入一个房间时,它们正在编辑特定的文档。因此,如果多个客户端在同一个房间中,它们都在编辑同一个文档。

从技术上讲,一个 socket 可以在多个房间中,但我们不希望让一个客户端同时编辑多个文档,因此如果他们切换文档,我们需要离开先前的房间并加入新的房间。这个小函数负责处理这个问题。

我们的 socket 正在监听来自客户端的三种事件类型:

  • getDoc
  • addDoc
  • editDoc

以及从我们的 socket 发出的两种事件类型:

  • document
  • documents

getDoc

让我们来处理第一种事件类型 - getDoc

io.on("connection", socket => {// ...socket.on("getDoc", docId => {safeJoin(docId);socket.emit("document", documents[docId]);});// ...
});

当客户端发出 getDoc 事件时,socket 将获取负载(在我们的情况下,它只是一个 id),加入具有该 docId 的房间,并将存储的 document 发送回发起请求的客户端。这就是 socket.emit('document', ...) 起作用的地方。

addDoc

让我们来处理第二种事件类型 - addDoc

io.on("connection", socket => {// ...socket.on("addDoc", doc => {documents[doc.id] = doc;safeJoin(doc.id);io.emit("documents", Object.keys(documents));socket.emit("document", doc);});// ...
});

使用 addDoc 事件,负载是一个 document 对象,目前只包含客户端生成的 id。我们告诉我们的 socket 加入该 ID 的房间,以便将来的编辑可以广播给同一房间中的任何人。

接下来,我们希望连接到我们的服务器的所有人都知道有一个新的文档可供使用,因此我们使用 io.emit('documents', ...) 函数向所有客户端广播。

请注意 socket.emit()io.emit() 之间的区别 - socket 版本用于仅向发起请求的客户端发出,io 版本用于向连接到我们的服务器的所有人发出。

editDoc

让我们来处理第三种事件类型 - editDoc

io.on("connection", socket => {// ...socket.on("editDoc", doc => {documents[doc.id] = doc;socket.to(doc.id).emit("document", doc);});// ...
});

使用 editDoc 事件,负载将是任何按键后文档的整个状态。我们将替换数据库中的现有文档,然后将新文档广播给当前正在查看该文档的客户端。我们通过调用 socket.to(doc.id).emit(document, doc) 来实现这一点,该方法会向该特定房间中的所有 socket 发出。

最后,每当建立新连接时,我们向所有客户端广播,以确保新连接在连接时接收到最新的文档更改:

io.on("connection", socket => {// ...io.emit("documents", Object.keys(documents));console.log(`Socket ${socket.id} has connected`);
});

在设置好 socket 函数之后,选择一个端口并在其上进行监听:

http.listen(4444, () => {console.log('Listening on port 4444');
});

在您的终端中运行以下命令以启动服务器:

node src/app.js

现在,我们已经拥有了一个完全功能的用于文档协作的 socket 服务器!

步骤 2 — 安装 @angular/cli 并创建客户端应用

打开一个新的终端窗口并导航到项目目录。

运行以下命令将 Angular CLI 安装为 devDependency

npm install @angular/cli@10.0.4 --save-dev

现在,使用 @angular/cli 命令创建一个新的 Angular 项目,不使用 Angular 路由,并使用 SCSS 进行样式设置:

ng new socket-app --routing=false --style=scss

然后,切换到服务器目录:

cd socket-app

现在,我们将安装我们的包依赖项:

npm install ngx-socket-io@3.2.0 --save

ngx-socket-io 是 Socket.IO 客户端库的 Angular 封装。

然后,使用 @angular/cli 命令生成 document 模型、document-list 组件、document 组件和 document 服务:

ng generate class models/document --type=model
ng generate component components/document-list
ng generate component components/document
ng generate service services/document

现在,您已经完成了项目的设置,可以继续为客户端编写代码。

应用模块

打开 app.modules.ts

nano src/app/app.module.ts

并导入 FormsModuleSocketioModuleSocketioConfig

// ... 其他导入
import { FormsModule } from '@angular/forms';
import { SocketIoModule, SocketIoConfig } from 'ngx-socket-io';

@NgModule 声明之前,定义 config

const config: SocketIoConfig = { url: 'http://localhost:4444', options: {} };

您会注意到这是我们在服务器的 app.js 中之前声明的端口号。

现在,将其添加到您的 imports 数组中,使其如下所示:

@NgModule({// ...imports: [// ...FormsModule,SocketIoModule.forRoot(config)],// ...
})

这将在 AppModule 加载时触发与我们的 socket 服务器的连接。

Document 模型和 Document 服务

打开 document.model.ts

nano src/app/models/document.model.ts

并定义 iddoc

export class Document {id: string;doc: string;
}

打开 document.service.ts

nano src/app/services/document.service.ts

并在类定义中添加以下内容:

import { Injectable } from '@angular/core';
import { Socket } from 'ngx-socket-io';
import { Document } from 'src/app/models/document.model';@Injectable({providedIn: 'root'
})
export class DocumentService {currentDocument = this.socket.fromEvent<Document>('document');documents = this.socket.fromEvent<string[]>('documents');constructor(private socket: Socket) { }getDocument(id: string) {this.socket.emit('getDoc', id);}newDocument() {this.socket.emit('addDoc', { id: this.docId(), doc: '' });}editDocument(document: Document) {this.socket.emit('editDoc', document);}private docId() {let text = '';const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';for (let i = 0; i < 5; i++) {text += possible.charAt(Math.floor(Math.random() * possible.length));}return text;}
}

这里的方法代表了 socket 服务器正在监听的三种事件类型的每个发射。currentDocumentdocuments 属性代表了 socket 服务器发射的事件,在客户端作为 Observable 进行消费。您可能会注意到对 this.docId() 的调用。这是一个小的私有方法,用于生成一个随机字符串,分配为文档 id。

Document 列表组件

让我们将文档列表放在一个侧边栏中。目前,它只显示 docId - 一串随机字符。

打开 document-list.component.html

nano src/app/components/document-list/document-list.component.html

并用以下内容替换其中的内容:

<div class='sidenav'><span(click)='newDoc()'>New Document</span><span[class.selected]='docId === currentDoc'(click)='loadDoc(docId)'*ngFor='let docId of documents | async'>{{ docId }}</span>
</div>

打开 document-list.component.scss

nano src/app/components/document-list/document-list.component.scss

并添加一些样式:

.sidenav {background-color: #111111;height: 100%;left: 0;overflow-x: hidden;padding-top: 20px;position: fixed;top: 0;width: 220px;span {color: #818181;display: block;font-family: 'Roboto', Tahoma, Geneva, Verdana, sans-serif;font-size: 25px;padding: 6px  8px  6px  16px;text-decoration: none;&.selected {color: #e1e1e1;}&:hover {color: #f1f1f1;cursor: pointer;}}
}

打开 document-list.component.ts

nano src/app/components/document-list/document-list.component.ts

并在类定义中添加以下内容:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Observable, Subscription } from 'rxjs';import { DocumentService } from 'src/app/services/document.service';@Component({selector: 'app-document-list',templateUrl: './document-list.component.html',styleUrls: ['./document-list.component.scss']
})
export class DocumentListComponent implements OnInit, OnDestroy {documents: Observable<string[]>;currentDoc: string;private _docSub: Subscription;constructor(private documentService: DocumentService) { }ngOnInit() {this.documents = this.documentService.documents;this._docSub = this.documentService.currentDocument.subscribe(doc => this.currentDoc = doc.id);}ngOnDestroy() {this._docSub.unsubscribe();}loadDoc(id: string) {this.documentService.getDocument(id);}newDoc() {this.documentService.newDocument();}
}

让我们从属性开始。documents 将是所有可用文档的流。currentDocId 是当前选定文档的 id。文档列表需要知道我们在哪个文档上,以便我们可以在侧边栏中突出显示该文档 id。_docSub 是给出当前或选定文档的 Subscription 的引用。我们需要这个引用,这样我们就可以在 ngOnDestroy 生命周期方法中取消订阅。

您会注意到 loadDoc()newDoc() 方法没有返回或分配任何内容。请记住,这些方法触发了 socket 服务器的事件,然后 socket 服务器会向我们的 Observables 发出事件。从上面的 Observable 模式中实现了获取现有文档或添加新文档的返回值。

文档组件

这将是文档编辑界面。

打开 document.component.html

nano src/app/components/document/document.component.html

并用以下内容替换其中的内容:


<textarea[(ngModel)]='document.doc'(keyup)='editDoc()'placeholder='开始输入...'
></textarea>

打开 document.component.scss

nano src/app/components/document/document.component.scss

并在默认的 HTML textarea 上更改一些样式:


textarea {border: none;font-size: 18pt;height: 100%;padding: 20px  0  20px  15px;position: fixed;resize: none;right: 0;top: 0;width: calc(100% - 235px);
}

打开 document.component.ts

src/app/components/document/document.component.ts

并在类定义中添加以下内容:


import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { startWith } from 'rxjs/operators';import { Document } from 'src/app/models/document.model';
import { DocumentService } from 'src/app/services/document.service';@Component({selector: 'app-document',templateUrl: './document.component.html',styleUrls: ['./document.component.scss']
})
export class DocumentComponent implements OnInit, OnDestroy {document: Document;private _docSub: Subscription;constructor(private documentService: DocumentService) { }ngOnInit() {this._docSub = this.documentService.currentDocument.pipe(startWith({ id: '', doc: '选择一个现有文档或创建一个新文档以开始' })).subscribe(document => this.document = document);}ngOnDestroy() {this._docSub.unsubscribe();}editDoc() {this.documentService.editDocument(this.document);}
}

与上面的 DocumentListComponent 中使用的模式类似,我们将订阅当前文档的更改,并在我们更改当前文档时向套接字服务器发送事件。这意味着如果任何其他客户端正在编辑我们正在编辑的相同文档,我们将看到所有更改,反之亦然。我们使用 RxJS 的 startWith 操作符在用户首次打开应用时提供一条小消息。

AppComponent

打开 app.component.html

nano src/app.component.html

并通过以下内容替换其中的内容来组合两个自定义组件:


<app-document-list></app-document-list>
<app-document></app-document>

步骤 3 —— 查看应用程序的运行情况

在我们的套接字服务器仍在一个终端窗口中运行的情况下,让我们打开一个新的终端窗口并启动我们的 Angular 应用程序:

ng serve

在单独的浏览器标签中打开多个 http://localhost:4200 实例并查看其运行情况。

!使用 Angular 和 Socket.IO 构建的实时文档协作应用程序

现在,您可以创建新文档并在两个浏览器窗口中看到它们更新。您可以在一个浏览器窗口中进行更改,并在另一个浏览器窗口中看到更改的反映。

结论

在本教程中,您已经完成了对使用 WebSocket 的初步探索。您使用它构建了一个实时文档协作应用程序。它支持多个浏览器会话连接到服务器,并更新和修改多个文档。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/710727.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网络编程作业day2

1.将TPC和UDP通信模型各敲两遍 &#xff08;1&#xff09;TPC通信模型&#xff1a; 服务器代码&#xff1a; #include <myhead.h> #define SERVER_IP "192.168.125.136" #define SERVER_PORT 1314 int main(int argc, const char *argv[]) {//1、创建用于监…

CLion 2023:专注于C和C++编程的智能IDE mac/win版

JetBrains CLion 2023是一款专为C和C开发者设计的集成开发环境&#xff08;IDE&#xff09;&#xff0c;它集成了许多先进的功能&#xff0c;旨在提高开发效率和生产力。 CLion 2023软件获取 CLion 2023的智能代码编辑器提供了丰富的代码补全和提示功能&#xff0c;使您能够更…

统计业务流量的毫秒级峰值 - 华为机试真题题解

考试平台&#xff1a; 时习知 分值&#xff1a; 200分&#xff08;第二题&#xff09; 考试时间&#xff1a; 两小时&#xff08;共3题&#xff09; 题目描述 业务模块往外发送报文时&#xff0c;有时会出现网卡队列满而丢包问题&#xff0c;但从常规的秒级流量统计结果看&…

Mybatis-Plus介绍

目录 一、Mybatis-Plus简介 1.1、介绍 1.2、特性 1.3、架构 1.4、Mybatis-Plus与Mybatis的区别 二、快速入门 2.1、首先创建数据库mybatis-plus 2.2、创建user表 2.3、插入数据 2.4、创建Spring-Boot项目 2.5、添加依赖 2.6、连接数据库 一、Mybatis-Plus简介 1.1、…

代码随想录第46天|139.单词拆分 多重背包理论基础 背包总结

文章目录 单词拆分思路&#xff1a;代码 多重背包≈0-1背包题目代码 背包总结 单词拆分 3 思路&#xff1a; 代码 class Solution {public boolean wordBreak(String s, List<String> wordDict) {HashSet<String> set new HashSet<>(wordDict);boolean[]…

15个非常实用的JavaScript技巧,提高你的开发效率

本文我们将探讨15个实用的JavaScript技巧&#xff0c;希望它们可以帮你提高开发效率&#xff0c;有用的话点赞收藏~。 1. 反转字符串 你有时候可能需要将字符串颠倒过来。在JavaScript中&#xff0c;有一个巧妙的一行代码可以实现这个目标&#xff1a; const reversedString…

sheng的学习笔记-卷积神经网络经典架构-LeNet-5、AlexNet、VGGNet-16

目录&#xff1a;目录 看本文章之前&#xff0c;需要学习卷积神经网络基础&#xff0c;可参考 sheng的学习笔记-卷积神经网络-CSDN博客 目录 LeNet-5 架构图 层级解析 1、输入层&#xff08;Input layer&#xff09; 2、卷积层C1&#xff08;Convolutional layer C1&…

Dockerfile(5) - CMD 指令详解

CMD 指定容器默认执行的命令 # exec 形式&#xff0c;推荐 CMD ["executable","param1","param2"] CMD ["可执行命令", "参数1", "参数2"...]# 作为ENTRYPOINT的默认参数 CMD ["param1","param…

VUE3自定义文章排行榜的简单界面

文章目录 一、代码展示二、代码解读三、结果展示 一、代码展示 <template><div class"article-ranking"><div class"header"><h2 class"title">{{ title }}</h2></div><div class"ranking-list&qu…

根据A(String)字段去重,并且选择B(Ingter)字段最大的值

数据格式&#xff1a; [SkillDTO(Job电线工, rankGrade高级工, r4), SkillDTO(Job监察员, rankGrade技师, r5), SkillDTO(Job监察员, rankGrade高级工, r4), SkillDTO(skillJob监察员, rankGrade中级工, r3)] List<SkillDTO> resultList SkillDTOList.stream().coll…

电子技术——PN结电流关系方程

电子技术——PN结电流关系方程 平衡状态下的PN结 平衡状态下的PN结界面总共有两种电流&#xff0c;一种为 扩散电流 另一种为 漂移电流 。两种电流形成的平衡区域称为 耗散区 。 在平衡状态扩散电流等于漂移电流&#xff0c;此时静电流为0&#xff0c;PN结外部没有电流&…

Java SPI:Service Provider Interface

SPI机制简介 SPI&#xff08;Service Provider Interface&#xff09;&#xff0c;是从JDK6开始引入的&#xff0c;一种基于ClassLoader来发现并加载服务的机制。 一个标准的SPI&#xff0c;由3个组件构成&#xff0c;分别是&#xff1a; Service&#xff1a;是一个公开的接口…

Java ElasticSearch面试题

Java ElasticSearch面试题 前言1、ElasticSearch是什么&#xff1f;2. 说说你们公司ES的集群架构&#xff0c;索引数据大小&#xff0c;分片有多少 &#xff1f;3. ES的倒排索引是什么&#xff1f;4. ES是如何实现 master 选举的?5. 描述一下 ES索引文档的过程&#xff1a;6、…

Centos系统(Linux)挂载硬盘/数据盘详细操作和开机自动挂载的两种方式

前提&#xff1a;已经做好磁盘阵列&#xff0c;将磁盘划分好 磁盘初始化操作步骤&#xff08;如果已经可以正常挂载可跳过)&#xff1a; 使用fdisk -l命令查看多出来的大容量的磁盘名称&#xff08;如果多块磁盘&#xff0c;查看需要挂载的磁盘名称&#xff09;&#xff0c;一…

embedding的原理和结构

embedding(向量化)是一个将数据转化为向量矩阵的过程&#xff0c;作用是&#xff1a;将高维稀疏向量转化为稠密向量&#xff0c;从而方便下游模型处理 简单的概念大家应该都知道了&#xff0c;以LLM为例 输入&#xff1a;文字 模型&#xff1a;embedding 输出&#xff1a;向量…

c++高精度乘法的原理及c++代码讲解

高精度乘法的原理主要是利用数学中乘法的基本原理&#xff0c;将大整数拆分成各个位数的相乘&#xff0c;然后累加得到最终结果。其过程如下&#xff1a; 将两个大整数相乘&#xff0c;从低位开始逐位相乘&#xff0c;得到部分乘积&#xff1b;将每一位的部分乘积相加&#xf…

【Emgu CV教程】7.8、图像锐化(增强)之同态滤波

文章目录 一、同态滤波大体原理二、代码三、效果举例 一、同态滤波大体原理 之前介绍的几个锐化、增强方法&#xff0c;包括更早之前介绍的图像模糊方法&#xff0c;都是基于空间域进行处理&#xff0c;也就是直接对目标点周边像素值进行各种数学运算。而这篇文章提到的同态滤…

学习计算机的好处

之前写了那么多计算机知识&#xff0c;却没有一篇写我学计算机的初衷。 掌握计算机技术不仅可以提高我们的就业能力和竞争力&#xff0c;同时有助于我们更好地认识世界&#xff0c;提高工作效率和解决问题的能力&#xff0c;更好地利用科技创造更美好的未来。 因此&#xff0c…

pyvisa库实现仪器控制

python控制仪器实现自动化常用pyvisa库&#xff0c;基本控制可大致分为创建仪器控制对象、写入控制指令、读取仪表信息和查询仪表状态&#xff0c;下面进行基本的讲解。 pyvisa库创建仪表控制对象 import tkinter.messagebox import pyvisaclass InstrumentControl:inst Non…

喜迎乔迁,开启新章 ▏易我科技新办公区乔迁庆典隆重举行

2024年1月18日&#xff0c;易我科技新办公区乔迁庆典在热烈而喜庆的氛围中隆重举行。新办公区的投入使用&#xff0c;标志着易我科技将以崭新姿态迈向新的发展阶段。 ▲ 易我科技新办公区 随着公司业务的不断发展和壮大&#xff0c;为了更好地适应公司发展的需要&#xff0c;…