socket.io让每一个人均可以开发属于本身的即时通信

上一篇文章《WebSocket是时候展示你优秀的一面了》实际上是一个未完待续的读物html

正因如此,答应了你们的东西仍是要兑现的,接下来的这篇文章里,就让咱们一块儿来利用可爱的socket.io实现个聊天室的功能吧前端

友情提示: 聊天功能开发若是是第一次写的话,确实会须要一段时间去咀嚼和消化,不过在你完整的敲过两三遍后,你就会慢慢的理解和运用了,加油,Fighting!!!git

这里放上该项目的地址,须要对照学习的,尽请拿走!github

聊天室的开发过程

其实这个过程从用户的角度来讲,其实无非就是链接上了,发送消息呗。web

然而实际上,从用户的观点看东西,也确实是这个样子的,那就不绕圈子了,直接进入主题数据库

创建链接

固然,没错,这绝对是全部奇妙玄学中的第一步,不创建链接,那还聊个球呢express

说到这里,忽然想到应该先把html的结构给你们,否则还怎么循序渐进的一块儿敲呢bootstrap

先贴一张目录的结构,下面的文件都对应目录便可 数组

页面结构

布局样式方面是直接使用bootstrap来搞的,方便快捷,主要就是让你们看看样子,这里就不太浪费时间了, index.html文件地址bash

没有任何功能,仅仅是页面布局,你们copy一下,看看样子便可了

下面咱们来分别试着写下客户端和服务端的两套建立链接的代码,一块儿敲敲敲吧

这才是重要的东西,开撸

客户端创建链接

// index.js文件
let socket = io();
// 监听与服务端的链接
socket.on('connect', () => {
    console.log('链接成功'); 
});
复制代码

socket.io用法简单,方便上手,欲购从速,哈哈,继续写服务端的链接吧

服务端创建链接

服务端的搭建咱们仍是用以前使用的express来处理

// app.js文件

const express = require('express');
const app = express();
// 设置静态文件夹,会默认找当前目录下的index.html文件当作访问的页面
app.use(express.static(__dirname));

// WebSocket是依赖HTTP协议进行握手的
const server = require('http').createServer(app);
const io = require('socket.io')(server);
// 监听与客户端的链接事件
io.on('connection', socket => {
    console.log('服务端链接成功');
});
// ☆ 这里要用server去监听端口,而非app.listen去监听(否则找不到socket.io.js文件)
server.listen(4000);
复制代码

以上内容就是客户端和服务端创建了websocket链接了,如此的so easy,那么接下来继续写发送消息吧

发送消息

列表Ul输入框按钮这些都齐全了,那就开始发送消息吧

经过socket.emit('message')方法来发送消息给服务端

// index.js文件

// 列表list,输入框content,按钮sendBtn
let list = document.getElementById('list'),
    input = document.getElementById('input'),
    sendBtn = document.getElementById('sendBtn');

// 发送消息的方法
function send() {
    let value = input.value;
    if (value) {
        // 发送消息给服务器
        socket.emit('message', value);
        input.value = '';
    } else {
        alert('输入的内容不能为空!');
    }
}
// 点击按钮发送消息
sendBtn.onclick = send;
复制代码

回车发送消息

每次都要点发送按钮,也是够反用户操做行为的了,因此仍是加上咱们熟悉的回车发送吧,看代码,+号表示新增的代码

// index.js文件
...省略

// 回车发送消息的方法
+ function enterSend(event) {
+    let code = event.keyCode;
+    if (code === 13)  send();
+ }
// 在输入框onkeydown的时候发送消息
+ input.onkeydown = function(event) {
+    enterSend(event);  
+ };
复制代码

前端已经把消息发出去了,接下来该服务端出马了,继续撸

服务端处理消息

// app.js文件
...省略

io.on('connection', socket => {
    // 监听客户端发过来的消息
+    socket.on('message', msg => {
         // 服务端发送message事件,把msg消息再发送给客户端
+        io.emit('message', {
+            user: '系统',
+            content: msg,
+            createAt: new Date().toLocaleString()
+        });              
+    });
});
复制代码

