Bootstrap5 & Socket.io

基于Bootstrap5构建页面,使用Socket.io处理后台信息
学习链接
03

实现细节

服务端

npm init - 得到package.json
修改package.json
"main": "index.js", -> "main": "server.js"

npm i express socket.io - 安装包,主角登场

npm i nodemon –save-dev - 测试环境安装

  • 安装 - 可服务热加载 - 不需要手动重启服务,用nodemon

修改scripts - 执行命令

1
2
3
"scripts": {
"dev":"nodemon server.js"
},

创建server.js
把所有前端文件,放入public文件!

可以开始后端代码了!

1
2
3
4
5
6
7
8
9
10
11
const express = require('express'); // 引入资源
const socketio = require('socket.io');

const app = express(); // 创建实例

const PORT = process.env.PORT || 9527; // 定义端口

app.listen(PORT, () => { // 测试链接
console.log('This server is running on port' + PORT)
})

输入npm run dev

可以看到
01

引入静态资源 - 可以看到页面了!

1
2
3
const path = require('path'); 

app.use(express.static(path.join(__dirname, 'public')));

02

引入http服务,创建http对象,用socket来接收,用server来监听!

所以app - express做了什么???use。。。引入静态资源。

1
2
3
4
5
6
7
8
9
10
const http = require('http'); // 挂载socketio

const app = express();
const server = http.createServer(app); // 手动创建http对象

const io = socketio(server); // 把http服务挂载到io

server.listen(PORT, () => {
console.log('This server is running on port ' + PORT)
})

可以访问以下路径了,socketio把客户端文件挂载到服务器上了!
http://localhost:9527/socket.io/socket.io.js

监听/发送

1
2
3
4
5
// 监听,有没有连进来。
io.on('connection', (socket) => {
io.emit('sysMessage', '欢迎加入聊天室') // 发送
})

在静态页内引入js - chat.html

1
2
3
4
5
// 来自socket.io
<script src="/socket.io/socket.io.js" type="text/javascript" charset="utf-8"></script>

// public下新建一个
<script src="./main.js" type="text/javascript" charset="utf-8"></script>

在main.js中写入
const socket = io();

就可以连接上socket服务了!
访问http://localhost:9527/chat.html
查看network,有ws标签,可以看到socket.io.js已经有了!

main.js继续写, 刷新chat.html页面,就可以看到数据了! - ‘欢迎加入聊天室’

1
2
3
4
5
socket.on('sysMessage', (message) => {
console.log(message)
})

socket.emit('chatMessage', 'Hi'); // 发送给服务器!

server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 监听,有没有连进来。
io.on('connection', (socket) => {
socket.emit('sysMessage', '欢迎加入聊天室');

// 这里打印在ide里面,应该在浏览器里面
// 把接收到的信息,再发送出去!
// socket.on('chatMessage', message => console.log(message)); // 接收 main.js发送的数据

socket.on('chatMessage', message => {
//socket.emit('chatMessage', message) // 这里要改成 io来发送 - io全局对象!!

// 开两个chat.html,一个刷新,另一个就能显示出来了!!!
io.emit('chatMessage', message) // 这里要改成 io来发送 - io全局对象!!

});
})

main.js里面监听 - 发送的信息..

1
2
3
4
5
6
7
8
9
10
11
const socket = io();

socket.on('sysMessage', (message) => {
console.log(message)
})

socket.emit('chatMessage', 'Hi');

socket.on('chatMessage', message => { // 这里是由服务器返回的.
console.log(message)
});

这里添加html内容.
在chat.html里面,用form套住输入信息.

id=’chat-form’ // 找到位置 - 提交操作
id=’msg’ // 获取输入信息 - 发送出去.

1
2
3
4
5
6
<form action="#" id='chat-form'>
<div class="input-group mb-3">
<input id='msg' type="text" class="form-control" placeholder="请输入消息">
<button class="btn btn-success"><i class="bi bi-send"></i>&nbsp;发送</button>
</div>
</form>

main.js - 发送消息,用的 chatMessage, 事件.

1
2
3
4
5
document.getElementById('chat-form').addEventListener('submit', e => {
e.preventDefault(); //组织默认提交事件.

socket.emit('chatMessage', document.getElementById('msg').value);
})

已经可以发送消息了.
现在,把信息显示在UI框架上面!
<div class="col-sm-12 col-md-8 message-container"> 在容器上添加类,写入信息 - 做加法!

main.js - 已经可以发送到UI框架了!,,, 渐渐忘了 on监听的是什么了…..

1
2
3
4
5
6
7
8
9
10
socket.on('chatMessage', message => {
// console.log(message)
document.querySelector('.message-container').innerHTML +=
`<div class="d-flex justify-content-end my-3">
<div class="card w-30 bg-success text-light">
<div class="card-body">${message}</div>
</div>
</div>`
});

如何区分发送的是谁? - 已经可以发送人名了
用对象的形式发送,
{name: ‘’, content: ‘’};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const name = 'John'

document.getElementById('chat-form').addEventListener('submit', e => {
e.preventDefault(); //组织默认提交事件.

// socket.emit('chatMessage', document.getElementById('msg').value);
socket.emit('chatMessage', {
name,
content: document.getElementById('msg').value
});
})


document.querySelector('.message-container').innerHTML +=
`<div class="d-flex justify-content-end my-3">
<div class="card w-30 bg-success text-light">
<div class="card-body">
<div class='card-title'>${message.name}</div>
<div class='card-text'>${message.content}</div>
</div>
</div>
</div>`

如何动态获取姓名?
通过url

这里引入qs 插件 - 写入chat.html里面
<script src="https://cdnjs.cloudflare.com/ajax/libs/qs/6.10.3/qs.min.js"></script>

