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

简介: 如何使用 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 的初步探索。您使用它构建了一个实时文档协作应用程序。它支持多个浏览器会话连接到服务器,并更新和修改多个文档。


目录
相关文章
|
3月前
|
JavaScript 前端开发 API
Vue.js与Angular的优劣分析
Vue.js和Angular都是非常流行的JavaScript框架,它们在构建现代Web应用程序方面各有优劣
122 64
|
2月前
|
监控 算法 JavaScript
基于 Node.js Socket 算法搭建局域网屏幕监控系统
在数字化办公环境中,局域网屏幕监控系统至关重要。基于Node.js的Socket算法实现高效、稳定的实时屏幕数据传输,助力企业保障信息安全、监督工作状态和远程技术支持。通过Socket建立监控端与被监控端的数据桥梁,确保实时画面呈现。实际部署需合理分配带宽并加密传输,确保信息安全。企业在使用时应权衡利弊,遵循法规,保障员工权益。
51 7
|
3月前
|
前端开发 JavaScript Java
如何使用 Spring Boot 和 Angular 开发全栈应用程序:全面指南
如何使用 Spring Boot 和 Angular 开发全栈应用程序:全面指南
74 1
|
3月前
|
JavaScript 前端开发 开发者
前端框架对比:Vue.js与Angular的优劣分析与选择建议
【10月更文挑战第27天】在前端开发领域,Vue.js和Angular是两个备受瞩目的框架。本文对比了两者的优劣,Vue.js以轻量级和易上手著称,适合快速开发小型到中型项目;Angular则由Google支持,功能全面,适合大型企业级应用。选择时需考虑项目需求、团队熟悉度和长期维护等因素。
98 1
|
3月前
|
JavaScript 前端开发 API
前端框架对比:Vue.js与Angular的优劣分析与选择建议
【10月更文挑战第26天】前端技术的飞速发展让开发者在构建用户界面时有了更多选择。本文对比了Vue.js和Angular两大框架,介绍了它们的特点和优劣,并给出了在实际项目中如何选择的建议。Vue.js轻量级、易上手,适合小型项目;Angular结构化、功能强大,适合大型项目。
116 1
|
5月前
|
缓存 JavaScript 中间件
优化Express.js应用程序性能:缓存策略、请求压缩和路由匹配
在开发Express.js应用时,采用合理的缓存策略、请求压缩及优化路由匹配可大幅提升性能。本文介绍如何利用`express.static`实现缓存、`compression`中间件压缩响应数据,并通过精确匹配、模块化路由及参数化路由提高路由处理效率,从而打造高效应用。
254 15
|
4月前
|
开发框架 前端开发 JavaScript
React、Vue.js 和 Angular主流前端框架和选择指南
在当今的前端开发领域,选择合适的框架对于项目的成功至关重要。本文将介绍几个主流的前端框架——React、Vue.js 和 Angular,探讨它们各自的特点、开发场景、优缺点,并提供选择框架的建议。
107 6
|
5月前
|
前端开发 JavaScript API
React、Vue.js 和 Angular前端三大框架对比与选择
前端框架是用于构建用户界面的工具和库,它提供组件化结构、数据绑定、路由管理和状态管理等功能,帮助开发者高效地创建和维护 web 应用的前端部分。常见的前端框架如 React、Vue.js 和 Angular,能够提高开发效率并促进团队协作。
243 4
|
5月前
|
JSON 前端开发 JavaScript
socket.io即时通信前端配合Node案例
本文介绍了如何使用socket.io库在Node.js环境下实现一个简单的即时通信前端配合案例,包括了服务端和客户端的代码实现,以及如何通过socket.io进行事件的发送和监听来实现实时通信。
81 2
|
5月前
|
前端开发 JavaScript 开发者
Express.js与前端框架的集成:React、Vue和Angular的示例与技巧
本文介绍了如何将简洁灵活的Node.js后端框架Express.js与三大流行前端框架——React、Vue及Angular进行集成,以提升开发效率与代码可维护性。文中提供了详细的示例代码和实用技巧,展示了如何利用Express.js处理路由和静态文件服务,同时在React、Vue和Angular中构建用户界面,帮助开发者快速掌握前后端分离的开发方法,实现高效、灵活的Web应用构建。
115 3

热门文章

最新文章

  • 1
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    23
  • 2
    Node.js 中实现多任务下载的并发控制策略
    32
  • 3
    【2025优雅草开源计划进行中01】-针对web前端开发初学者使用-优雅草科技官网-纯静态页面html+css+JavaScript可直接下载使用-开源-首页为优雅草吴银满工程师原创-优雅草卓伊凡发布
    25
  • 4
    【JavaScript】深入理解 let、var 和 const
    48
  • 5
    【04】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架二次开发准备工作-以及建立初步后端目录菜单列-优雅草卓伊凡商业项目实战
    44
  • 6
    【03】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架搭建-服务端-后台管理-整体搭建-优雅草卓伊凡商业项目实战
    53
  • 7
    【02】Java+若依+vue.js技术栈实现钱包积分管理系统项目-商业级电玩城积分系统商业项目实战-ui设计图figmaUI设计准备-figma汉化插件-mysql数据库设计-优雅草卓伊凡商业项目实战
    55
  • 8
    如何通过pm2以cluster模式多进程部署next.js(包括docker下的部署)
    71
  • 9
    【01】Java+若依+vue.js技术栈实现钱包积分管理系统项目-商业级电玩城积分系统商业项目实战-需求改为思维导图-设计数据库-确定基础架构和设计-优雅草卓伊凡商业项目实战
    55
  • 10
    JavaWeb JavaScript ③ JS的流程控制和函数
    62