io.emit()方法是向大厅和全部人房间内的人广播

客户端渲染消息

咱们继续在index.js这里写,把服务端传过来的消息接收并渲染出来

// index.js文件
...省略

// 监听message事件来接收服务端发来的消息
+ socket.on('message', data => {
      // 建立新的li元素,最终将其添加到list列表
+     let li = document.createElement('li');
+     li.className = 'list-group-item';
+     li.innerHTML = `
        <p style="color: #ccc;">
            <span class="user">${data.user}</span>
            ${data.createAt}
        </p>
        <p class="content">${data.content}</p>`;
      // 将li添加到list列表中
+     list.appendChild(li);
      // 将聊天区域的滚动条设置到最新内容的位置
+     list.scrollTop = list.scrollHeight;
+ });
复制代码

写到这里,发送消息的部分就已经完事了,执行代码应该均可以看到以下图的样子了

看到上面的图后,咱们应该高兴一下,毕竟有消息了,离成功又近了一步两步三四步

虽然上面的代码还有瑕疵,不过不要方,让咱们继续完善它

根据图片所示,全部的用户都是“系统”,这根本就分不清谁是谁了,让咱们来判断一下,须要加个用户名

建立用户名

这里咱们能够知道,当用户是第一次进来的时候,是没有用户名的,须要在设置以后才会显示对应的名字

因而乎,咱们就把第一次进来后输入的内容看成用户名

// app.js文件
...省略
// 把系统设置为常量,方便使用
const SYSTEM = '系统';

io.on('connection', socket => {
    // 记录用户名,用来记录是否是第一次进入,默认是undefined
+   let username;
    socket.on('message', msg => {
        // 若是用户名存在
+       if (username) {
             // 就向全部人广播
+            io.emit('message', {
+                user: username,
+                content: msg,
+                createAt: new Date().toLocaleString()
+            });
+       } else {  // 用户名不存在的状况
             // 若是是第一次进入的话,就将输入的内容当作用户名
+            username = msg;
             // 向除了本身的全部人广播,毕竟进没进入本身是知道的,不必跟本身再说一遍
+            socket.broadcast.emit('message', {
+                user: SYSTEM,
+                content: `${username}加入了聊天!`,
+                createAt: new Date().toLocaleString()
+            });
+        }
    });
});
复制代码

☆️ socket.broadcast.emit,这个方法是向除了本身外的全部人广播

没错,毕竟本身进没进聊天室本身内心还没数么,哈哈

下面再看下执行的效果,请看图

最基本的发消息功能已经实现了,下面咱们再接再砺,完成一个 私聊功能吧

添加私聊

在群里你们都知道@一下就表明这条消息是专属被@的那我的的,其余人是不用care的

如何实现私聊呢?这里咱们采用,在消息列表list中点击对方的用户名进行私聊,因此废话很少说,开写吧

@一下

// index.js文件
...省略

  // 私聊的方法
+ function privateChat(event) {
+    let target = event.target;
     // 拿到对应的用户名
+    let user = target.innerHTML;
     // 只有class为user的才是目标元素
+    if (target.className === 'user') {
         // 将@用户名显示在input输入框中
+        input.value = `@${user} `;
+    }
+ }
  // 点击进行私聊
+ list.onclick = function(event) {
+    privateChat(event);
+ };
复制代码

客户端已将@用户名这样的格式设置在了输入框中,只要发送消息,服务端就能够进行区分,是私聊仍是公聊了,下面继续写服务端的处理逻辑吧

服务端处理

首先私聊的前提是已经获取到了用户名了

而后正则判断一下,哪些消息是属于私聊的

最后还须要找到对方的socket实例,好方便发送消息给对方

那么,看以下代码

// app.js文件
...省略

// 用来保存对应的socket,就是记录对方的socket实例
+ let socketObj = {};

