/
ALMY
/
Chat_WebSocket
Обзор
Документация
Войти
/
ALMY
/
Chat_WebSocket
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
web/chat.js
262 строки
18 KB
almy-win
iteration 5-optimization + fix bugs + add comments
01 июн 2026, 20:08
01 июн 2026, 20:08
2cfadb7
Код
Авторство
О чём код?
// Находим и сохраняем DOM-элементы интерфейса для дальнейшей работы с ними const messagesEl = document.getElementById('messages'); // Окно, где отображаются сообщения чата const messageInput = document.getElementById('messageInput'); // Поле ввода текста сообщения const sendButton = document.getElementById('sendButton'); // Кнопка «Отправить» const usersListEl = document.getElementById('usersList'); // Боковая панель со списком пользователей // Объявляем глобальные переменные состояния чата let ws; // Сюда запишем экземпляр WebSocket-соединения после подключения let username = ''; // Имя текущего пользователя (никнейм) let currentChat = null; // Имя пользователя, с которым сейчас открыт приватный чат let users = []; // Список всех пользователей, которые сейчас онлайн let unread = {}; // Объект-карта для отслеживания непрочитанных сообщений (например: { 'Ivan': true }) /** * Функция инициализации и настройки WebSocket-соединения с сервером. * Отвечает за прием, обработку сообщений и реакцию на разрыв связи. */ function connect() { // Открываем WebSocket-соединение. Используем текущий хост (домен/порт) сайта ws = new WebSocket(`ws://${window.location.host}/ws`); // Событие: соединение успешно установлено ws.onopen = () => { // Сразу запрашиваем у пользователя никнейм promptForUsername(); }; // Событие: от сервера пришло текстовое сообщение ws.onmessage = (event) => { // Очищаем полученный текст от лишних пробелов по краям const msg = event.data.trim(); // Случай 1: Сервер прислал обновленный список пользователей (команда начинается с /users) if (msg.startsWith('/users ')) { const listStr = msg.slice(7); // Отрезаем саму команду '/users ', оставляя только строку с никами // Если строка не пустая, разбиваем её по запятой, убирая случайные пробелы. Если пустая — пишем пустой массив. users = listStr ? listStr.split(',').filter(u => u.trim() !== '') : []; updateUserList(); // Перерисовываем список пользователей на экране return; // Завершаем обработку этого сообщения } // Регулярное выражение для разбора приватных сообщений формата: [ЧЧ:ММ Отправитель]->[Получатель]: Проверка: Текст const privateMsgRegex = /^\[(\d{2}:\d{2}) ([^\]]+)\]->\[(.+?)\]: ([^:]+): (.*)$/; const match = msg.match(privateMsgRegex); // Проверяем, подходит ли сообщение под этот формат // Случай 2: Это валидное приватное сообщение if (match) { // Извлекаем нужные кусочки данных из регулярного выражения const [, time, sender, target, senderCheck, body] = match; const from = sender.trim(); // Убираем пробелы из имени отправителя // Проверяем, имеет ли это сообщение отношение к нам (мы либо отправитель, либо получатель) if (target === username || from === username) { // Выводим сообщение в окно чата addMessage(from, `[${time}] ${from}: ${body}`); // Если сообщение пришло от кого-то другого и этот чат сейчас НЕ открыт как активный if (from !== currentChat && from !== username) { unread[from] = true; // Отмечаем этот чат в объекте unread как непрочитанный updateUserList(); // Перерисовываем список, чтобы у пользователя загорелся индикатор // Если вкладка браузера свернута или пользователь в другой вкладке if (document.hidden) { // Отправляем системное push-уведомление на рабочий стол showBrowserNotification(`Сообщение от ${from}`, body); } } } return; // Завершаем обработку } // Случай 3: Сообщение формата "Вы -> Кому-то: текст", подтверждающее нашу отправку if (msg.includes('Вы ->')) { // Вытаскиваем сам текст сообщения после двоеточия const bodyMatch = msg.match(/Вы -> [^:]+: (.*)/); const body = bodyMatch ? bodyMatch[1] : 'Сообщение'; // Вытаскиваем время отправки из квадратных скобок, либо берем текущее время ПК const timeMatch = msg.match(/\[(\d{2}:\d{2})\]/); const time = timeMatch ? timeMatch[1] : new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); // Добавляем наше сообщение на экран в категорию «Вы» addMessage('Вы', `[${time}] Вы: ${body}`); return; // Завершаем обработку } // Случай 4: Если сообщение не подошло ни под один фильтр, считаем его системным (серверным) addSystemMessage(msg); }; // Событие: Соединение с сервером закрылось (упал сервер, пропал интернет и т.д.) ws.onclose = () => { // Информируем пользователя о проблеме прямо в чате addSystemMessage('⚠️ Соединение потеряно. Переподключение...'); // Пробуем заново подключиться к серверу через 3 секунды (3000 мс) setTimeout(connect, 3000); }; // Событие: Произошла какая-то ошибка в работе вебсокета ws.onerror = (err) => { console.error('Ошибка WebSocket:', err); // Логируем ошибку в консоль разработчика для отладки }; } /** * Функция для обязательного запроса имени пользователя при входе. * Не пустит в чат, пока не будет введено валидное имя. */ function promptForUsername() { const nick = prompt('Введите ваше имя:'); // Показываем стандартное модальное окно ввода // Если пользователь нажал "Отмена" или ввёл только пробелы if (!nick || !nick.trim()) { alert('Имя обязательно!'); // Предупреждаем return promptForUsername(); // Запускаем функцию заново (рекурсия), пока не введет имя } username = nick.trim(); // Сохраняем очищенное имя в глобальную переменную ws.send(`/nick ${username}`); // Отправляем серверу команду регистрации никнейма messageInput.disabled = false; // Активируем поле ввода текста (делаем его доступным для печати) } /** * Функция для динамического обновления списка пользователей в UI. */ function updateUserList() { usersListEl.innerHTML = ''; // Полностью очищаем текущий HTML-список пользователей // Фильтруем массив: исключаем из списка самого себя (чтобы не переписываться с собой) const onlineUsers = users.filter(u => u !== username); // Если в сети больше вообще никого нет if (onlineUsers.length === 0) { const li = document.createElement('li'); // Создаем пустой элемент списка li.className = 'placeholder'; // Задаем класс для стилизации заглушки li.textContent = 'Нет пользователей'; // Пишем текст заглушки usersListEl.appendChild(li); // Добавляем заглушку в дерево документа return; // Выходим из функции } // Если другие пользователи есть, перебираем их циклом onlineUsers.forEach(user => { const li = document.createElement('li'); // Создаем элемент списка для юзера li.className = 'user-item'; // Задаем базовый класс стиля // Если этот юзер совпадает с тем, чей чат сейчас открыт — добавляем класс активности if (user === currentChat) li.classList.add('active'); // Если от этого юзера есть непрочитанные сообщения — добавляем класс непрочитанного if (unread[user]) li.classList.add('unread'); li.textContent = user; // Пишем имя пользователя внутрь элемента li.onclick = () => selectUser(user); // Назначаем клик по юзеру: открыть чат с ним usersListEl.appendChild(li); // Добавляем готовый элемент в список на странице }); } /** * Функция переключения активного чата на выбранного пользователя. * @param {string} user - Имя пользователя, с которым открываем диалог. */ function selectUser(user) { currentChat = user; // Запоминаем в глобальной переменной, с кем мы сейчас говорим delete unread[user]; // Удаляем флаг «непрочитано» для этого пользователя, так как мы открыли чат updateUserList(); // Сразу перерисовываем список, чтобы убрать маркер непрочитанного сообщения messagesEl.innerHTML = ''; // Очищаем старые сообщения (данный клиент не хранит историю локально) const header = document.createElement('div'); // Создаем элемент заголовка чата header.className = 'placeholder'; // Стилизуем его header.textContent = `Чат с ${user}`; // Пишем, с кем ведется диалог messagesEl.appendChild(header); // Выводим этот заголовок на экран } /** * Функция добавления обычного текстового сообщения в окно переписки. * @param {string} sender - Кто отправил (имя или 'Вы') * @param {string} text - Полный текст сообщения */ function addMessage(sender, text) { if (!currentChat) return; // Если ни один чат не выбран, игнорируем попытки что-то отрисовать // Если сообщение пришло не от текущего собеседника, не от нас, и в тексте нет явного упоминания нашего ника — игнорируем if (sender !== currentChat && sender !== 'Вы' && !text.includes(`${username}:`)) { return; } // Ищем элемент-заглушку ("Чат с ...") const placeholder = messagesEl.querySelector('.placeholder'); if (placeholder) { messagesEl.removeChild(placeholder); // Удаляем заглушку, так как пошли реальные сообщения } // Проверяем, наше ли это сообщение (по слову 'Вы' или по вхождению нашего имени в текст) const isMine = sender === 'Вы' || text.includes(`${username}:`); const div = document.createElement('div'); // Создаем контейнер для сообщения // Задаем класс: 'message mine' (справа, зеленый/синий) или 'message other' (слева, серый) div.className = `message ${isMine ? 'mine' : 'other'}`; div.textContent = text; // Безопасно вставляем текст (защита от XSS-уязвимостей) messagesEl.appendChild(div); // Добавляем сообщение в окно чата messagesEl.scrollTop = messagesEl.scrollHeight; // Автоматически прокручиваем скролл чата в самый ник } /** * Функция для вывода ярких системных уведомлений (серверные события, ошибки подключения). * @param {string} text - Текст уведомления */ function addSystemMessage(text) { const div = document.createElement('div'); // Создаем контейнер сообщения div.className = 'message other'; // Берем базовый стиль чужого сообщения div.style.backgroundColor = '#f39c12'; // Принудительно красим фон в оранжевый цвет уведомления div.style.color = 'white'; // Текст делаем белым для читаемости div.textContent = text; // Вставляем системный текст messagesEl.appendChild(div); // Отрисовываем в чате messagesEl.scrollTop = messagesEl.scrollHeight; // Прокручиваем чат вниз } /** * Функция создания стандартного всплывающего уведомления операционной системы/браузера. * @param {string} title - Заголовок уведомления (например, имя отправителя) * @param {string} body - Текст уведомления (само сообщение) */ function showBrowserNotification(title, body) { // Проверяем: разрешил ли пользователь уведомления И скрыта ли сейчас вкладка чата if (Notification.permission === 'granted' && document.hidden) { // Создаем встроенный системный Notification new Notification(title, { body, icon: 'https://via.placeholder.com/40' }); } } // При старте скрипта: проверяем, поддерживает ли браузер Notification API if ('Notification' in window) { Notification.requestPermission(); // Запрашиваем у пользователя разрешение на показ push-уведомлений } // Назначаем обработчик на клик по кнопке «Отправить» sendButton.onclick = sendMessage; // Назначаем обработчик на нажатие клавиш в поле ввода messageInput.addEventListener('keypress', e => { if (e.key === 'Enter') sendMessage(); // Если нажали клавишу Enter — вызываем функцию отправки }); /** * Функция считывания сообщения из поля ввода и отправки его на сервер по WebSocket. */ function sendMessage() { const text = messageInput.value.trim(); // Берём текст, убирая случайные пробелы по краям // Валидация: если текст пустой или не выбран конкретный собеседник if (!text || !currentChat) { alert('Выберите собеседника!'); // Предупреждаем пользователя return; // Прерываем отправку } // Отправляем на сервер команду в формате: /msg ИмяСобеседника ТекстСообщения ws.send(`/msg ${currentChat} ${text}`); messageInput.value = ''; // Полностью очищаем поле ввода для следующего сообщения } // Запускаем весь процесс: инициируем первое подключение к серверу чата connect();