предыдущий пост«WebSocket — время показать себя с хорошей стороны»На самом деле это незаконченная книга.
Из-за этого то, что было обещано вам, все еще должно быть выполнено.В следующей статье давайте используем прекрасный socket.io для достижениячатфункция
дружеское напоминание: Если вы пишете функцию чата в первый раз, вам потребуется время, чтобы ее разжевать и переварить, но после того, как вы постучите два или три раза, вы потихоньку поймете и будете ее использовать.
выложи проект сюдаадрес, если вам нужно изучить его, пожалуйста, заберите его!
Процесс разработки чата
На самом деле, с точки зрения пользователя, этот процесс — не что иное, как подключение и отправка сообщения.
Однако на самом деле взгляд на вещи с точки зрения пользователя действительно таков, поэтому не ходите по кругу и сразу переходите к теме.
установить соединение
Конечно да, это точно первый шаг во всей замечательной метафизике.Если не установить связь, то давайте поговорим об этом.
Говоря об этом, я вдруг подумал, что я должен сначала дать вам структуру html, иначе как я могу сбить ее пошагово?
Сначала вставьте структуру каталогов, все следующие файлы могут соответствовать каталогу
структура страницы
Стиль вёрстки делается напрямую с помощью бутстрапа, что удобно и быстро.Главное, чтобы все посмотрели.Здесь это не пустая трата времени.index.htmlадрес файла
Никакой функции, только макет страницы, всеcopyПросто взгляните и посмотрите, как это выглядит
Давайте попробуем написать два набора кода для создания соединения на стороне клиента и на стороне сервера, соберем вместе
этоважные вещи, открыть
Клиент устанавливает соединение
// index.js文件
let socket = io();
// 监听与服务端的连接
socket.on('connect', () => {
console.log('连接成功');
});
socket.io удобен и удобен в использовании.Если хотите купить, поторопитесь, ха-ха, продолжайте писать подключение сервера.
Сервер устанавливает соединение
Для построения сервера мы по-прежнему используем экспресс, используемый ранее для работы с ним.
// 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);
Вышеупомянутое содержание заключается в том, что клиент и сервер установили соединение через веб-сокет, поэтому просто продолжайте писать и отправлять сообщения.
Отправить сообщение
Список ул.,Поле ввода,кнопкаВсе готово, начинаем отправлять сообщения
пройти черезМетод socket.emit('сообщение')отправить сообщение на сервер
// 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, этот метод для трансляции всем, кроме себя
Правильно, в конце концов, я не знаю, нахожусь ли я в чате, ха-ха
Давайте посмотрим на эффект реализации ниже, пожалуйста, посмотрите на картинку
Реализована самая основная функция отправки сообщений, давайте приложим настойчивые усилия для завершенияприватный чатпанель функцийДобавить приватный чат
Все в группе знают, что @ Once означает, что сообщение предназначено исключительно для человека, который был @@, и другим не нужно заботиться.
Как добиться приватного чата? Здесь мы используем, нажмите на имя пользователя другой стороны в списке списка сообщений, чтобы пообщаться в частном порядке, так что не говорите глупостей, давайте напишем это
@немного
// 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);
+ };
Клиент установил формат @username в поле ввода.Пока сообщение отправляется, сервер может различать, является ли это приватным чатом или общедоступным чатом.Давайте продолжим писать логику обработки сервера.
обработка на стороне сервера
Прежде всего, предпосылка приватного чата заключается в том, что имя пользователя получено.
Затем регулярно оценивайте, какие сообщения относятся к приватным чатам.
Наконец, вам нужно найти экземпляр сокета другой стороны, чтобы было удобно отправлять сообщения другой стороне.
Затем посмотрите на следующий код
// 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;
}
});
});
На данный момент мы доделали функции публичного чата и приватного чата.Это приятно, это восхитительно, но мы не можем быть высокомерными.Давайте немного его улучшим.мельчайший
Теперь все логины и пузыри для отправки сообщенийодин цвет, по фактутрудно отличитьРазличия между пользователями
Итак, давайте изменим часть цвета
Назначьте пользователям разные цвета
Серверная обработка цветов
// 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 вы можете добавить цвет в стиль стиля для соответствующего имени пользователя и контента Код выглядит следующим образом
// 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;
});
Закончил писать, посмотрим эффект
Пиши сюда, смотри сюда, ты устал, молодежь не сдаетсяТеперь давайте напишемтеоретическиПоследняя функция, войдите в определенную группу, чтобы общаться в чате, сообщение могут видеть только люди в группе
Присоединяйтесь к назначенной комнате (группе)
Мы видели кнопки двух групп на скриншоте выше, вы можете увидеть, что они делают, когда вы видите буквальное значение, они готовы к этому моменту.
Далее, давайте еще, продолжайте играть, скоро мы закончим шедевр
Клиент - Вход и выход из комнаты (группа)
// 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';
+ });
определено вышеjoinиleaveМетод можно вызвать непосредственно по соответствующей кнопке, как показано на следующем рисунке.
Далее продолжаем писать логику кода серверной частиСервер - обработка входа и выхода из комнаты (группа)
// 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 и входа в комнату для чата отобразится эффект, показанный ниже. Только пользователи в одной комнате могут видеть сообщения друг с другом.
Хоть воробей и маленький, но все внутренние органы у него есть.Каждый, кто настаивает на том, чтобы написать здесь, является победителем, но я все же хочу улучшить последнюю маленькую функцию, которая заключается в том, чтобы показать это.исторические новостиВедь каждый раз, когда я захожу в чат, он выглядит пустым и бледным, я все еще надеюсь узнать, о чем болтали предыдущие пользователи.
Итак, продолжайте, давайте реализуем нашу последнюю функцию
Показать исторические новости
На самом деле, в случае корректной разработки, все сообщения, введенные пользователем, должны храниться в базе данных, но мы не будем здесь задействовать другие точки знаний, а будем напрямую использовать чистую front-end технологию для имитации реализации.
Получить исторические новости
Здесь пусть клиент отправит событие getHistory, и когда подключение к сокету будет успешным, сообщите серверу, что мы хотим получить последние 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;
+ });
Все это сделано, и функция окончательного исторического сообщения завершена, как показано на следующем рисунке.
Напоследок разберемся с функциями.Для тех, кто тут упорствовал, не знаю, как выразить вам свое восхищение.Молодцы.разобраться
Доработана функция чата.У меня немного кружится голова,когда я его здесь вижу.Теперь вкратце напомню какие у него функции на самом деле есть.
- Создайте коммуникационное соединение через веб-сокет между клиентом и сервером.
- Клиент и сервер отправляют сообщения друг другу
- Добавить имя пользователя
- Добавить приватный чат
- Войти/выйти из чата комнаты
- исторические новости
Маленькие советы
В приведенном выше коде сделаны следующие различия для часто используемых методов отправки сообщений:
- socket.send() отправляет сообщения, чтобы показать себя
- io.emit() отправляет сообщение для всеобщего обозрения
- socket.broadcast.emit() отправляет сообщения, которые можно увидеть, кроме самих себя
Наконец, позвольте мне рассказать вам о своих чувствах, эту статью немного неудобно писать.
Поскольку статья не может выразить радость, как личное повествование, я также изучаю, как хорошо писать технические статьи. Я надеюсь, что каждый сможет понять и высказать больше мнений (как написать новую часть кода, более понятно с первого взгляда), спасибо за внимание, до свидания! ! !