io.on('connection', socket => {
    let username;
    socket.on('message', msg => {
        if (username) {
            // 正则判断消息是否为私聊专属
+           let private = msg.match(/@([^ ]+) (.+)/);

+           if (private) {  // 私聊消息
                 // 私聊的用户,正则匹配的第一个分组
+                let toUser = private[1];
                 // 私聊的内容,正则匹配的第二个分组
+                let content = private[2];
                 // 从socketObj中获取私聊用户的socket
+                let toSocket = socketObj[toUser];

+                if (toSocket) {
                     // 向私聊的用户发消息
+                    toSocket.send({
+                        user: username,
+                        content,
+                        createAt: new Date().toLocaleString()
+                    });
+                }
            } else {    // 公聊消息
                io.emit('message', {
                    user: username,
                    content: msg,
                    createAt: new Date().toLocaleString()
                });
            }
        } else { // 用户名不存在的状况
            ...省略
            // 把socketObj对象上对应的用户名赋为一个socket
            // 如: socketObj = { '周杰伦': socket, '谢霆锋': socket }
+           socketObj[username] = socket;
        }
    });
});
复制代码

写到这里,咱们已经完成了公聊和私聊的功能了,可喜可贺,很是了不得了已经,可是不能傲娇,咱们再完善一些小细节

如今全部用户名和发送消息的气泡都是一个颜色,其实这样也很差区分用户之间的差别

SO,咱们来改下颜色的部分

分配用户不同的颜色

服务端处理颜色

// app.js文件
...省略
let socketObj = {};
// 设置一些颜色的数组,让每次进入聊天的用户颜色都不同
+ let userColor = ['#00a1f4', '#0cc', '#f44336', '#795548', '#e91e63', '#00bcd4', '#009688', '#4caf50', '#8bc34a', '#ffc107', '#607d8b', '#ff9800', '#ff5722'];

// 乱序排列方法,方便把数组打乱
+ function shuffle(arr) {
+    let len = arr.length, random;
+    while (0 !== len) {
        // 右移位运算符向下取整
+        random = (Math.random() * len--) >>> 0; 
        // 解构赋值实现变量互换
+        [arr[len], arr[random]] = [arr[random], arr[len]]; 
+    }
+    return arr;
+ }

io.on('connection', socket => {
     let username;
+    let color;  // 用于存颜色的变量

    socket.on('message', msg => {
        if (username) {
            ...省略
            if (private) {
                ...省略
                if (toSocket) {
                    toSocket.send({
                        user: username,
+                       color,
                        content: content,
                        createAt: new Date().toLocaleString()
                    });
                }
            } else {
                io.emit('message', {
                    user: username,
+                   color,
                    content: msg,
                    createAt: new Date().toLocaleString()
                });
            }
        } else { // 用户名不存在的状况
            ...省略
            // 乱序后取出颜色数组中的第一个,分配给进入的用户
+           color = shuffle(userColor)[0];

            socket.broadcast.emit('message', {
                user: '系统',
+               color,
                content: `${username}加入了聊天!`,
                createAt: new Date().toLocaleString()
            });
        }
    });
});
复制代码

服务端那边给分配好了颜色,前端这边再渲染一下就行了,接着写下去,不要停

渲染颜色

在建立的li元素上,给对应的用户名和内容分别在style样式中加个颜色就能够了,代码以下

// index.js
... 省略

socket.on('message', data => {
    let li = document.createElement('li');
    li.className = 'list-group-item';
    // 给对应元素设置行内样式添加颜色
+   li.innerHTML = `<p style="color: #ccc;"><span class="user" style="color:${data.color}">${data.user} </span>${data.createAt}</p>
                    <p class="content" style="background:${data.color}">${data.content}</p>`;
    list.appendChild(li);
    // 将聊天区域的滚动条设置到最新内容的位置
    list.scrollTop = list.scrollHeight;
});
复制代码

写完是写完了,咱们看看效果吧

写到这里,看到这里,是否疲倦了呢,年轻人不要放弃

Now,让咱们来写理论上的最最最后一个功能吧,进入某个群里聊天,该消息只有群里的人能够看到

加入指定房间(群)

咱们一直在上面的截图中看到了两个群的按钮,看到字面意思就能知道是干吗的,就是为了这一刻而准备的

下面咱们再来,继续撸,立刻就要完成大做了

