构建 Angular CRUD 模块与实现实时通信功能
1. 构建 Angular CRUD 模块
在开发过程中,我们可以构建一个完整的 CRUD(创建、读取、更新、删除)模块,以下为具体步骤。
1.1 准备模块基础设施
在处理响应对象时,我们将其映射为仅发送 JSON 对象,并捕获任何错误以修改响应,这样组件只需处理数据本身。完成这些后,模块基础设施就为子组件做好了准备。
1.2 实现创建子组件
“创建”子组件负责创建新文章,具体操作如下:
- 创建文件 :在 public/app/articles 文件夹内创建名为 create 的新文件夹,在该文件夹中创建 create.component.ts 文件,并粘贴以下代码:
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ArticlesService } from '../articles.service';
@Component({
selector: 'create',
templateUrl: 'app/articles/create/create.template.html'
})
export class CreateComponent {
article: any = {};
errorMessage: string;
constructor(private _router:Router,
private _articlesService: ArticlesService) {}
create() {
this._articlesService
.create(this.article)
.subscribe(createdArticle => this._router.navigate(['/articles',
createdArticle._id]),
error => this.errorMessage = error);
}
}
- 代码解释 :首先从 Angular 库和
ArticlesService导入所需模块,创建一个包含空文章和错误消息对象的组件。组件构造函数注入Router和ArticlesService服务,create()方法使用ArticlesService创建新文章对象,在可观察对象订阅中,使用Router服务导航到查看组件,并携带新创建文章的 ID,若出现错误则设置组件的错误消息属性。 - 创建模板 :在
public/app/articles/create文件夹中创建create.template.html文件,粘贴以下代码:
<h1>New Article</h1>
<form
(ngSubmit)="create()"
novalidate>
<div>
<label for="title">Title</label>
<div>
<input type="text" required [
(ngModel)]="article.title" name="title"
placeholder="Title">
</div>
</div>
<div>
<label for="content">Content</label>
<div>
<textarea type="text" required cols="30" rows="10"
[(ngModel)]="article.content"
name="content" placeholder="Content"></textarea>
</div>
</div>
<div>
<input type="submit">
</div>
<strong id="error">
{{errorMessage}}
</strong>
</form>
- 模板解释 :该模板包含一个简单表单,有两个文本输入字段和一个提交按钮,文本字段使用
ngModel指令将用户输入绑定到组件属性,ngSubmit指令在表单提交时调用组件的create()方法,表单末尾的错误消息在出现错误时显示。
1.3 实现查看子组件
“查看”子组件负责展示单篇文章,具体操作如下:
- 创建文件 :在 public/app/articles 文件夹内创建名为 view 的新文件夹,在该文件夹中创建 view.component.ts 文件,并粘贴以下代码:
import { Component } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { AuthenticationService } from '../../authentication/authentication.service';
import { ArticlesService } from '../articles.service';
@Component({
selector: 'view',
templateUrl: 'app/articles/view/view.template.html',
})
export class ViewComponent {
user: any;
article: any;
paramsObserver: any;
errorMessage: string;
allowEdit: boolean = false;
constructor(private _router:Router,
private _route: ActivatedRoute,
private _authenticationService: AuthenticationService,
private _articlesService: ArticlesService) {}
ngOnInit() {
this.user = this._authenticationService.user
this.paramsObserver = this._route.params.subscribe(params => {
let articleId = params['articleId'];
this._articlesService
.read(articleId)
.subscribe(
article => {
this.article = article;
this.allowEdit = (this.user && this.user._id ===
this.article.creator._id);
},
error => this._router.navigate(['/articles'])
);
});
}
ngOnDestroy() {
this.paramsObserver.unsubscribe();
}
delete() {
this._articlesService.delete(this.article._id).subscribe(deletedArticle =>
this._router.navigate(['/articles']),
error => this.errorMessage = error);
}
}
- 代码解释 :导入所需模块,创建包含文章属性、当前用户属性、参数观察者属性、允许编辑标志和错误消息属性的组件。构造函数注入
Router、RouteParams、ArticlesService和AuthenticationService服务,并设置当前用户属性。ngOnInit方法在组件初始化时读取文章 ID 参数,使用ArticlesService获取现有文章,在可观察对象订阅中设置组件的文章属性并确定当前用户是否可以编辑文章,若出现错误则使用Router服务导航回列表路由。ngOnDestroy方法取消订阅参数观察者,delete()方法使用ArticlesService删除查看的文章并返回文章列表。 - 创建模板 :在
public/app/articles/view文件夹中创建view.template.html文件,粘贴以下代码:
<section
*ngIf="article && article.creator">
<h1>
{{article.title}}
</h1>
<div *ngIf="allowEdit">
<a
[routerLink]="['/articles', article._id, 'edit']"
>edit</a>
<button
(click)="delete()"
>delete</button>
</div>
<small>
<em>Posted on
{{article.created}}
by
{{article.creator.fullName}}
</em>
</small>
<p>
{{article.content}}
</p>
</section>
- 模板解释 :该模板使用双花括号语法展示文章信息,使用
ngIf指令仅向文章创建者展示编辑链接和删除按钮,编辑链接将用户导向编辑子组件,删除按钮调用控制器的delete()方法。
1.4 实现编辑子组件
“编辑”子组件负责编辑现有文章,具体操作如下:
- 创建文件 :在 public/app/articles 文件夹内创建名为 edit 的新文件夹,在该文件夹中创建 edit.component.ts 文件,并粘贴以下代码:
import { Component } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { ArticlesService } from '../articles.service';
@Component({
selector: 'edit',
templateUrl: 'app/articles/edit/edit.template.html'
})
export class EditComponent {
article: any = {};
errorMessage: string;
paramsObserver: any;
constructor(private _router:Router,
private _route: ActivatedRoute,
private _articlesService: ArticlesService) {}
ngOnInit() {
this.paramsObserver = this._route.params.subscribe(params => {
let articleId = params['articleId'];
this._articlesService.read(articleId).subscribe(article => {
this.article = article;
},
error => this._router.navigate(['/articles']));
});
}
ngOnDestroy() {
this.paramsObserver.unsubscribe();
}
update() {
this._articlesService.update(this.article).subscribe(savedArticle =>
this._router.navigate(['/articles', savedArticle._id]),
error => this.errorMessage = error);
}
}
- 代码解释 :导入所需模块,创建包含文章属性和错误消息属性的组件。构造函数注入相关服务,在
ngOnInit方法中读取文章 ID 参数,使用ArticlesService获取现有文章,在可观察对象订阅中设置组件的文章属性,若出现错误则使用Router服务导航回列表路由。ngOnDestroy方法取消订阅参数观察者,update()方法使用ArticlesService更新查看的文章并返回查看路由。 - 创建模板 :在
public/app/articles/edit文件夹中创建edit.template.html文件,粘贴以下代码:
<h1>Edit Article</h1>
<form
(ngSubmit)="update()"
novalidate>
<div>
<label for="title">Title</label>
<div>
<input type="text" required
[(ngModel)]="article.title"
name="title" placeholder="Title">
</div>
</div>
<div>
<label for="content">Content</label>
<div>
<textarea type="text" required cols="30" rows="10"
[(ngModel)]="article.content"
name="content" placeholder="Content"></textarea>
</div>
</div>
<div>
<input type="submit" value="Update">
</div>
<strong>
{{errorMessage}}
</strong>
</form>
- 模板解释 :模板包含一个简单表单,文本字段使用
ngModel指令将用户输入绑定到组件的文章属性,ngSubmit指令在表单提交时调用组件的update()方法,表单末尾的错误消息在编辑出错时显示。
1.5 实现列表子组件
“列表”子组件负责展示文章列表,具体操作如下:
- 创建文件 :在 public/app/articles 文件夹内创建名为 list 的新文件夹,在该文件夹中创建 list.component.ts 文件,并粘贴以下代码:
import { Component } from '@angular/core';
import { ArticlesService } from '../articles.service';
@Component({
selector: 'list',
templateUrl: 'app/articles/list/list.template.html'
})
export class ListComponent{
articles: any;
errorMessage: string;
constructor(private _articlesService: ArticlesService) {}
ngOnInit() {
this._articlesService.list().subscribe(articles => this.articles = articles);
}
}
- 代码解释 :导入所需模块,创建包含文章属性和错误消息属性的组件。构造函数注入
ArticlesService服务,ngOnInit方法使用该服务获取文章列表并设置组件的文章属性。 - 创建模板 :在
public/app/articles/list文件夹中创建list.template.html文件,粘贴以下代码:
<h1>Articles</h1>
<ul>
<li
*ngFor="let article of articles"
>
<a [routerLink]="['/articles', article._id]">
{{article.title}}
</a>
<br>
<small>
{{article.created}}
/
{{article.creator.fullName}}
</small>
<p>{{article.content}}</p>
</li>
</ul>
<div
*ngIf="articles && articles.length === 0"
>
No articles yet, why don't you <a [routerLink]="['/articles/create']">create one</a>?
</div>
- 模板解释 :模板使用
ngFor指令渲染文章列表,每个列表项代表一篇文章,使用routerLink链接到单篇文章视图,使用ngIf指令在没有现有文章时提示用户创建新文章。
1.6 完成实现
为用户提供新 CRUD 模块路由的链接,修改 public/app/home/home.template.html 文件如下:
<div *ngIf="user">
<h1>Hello {{user.firstName}}</h1>
<a href="/api/auth/signout">Signout</a>
<ul>
<li><a [routerLink]="['/articles']">List Articles</a></li>
<li><a [routerLink]="['/articles/create']">Create Article</a></li>
</ul>
</div>
<div *ngIf="!user">
<a [routerLink]="['/authentication/signup']">Signup</a>
<a [routerLink]="['/authentication/signin']">Signin</a>
</div>
此更改仅在用户登录时显示新文章组件路由的链接,未登录时隐藏。完成上述操作后,可使用命令行工具导航到 MEAN 应用程序的根文件夹,运行 npm start 启动应用程序,在浏览器中访问 http://localhost:3000 进行测试。
2. 引入 WebSockets 与 Socket.io
现代 Web 应用程序越来越需要服务器和浏览器之间的实时通信,以下为相关介绍。
2.1 实时通信需求
像 Facebook、Twitter 和 Gmail 等现代 Web 应用程序都具备实时功能,能持续向用户展示最新信息。与传统应用程序不同,实时应用程序中服务器和浏览器的常见角色可能会反转,服务器无需等待浏览器请求,只要有新数据就会发送给浏览器。
2.2 实现 Comet 功能的方法
- XMLHttpRequest(XHR)轮询 :浏览器定期向服务器发送请求,服务器只有在有新数据时才返回响应,否则返回空响应。此方法会产生大量无意义请求,且更新时间依赖请求周期,可能导致客户端状态更新延迟。
- XHR 长轮询 :浏览器向服务器发送 XHR 请求,服务器有新数据时才返回响应,浏览器收到响应后再发起新的长轮询请求。这种方式能更好地管理请求,服务器可立即更新浏览器信息,已成为实时应用程序的标准方法,有多种实现方式。
2.3 WebSockets 协议
随着现代浏览器的发展和 HTML5 规范的普及,出现了全双工 WebSockets 协议用于实现实时通信。在支持该协议的浏览器中,服务器和浏览器的初始连接通过 HTTP 进行,即 HTTP 握手,之后在 TCP 套接字上打开一个持续的通信通道,实现双向通信,有助于降低服务器负载、减少消息延迟并统一 PUSH 通信。
2.4 WebSockets 的问题与解决方案
WebSockets 存在两个主要问题:一是浏览器兼容性问题,由于规范较新,旧浏览器不支持;二是 HTTP 代理、防火墙和托管提供商的问题,它们可能不支持该协议并阻止套接字通信。为解决这些问题,开发了 Socket.io 库,它能根据可用资源在不同协议间切换,优化可用性,供 Node.js 开发者免费使用。
以下是实现实时通信方式的对比表格:
| 实现方式 | 优点 | 缺点 |
| ---- | ---- | ---- |
| XHR 轮询 | 实现简单 | 产生大量无意义请求,更新有延迟 |
| XHR 长轮询 | 更好管理请求,更新及时 | 基于 HTTP 和 XHR 协议的 hack 方式 |
| WebSockets | 双向通信,降低负载和延迟 | 浏览器兼容性和中间件支持问题 |
| Socket.io | 优化可用性,切换协议 | - |
下面是实时通信方式的流程图:
graph LR
A[实时通信需求] --> B[XHR轮询]
A --> C[XHR长轮询]
A --> D[WebSockets]
D --> E[兼容性问题]
D --> F[中间件支持问题]
E --> G[Socket.io]
F --> G[Socket.io]
3. 使用 Socket.io 实现实时功能
3.1 设置 Socket.io 模块
Socket.io 是一个强大的库,它允许 Node.js 开发者在现代浏览器中使用 WebSockets 实现实时通信,同时在旧浏览器中提供备用协议。要使用 Socket.io,首先需要在项目中安装它。可以通过 npm 进行安装,在项目根目录下打开命令行工具,运行以下命令:
npm install socket.io
安装完成后,就可以在项目中引入并使用 Socket.io 了。
3.2 配置 Express 应用程序
为了让 Express 应用程序支持 Socket.io,需要对 Express 应用进行一些配置。以下是一个简单的示例代码:
const express = require('express');
const app = express();
const http = require('http').Server(app);
const io = require('socket.io')(http);
// 其他 Express 中间件和路由配置
// ...
const port = 3000;
http.listen(port, () => {
console.log(`Server running on port ${port}`);
});
在这个示例中,我们首先创建了一个 Express 应用,然后使用 http 模块创建了一个 HTTP 服务器,并将 Express 应用作为参数传递给它。接着,我们使用 socket.io 模块初始化了一个 Socket.io 实例,并将 HTTP 服务器作为参数传递给它。最后,我们启动了 HTTP 服务器。
3.3 设置 Socket.io/Passport 会话
如果你的应用程序使用了 Passport 进行身份验证,那么需要确保 Socket.io 能够访问 Passport 的会话信息。可以通过以下步骤实现:
1. 安装必要的中间件 :安装 express-session 和 connect-mongo 模块,用于处理会话和将会话信息存储在 MongoDB 中。
npm install express-session connect-mongo
- 配置会话中间件 :在 Express 应用中配置会话中间件,并将其应用到 Socket.io 中。
const session = require('express-session');
const MongoStore = require('connect-mongo')(session);
const mongoose = require('mongoose');
// 连接 MongoDB
mongoose.connect('mongodb://localhost:27017/your-database', { useNewUrlParser: true, useUnifiedTopology: true });
const sessionMiddleware = session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
store: new MongoStore({ mongooseConnection: mongoose.connection })
});
app.use(sessionMiddleware);
// 将会话中间件应用到 Socket.io
io.use((socket, next) => {
sessionMiddleware(socket.request, socket.request.res || {}, next);
});
通过以上配置,Socket.io 就可以访问 Passport 的会话信息,从而实现基于身份验证的实时通信。
3.4 连接 Socket.io 路由
在 Socket.io 中,可以使用事件来处理客户端和服务器之间的通信。以下是一个简单的示例,展示了如何在服务器端监听客户端连接事件,并向客户端发送消息:
io.on('connection', (socket) => {
console.log('A user connected');
// 向客户端发送消息
socket.emit('message', 'Welcome to the chat room!');
// 监听客户端发送的消息
socket.on('chat message', (msg) => {
console.log('Received message:', msg);
// 向所有客户端广播消息
io.emit('chat message', msg);
});
// 监听客户端断开连接事件
socket.on('disconnect', () => {
console.log('A user disconnected');
});
});
在这个示例中,我们使用 io.on('connection') 监听客户端连接事件,当有客户端连接时,会在服务器端打印一条日志,并向客户端发送一条欢迎消息。然后,我们使用 socket.on('chat message') 监听客户端发送的消息,并使用 io.emit('chat message') 向所有客户端广播该消息。最后,我们使用 socket.on('disconnect') 监听客户端断开连接事件。
3.5 使用 Socket.io 客户端对象
在客户端,需要引入 Socket.io 客户端库,并连接到服务器。以下是一个简单的示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Socket.io Chat</title>
</head>
<body>
<input type="text" id="msg" placeholder="Type a message">
<button id="send">Send</button>
<ul id="messages"></ul>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
// 监听服务器发送的消息
socket.on('message', (msg) => {
const li = document.createElement('li');
li.textContent = msg;
document.getElementById('messages').appendChild(li);
});
// 监听服务器广播的消息
socket.on('chat message', (msg) => {
const li = document.createElement('li');
li.textContent = msg;
document.getElementById('messages').appendChild(li);
});
// 发送消息
document.getElementById('send').addEventListener('click', () => {
const msg = document.getElementById('msg').value;
socket.emit('chat message', msg);
document.getElementById('msg').value = '';
});
</script>
</body>
</html>
在这个示例中,我们首先引入了 Socket.io 客户端库,并使用 io() 方法连接到服务器。然后,我们使用 socket.on('message') 监听服务器发送的消息,并将其显示在页面上。接着,我们使用 socket.on('chat message') 监听服务器广播的消息,并将其显示在页面上。最后,我们使用 socket.emit('chat message') 向服务器发送消息。
3.6 构建一个简单的聊天室
结合上述步骤,我们可以构建一个简单的聊天室。以下是一个完整的示例:
- 服务器端代码 :
const express = require('express');
const app = express();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const session = require('express-session');
const MongoStore = require('connect-mongo')(session);
const mongoose = require('mongoose');
// 连接 MongoDB
mongoose.connect('mongodb://localhost:27017/your-database', { useNewUrlParser: true, useUnifiedTopology: true });
const sessionMiddleware = session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
store: new MongoStore({ mongooseConnection: mongoose.connection })
});
app.use(sessionMiddleware);
// 将会话中间件应用到 Socket.io
io.use((socket, next) => {
sessionMiddleware(socket.request, socket.request.res || {}, next);
});
// 静态文件服务
app.use(express.static(__dirname + '/public'));
io.on('connection', (socket) => {
console.log('A user connected');
// 向客户端发送消息
socket.emit('message', 'Welcome to the chat room!');
// 监听客户端发送的消息
socket.on('chat message', (msg) => {
console.log('Received message:', msg);
// 向所有客户端广播消息
io.emit('chat message', msg);
});
// 监听客户端断开连接事件
socket.on('disconnect', () => {
console.log('A user disconnected');
});
});
const port = 3000;
http.listen(port, () => {
console.log(`Server running on port ${port}`);
});
- 客户端代码 :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Socket.io Chat</title>
</head>
<body>
<input type="text" id="msg" placeholder="Type a message">
<button id="send">Send</button>
<ul id="messages"></ul>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
// 监听服务器发送的消息
socket.on('message', (msg) => {
const li = document.createElement('li');
li.textContent = msg;
document.getElementById('messages').appendChild(li);
});
// 监听服务器广播的消息
socket.on('chat message', (msg) => {
const li = document.createElement('li');
li.textContent = msg;
document.getElementById('messages').appendChild(li);
});
// 发送消息
document.getElementById('send').addEventListener('click', () => {
const msg = document.getElementById('msg').value;
socket.emit('chat message', msg);
document.getElementById('msg').value = '';
});
</script>
</body>
</html>
将客户端代码保存为 public/index.html 文件,并将其放在项目的根目录下。运行服务器端代码,然后在浏览器中访问 http://localhost:3000 ,就可以看到一个简单的聊天室界面。在输入框中输入消息,点击发送按钮,消息就会被发送到服务器,并广播给所有连接的客户端。
总结
通过以上步骤,我们首先构建了一个完整的 Angular CRUD 模块,包括创建、查看、编辑和列表子组件,实现了对文章的基本操作。然后,我们引入了 WebSockets 和 Socket.io 库,了解了实现实时通信的不同方法,以及 WebSockets 协议的优缺点。最后,我们使用 Socket.io 构建了一个简单的聊天室,实现了服务器和客户端之间的实时通信。这些技术可以帮助我们开发出更加高效、实时的 Web 应用程序。
以下是一个总结表格,对比了不同实时通信方法的特点:
| 方法 | 优点 | 缺点 | 适用场景 |
| ---- | ---- | ---- | ---- |
| XHR 轮询 | 实现简单 | 产生大量无意义请求,更新有延迟 | 对实时性要求不高的场景 |
| XHR 长轮询 | 更好管理请求,更新及时 | 基于 HTTP 和 XHR 协议的 hack 方式 | 对实时性有一定要求的场景 |
| WebSockets | 双向通信,降低负载和延迟 | 浏览器兼容性和中间件支持问题 | 对实时性要求高,且客户端浏览器较新的场景 |
| Socket.io | 优化可用性,切换协议 | - | 跨浏览器、跨平台的实时通信场景 |
下面是一个流程图,展示了使用 Socket.io 实现实时通信的基本流程:
graph LR
A[客户端] --> B[连接到服务器]
B --> C[服务器监听连接事件]
C --> D[服务器发送欢迎消息]
A --> E[客户端发送消息]
E --> F[服务器监听消息事件]
F --> G[服务器广播消息]
G --> H[客户端接收消息]
A --> I[客户端断开连接]
I --> J[服务器监听断开事件]
通过学习这些内容,你可以更好地掌握实时通信技术,并将其应用到实际项目中。希望这篇博客对你有所帮助!
超级会员免费看
2712

被折叠的 条评论
为什么被折叠?