main.js里面 - 可以获取到url的信息了!

1
2
3
4
5
// 如果没有{ignoreQueryPrefix: true},会多一个问号
// url - ?name=John
const params = Qs.parse(location.search, {ignoreQueryPrefix: true})

console.log(params)//打印{name:'John'}

如何知道是不是自己发送的?
在发送的时候判断!
判断接收到的和url是不是一样 - 我发送出去的发送给自己 - 接收。

1
2
3
4
5
6
7
8
9
10
11
const isMine = message.name === name;

document.querySelector('.message-container').innerHTML +=
`<div class="d-flex ${isMine ? 'justify-content-end' : 'justify-content-start'} my-3">
<div class="card w-30 ${isMine ? 'bg-success text-light' : 'text-dark'} ">
<div class="card-body">
<div class='card-title'>${isMine ? '我' : message.name}</div>
<div class='card-text'>${message.content}</div>
</div>
</div>
</div>`

如何处理用户列表?

定义一个user.js, 在server.js中导入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const users = [];

function userJoin(id, name, room) { // 加入用户列表
const user = {id, name, room};
users.push(user);
return user;
}
function getRoomUsers(room) { // 返回在同一个房间的用户。
return users.filter(user => user.room === room);
}
module.exports = {
userJoin,
getRoomUsers
}

main.js

1
2
3
4
5
6
7
8
9
const name = params.name
const room = params.room

socket.emit('joinRoom', {name, room}) // 发送

socket.on('roomUsers', userList => { // 接收当前房间的用户。
console.log(userList)
})

server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const {getRoomUsers, userJoin} = require('./user')

// 监听,有没有连进来。
io.on('connection', (socket) => {

socket.on('joinRoom', ({name, room}) => {
// 加入房间???
socket.join(room);

userJoin(socket.id, name, room); // 把user加入users。。加入用户列表

// 全局变量,发送给当前room
io.to(room).emit('roomUsers', getRoomUsers(room)) // 发送用户列表给客户端

socket.to(room).emit('sysMessage', '欢迎加入聊天室');

socket.on('chatMessage', message => {
// 这个全局io 很重要...
io.to(root).emit('chatMessage', message);
}
);
})

})

可以获取用户列表了,问题是刷新后,用户会再次加入 - 重复
因为刷新后,断开连接,又连接一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
user.js

//删除用户
function userLeave(id) {
const index = users.findIndex(user => user.id === id);

if(index !== -1) {
return users.splice(index, 1)[0];
}
}
module.exports = {
userJoin,
getRoomUsers,
userLeave
}

-------------

server.js

// 监听系统事件。
socket.on('disconnect', () => {

userLeave(socket.id);

// 重新发送一份用户列表
io.to(room).emit('roomUsers', getRoomUsers(room)) // // 发送用户列表给客户端
})

获取了用户列表 - 写入页面!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
socket.on('roomUsers', userList => {
// console.log(userList)
const userListEl = document.querySelector('.user-list'); // ul的字段,写li进去

let listHtml = '';
userList.map(user => {
listHtml += `<li class='list-group-item'>${user.name}</li>'`
})

// 加入之前先清空
while(userListEl.childNodes.length > 3) {
userListEl.removeChild(userListEl.lastChild);
}

userListEl.innerHTML += listHtml;
})

填写房间名称 - 可以用列表

1
2
3
4
5
6
const room = params.room
// room名称
document.querySelector('.room-name').innerHTML = room;

如果换个room,bob也会走。

细节优化!
输入信息,不可输入空信息,发送后,信息框为空

1
2
3
4
5
6
7
8
9
10
11
12
13
// socket.emit('chatMessage', document.getElementById('msg').value);
const msgEl = document.getElementById('msg')

if(msgEl.value === '') {
alert('请输入信息');
return;
}
socket.emit('chatMessage', {
name,
content: msgEl.value
});
msgEl.value = ''; // 发送完,消息设置为空

输入信息,滚动条应该在最下面!

1
2
3
4
5
6
7
8
9
10
11
12
const scroll = function() {
const rightPanel = document.querySelector('.right-panel'); // 最外层
const messageContainer = document.querySelector('.message-container'); // 信息区域

//scrollTop() 方法设置或返回被选元素的垂直滚动条位置。
// scrollHeight - 整体的高度!

rightPanel.scrollTop = messageContainer.scrollHeight; // 到底了

}

在发送信息的时候,调用scroll

最后 - index.html
传递参数 - name / room

加个form, action - 指定跳转,method - 写入url!
<form action="chat.html" method="get">

给input指定name - name
给select指定name - room

完结!!!

还有任务:
进入页面 - 显示欢迎 **
可以在聊天室切换房间! ** 如何切换房间???
dom 元素什么时候获取??
server.js - 服务端 - 负责转发通信
main.js - 客户端 - 负责处理页面???

总结

Bootstrap构建页面,慢慢体会到好处了!

express - 开启服务,连接静态资源
http - 结合socket.io
socket.io - 转发通信
path - 处理本地资源

nodemon - 热加载 - 开发模式

socket.io,负责转发通信。有全局通信和局部通信之分。
主要通过on 和emit来触发,用回调函数传递信息。
步骤:

  1. 构建页面 - Bootstrap
  2. 建立连接 - express http + socket.io path
  3. 转发信息 - on emit
  4. 用js写入dom - innerHTML + 字符串拼接
  5. 创建user.js 来处理用户信息的资源获取与修改。

补充资料

构建即时聊天计数

  • 长连接 - 占用资源,一条通信线路一直开着
  • 轮询 - 请求量大,占用资源
  • websocket - 客户端可向服务器发起请求,消耗资源小

network中查看socket 有id - 获取它 - 唯一性。