客户端-进出房间(群)

// index.js文件
...省略

// 进入房间的方法
+ function join(room) {
+    socket.emit('join', room);
+ }
// 监听是否已进入房间
// 若是已进入房间,就显示离开房间按钮
+ socket.on('joined', room => {
+    document.getElementById(`join-${room}`).style.display = 'none';
+    document.getElementById(`leave-${room}`).style.display = 'inline-block';
+ });

// 离开房间的方法
+ function leave(room) {
    socket.emit('leave', room);
+ }
// 监听是否已离开房间
// 若是已离开房间,就显示进入房间按钮
+ socket.on('leaved', room => {
+    document.getElementById(`leave-${room}`).style.display = 'none';
+    document.getElementById(`join-${room}`).style.display = 'inline-block';
+ });
复制代码

上面定义的joinleave方法直接在对应的按钮上调用便可了,以下图所示

下面咱们继续写服务端的代码逻辑

服务端-处理进出房间(群)

// app.js文件
...省略
io.on('connection', socket => {
    ...省略
    // 记录进入了哪些房间的数组
+   let rooms = [];
    io.on('message', msg => {
        ...省略
    });
    // 监听进入房间的事件
+   socket.on('join', room => {
+       // 判断一下用户是否进入了房间,若是没有就让其进入房间内
+       if (username && rooms.indexOf(room) === -1) {
            // socket.join表示进入某个房间
+           socket.join(room);
+           rooms.push(room);
            // 这里发送个joined事件,让前端监听后,控制房间按钮显隐
+           socket.emit('joined', room);
            // 通知一下本身
+           socket.send({
+               user: SYSTEM,
+               color,
+               content: `你已加入${room}战队`,
+               createAt: new Date().toLocaleString()
+           });
+       }
+   });
    // 监听离开房间的事件
+   socket.on('leave', room => {
        // index为该房间在数组rooms中的索引,方便删除
+       let index = rooms.indexOf(room);
+       if (index !== -1) {
+           socket.leave(room); // 离开该房间
+           rooms.splice(index, 1); // 删掉该房间
            // 这里发送个leaved事件,让前端监听后,控制房间按钮显隐
+           socket.emit('leaved', room);
            // 通知一下本身
+           socket.send({
+               user: SYSTEM,
+               color,
+               content: `你已离开${room}战队`,
+               createAt: new Date().toLocaleString()
+           });
+       }
+   });
});
复制代码

写到这里,咱们也实现了加入和离开房间的功能,以下图所示

既然进入了房间内,那么很显然,发言的内容只能是在房间内的人才能看到,这点咱们都懂

因此下面咱们再写一下房间内发言的逻辑,继续在app.js中开撸

处理房间内发言

// app.js文件
...省略
// 上来记录一个socket.id用来查找对应的用户
+ let mySocket = {};

io.on('connection', socket => {
    ...省略
    // 这是全部链接到服务端的socket.id
+   mySocket[socket.id] = socket;
    
    socket.on('message', msg => {
        if (private) {
            ...省略
        } else {
            // 若是rooms数组有值,就表明有用户进入了房间
+           if (rooms.length) {
                // 用来存储进入房间内的对应的socket.id
+               let socketJson = {};

+               rooms.forEach(room => {
                    // 取得进入房间内所对应的全部sockets的hash值,它即是拿到的socket.id
+                   let roomSockets = io.sockets.adapter.rooms[room].sockets;
+                   Object.keys(roomSockets).forEach(socketId => {
                        console.log('socketId', socketId);
                        // 进行一个去重,在socketJson中只有对应惟一的socketId
+                       if (!socketJson[socketId]) {
+                           socketJson[socketId] = 1;
+                       }
+                   });
+               });

                // 遍历socketJson,在mySocket里找到对应的id,而后发送消息
+               Object.keys(socketJson).forEach(socketId => {
+                   mySocket[socketId].emit('message', {
+                       user: username,
+                       color,
+                       content: msg,
+                       createAt: new Date().toLocaleString()
+                   });
+               });
            } else {
                // 若是不是私聊的,向全部人广播
                io.emit('message', {
                    user: username,
                    color,
                    content: msg,
                    createAt: new Date().toLocaleString()
                });
            }
        }
    });
});
复制代码

