如何使用 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.pswp.cn/news/710727.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/710727.shtml
英文地址,請注明出處:http://en.pswp.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;…