从新运行app.js文件后,再进入房间聊天,会展现以下图的效果,只有在同一个房间内的用户,才能相互之间看到消息

麻雀虽小但五脏俱全,坚持写到这里的每一位都是赢家,不过我还想再完善最后一个小功能,就是展现一下 历史消息

毕竟每次一进到聊天室都是空空如也的样子也太苍白了,仍是但愿了解到以前的用户聊了哪些内容的

那么继续加油,实现咱们最后一个功能吧

展现历史消息

其实正确开发的状况,用户输入的全部消息应该是存在数据库中进行保存的,不过咱们这里就不涉及其余方面的知识点了,就直接用纯前端的技术去模拟一下实现了

获取历史消息

这里让客户端去发送一个getHistory的事件,在socket链接成功的时候,告诉服务器咱们要拿到最新的20条消息记录

// index.js
...省略

socket.on('connect', () => {
    console.log('链接成功');
    // 向服务器发getHistory来拿消息
+   socket.emit('getHistory');
});
复制代码

服务端处理历史记录并返回

// app.js
...省略

// 建立一个数组用来保存最近的20条消息记录,真实项目中会存到数据库中
let msgHistory = [];

io.on('connection', socket => {
    ...省略
    io.on('message', msg => {
        ...省略
        if (private) {
            ...省略
        } else {
            io.emit('message', {
                user: username,
                color,
                content: msg,
                createAt: new Date().toLocaleString()
            });
            // 把发送的消息push到msgHistory中
            // 真实状况是存到数据库里的
+           msgHistory.push({
+               user: username,
+               color,
+               content: msg,
+              createAt: new Date().toLocaleString()
+          });
        }
    });
    
    // 监听获取历史消息的事件
+   socket.on('getHistory', () => {
        // 经过数组的slice方法截取最新的20条消息
+       if (msgHistory.length) {
+           let history = msgHistory.slice(msgHistory.length - 20);
            // 发送history事件并返回history消息数组给客户端
+           socket.emit('history', history);
+       }
+   });
});
复制代码

客户端渲染历史消息

// index.js
...省略

// 接收历史消息
+ socket.on('history', history => {
    // history拿到的是一个数组,因此用map映射成新数组,而后再join一下链接拼成字符串
+   let html = history.map(data => {
+       return `<li class="list-group-item">
            <p style="color: #ccc;"><span class="user" style="color:${data.color}">${data.user} </span>${data.createAt}</p>
            <p class="content" style="background-color: ${data.color}">${data.content}</p>
        </li>`;
+   }).join('');
+   list.innerHTML = html + '<li style="margin: 16px 0;text-align: center">以上是历史消息</li>';
    // 将聊天区域的滚动条设置到最新内容的位置
+   list.scrollTop = list.scrollHeight;
+ });
复制代码

这样就所有大功告成了,完成了最后的历史消息功能,以下图所示效果

最后进行一个功能上的梳理吧,坚持到这里的人,我已经不知道如何表达对你的敬佩了,好样的

梳理一下

聊天室的功能完成了,看到这里头有点晕了,如今简单回忆一下,实际都有哪些功能

  1. 建立客户端与服务端的websocket通讯链接
  2. 客户端与服务端相互发送消息
  3. 添加用户名
  4. 添加私聊
  5. 进入/离开房间聊天
  6. 历史消息

小Tips

针对以上代码中经常使用的发消息方法进行一下区分:

  • socket.send()发送消息是为了给本身看的
  • io.emit()发送消息是给全部人看的
  • socket.broadcast.emit()发送消息除了本身都能看到

最后的最后,说下个人感觉,这篇文章写的有些难受

由于文章不能像亲口叙述同样表达的痛快,因此也在探索如何写好技术类文章,望你们理解以及多提意见吧(新增代码部分如何写的更一目了然),感谢你们辛苦的观看了,再见了!!!

相关文章
相关标签/搜索