Bootstrap

基于nodejs+json+websocket+html的聊天应用

实现

html

<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <title>Instant Messaging</title>
  <!-- 引入Bootstrap CSS -->
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
  <style>
    body {
      font-family: Arial, sans-serif;
      display: flex;
      flex-direction: column;
      height: 100vh;
    }

    #chat {
      border: 1px solid #ccc;
      height: calc(100% - 120px);
      overflow-y: scroll;
      padding: 10px;
    }

    .message {
      margin-bottom: 10px;
    }

    .avatar {
      width: 50px;
      height: 50px;
      border-radius: 50%;
    }

    .message-content {
      background-color: #e1ffc7;
      padding: 10px;
      border-radius: 15px;
      max-width: 60%;
      word-wrap: break-word;
    }

    .message-content.other {
      background-color: #c7e1ff;
    }

    .message.self .message-content {
      margin-left: auto;
    }

    .message-time {
      font-size: 16px;
    }

    .message {
      display: flex;
      align-items: flex-start;
      margin-bottom: 10px;
    }

    .avatar {
      width: 50px;
      height: 50px;
      border-radius: 50%;
      margin-right: 10px;
    }

    .message-sender {
      font-size: 14px;
      margin: 0 0 5px 0;
      color: #999;
    }

    .message-content {
      background-color: #e1ffc7;
      padding: 10px;
      border-radius: 15px;
      max-width: 60%;
      word-wrap: break-word;
      display: flex;
      flex-direction: column;
    }

    .message-content.other {
      background-color: #c7e1ff;
      align-self: flex-start;
    }

    .self {
      display: flex;
      flex-direction: row-reverse;
    }

    #container-fluid {
      width: 88%;
    }

    /* #auth-buttons {
      width: 100%;
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
    } */

    /* 确保body和html占满整个视口,并移除默认边距 */
    body,
    html {
      height: 100%;
      margin: 0;
    }

    /* 使用Flexbox布局使#auth-buttons居中 */
    #auth-buttons {
      position: absolute;
      display: flex;
      flex-direction: column;
      /* 垂直排列子元素 */
      align-items: center;
      /* 水平居中对齐 */
      justify-content: center;
      /* 垂直居中对齐 */
      width: 480px;
      top: 25%;
      left: 50%;
      margin-left: -240px;
      /* left: -50%; */
      /* 宽度设置为100% */
    }

    /* 可选:为输入框和按钮添加一些间距 */
    #wrapper {
      max-width: 400px;
      /* 设置最大宽度 */
      width: 100%;
      /* 宽度设置为100% */
    }

    .w-100 {
      width: 100px;
    }
  </style>
</head>

<body>
  <!-- 登录注册表单 -->
  <div id="auth-buttons">
    <div class="w-100 mb-4">
      <input id="account-input" class="form-control mb-2" type="text" placeholder="账户">
      <input id="password-input" class="form-control" type="password" placeholder="密码">
    </div>
    <div class="w-100">
      <button id="login-btn" class="btn btn-primary mr-2 w-100 mb-2">登录</button>
      <button id="register-btn" class="btn btn-secondary w-100">注册</button>
    </div>
  </div>

  <div id="container-fluid" style="height: 80%;display: none;">
   
    <div class="row" style="height: 80%;">
      <div class="col" id="chat"></div>
    </div>
    <div>连接状态:<button type="button" class="btn btn-success btn-sm">Success</button></div>
    <div class="col-auto" style="padding: 10px;">
      <input id="message" class="form-control" type="" placeholder="输入消息..." />
      <button id="sendBtn" class="btn btn-primary mt-2">发送</button>
    </div>

  </div>
  <!-- 添加消息提示容器 -->
  <div id="message-alert" class="alert alert-danger d-none" role="alert">
    <!-- 错误信息将插入到这里 -->
  </div>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>

  <script>

    // 获取登录和注册按钮
    const loginBtn = document.getElementById('login-btn');
    const registerBtn = document.getElementById('register-btn');
    const authButtons = document.getElementById('auth-buttons');
    const chatContainer = document.getElementById('container-fluid');
    const accountInput = document.getElementById('account-input');
    const passwordInput = document.getElementById('password-input');
    const messageAlert = document.getElementById('message-alert');

    changeContentShow = () => {
      console.log(authButtons.style.display)
      authButtons.style.display = 'none';
      chatContainer.style.display = 'block';
    }

    // 登录按钮点击事件
    loginBtn.onclick = () => {
      const account = accountInput.value.trim();
      const password = passwordInput.value;
      if (account === '' || password === '') {
        // 使用Bootstrap的消息提示显示错误信息
        messageAlert.textContent = '账户和密码不能为空';
        messageAlert.classList.remove('d-none'); // 显示消息提示
        return;
      }
      messageAlert.style.display = 'none'
      ws.send(JSON.stringify({ type: 'login', username: account, password }));

      // 登录成功后隐藏登录按钮,显示聊天界面
      changeContentShow()
    };

    const setTimer = (time) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve();
        }, time);
      });
    }
    // 注册按钮点击事件
    registerBtn.onclick = () => {
      // 执行注册逻辑
      const account = accountInput.value.trim();
      const password = passwordInput.value;
      if (account === '' || password === '') {
        // 使用Bootstrap的消息提示显示错误信息
        messageAlert.textContent = '账户和密码不能为空';
        messageAlert.classList.remove('d-none'); // 显示消息提示
        return;
      }
      messageAlert.style.display = 'none'
      ws.send(JSON.stringify({ type: 'register', username: account, password }));
      // ...
      // 注册成功后隐藏注册按钮,显示聊天界面
      changeContentShow()
    };

    // ...(保留原有的WebSocket连接和其他代码)...
    const ws = new WebSocket('ws://localhost:8082');
    const chat = document.getElementById('chat');
    const messageInput = document.getElementById('message');
    const sendBtn = document.getElementById('sendBtn');


    // 监听服务器发送的消息
    ws.onmessage = async (event) => {
      console.log(event.data);
      const data = JSON.parse(event.data);
      if (data.type === 'login' && data.code === 200) {
        localStorage.setItem('userInfo', JSON.stringify(data.clients));
        changeContentShow()
        return;
      }
      if (data.type === 'error') {
        alert(data.message);
        location.reload();
      } else {
        if (Array.isArray(data)) {
          data.forEach(m => {
            addMessage(m, true);
          });
        } else {
          addMessage(data, true);
        }
      }
    };

    // 发送消息
    sendBtn.onclick = () => {
      const msg = messageInput.value.trim();
      if (msg) {
        ws.send(JSON.stringify({ type: 'message', message: msg }));
        addMessage({ message: msg }, false); // 自己发送的消息
        messageInput.value = '';
      }
    };

    // 允许按 Enter 键发送消息
    messageInput.addEventListener('keyup', (event) => {
      if (event.key === 'Enter') {
        sendBtn.click();
      }
    });

    function addMessage(message, isOther, senderName = '') {
      const messageContainer = document.createElement('div');
      messageContainer.className = `message ${isOther ? 'other' : 'self'}`;

      // 添加头像
      const avatar = document.createElement('img');
      avatar.src = isOther ? './public/images/downloaded-image3.jpg' : './public/images/downloaded-image6.jpg'; // 替换为实际头像路径或默认头像
      console.log(localStorage.getItem('userInfo'))
      const username = JSON.parse(localStorage.getItem('userInfo'))?.username || '我';
      avatar.alt = senderName || (isOther ? message.sender : username);
      avatar.className = 'avatar';

      // 添加网名
      const name = document.createElement('div');
      name.className = 'message-sender';
      name.textContent = senderName || (isOther ? message.sender : username);

      // 添加消息内容
      const messageContent = document.createElement('div');
      if (isOther) {
        messageContent.className = `message-content other`;
      } else {
        messageContent.className = `message-content`;
      }
      messageContent.textContent = message.message;

      // 组合元素
      messageContainer.appendChild(avatar);
      messageContainer.appendChild(name);
      messageContainer.appendChild(messageContent);
      chat.appendChild(messageContainer);
      chat.scrollTop = chat.scrollHeight;
    }
    if (localStorage.getItem('userInfo')) {
      // const { username, password } = JSON.parse(localStorage.getItem('userInfo'));
      // ws.send(JSON.stringify({ type: 'login', username, password }));
      changeContentShow()
    }
  </script>
</body>

</html>

nodejs

const WebSocket = require('ws');
const fs = require('fs');
const path = require('path');

const wss = new WebSocket.Server({ port: 8082 });
const usersFilePath = path.join(__dirname, 'users.json');
const chatLogFilePath = path.join(__dirname, 'chatlog.json');
let clientNameCounter = 1;
const clients = new Map();

// 加载用户数据
let users = [];
if (fs.existsSync(usersFilePath)) {
    const data = fs.readFileSync(usersFilePath, 'utf-8');
    users = JSON.parse(data);
}

// 当有客户端连接时触发
wss.on('connection', (ws) => {
    console.log('新客户端已连接');

    // 提示客户端输入用户名和密码
    // ws.send(JSON.stringify({ type: 'register' }));

    // 监听客户端的响应
    ws.on('message', (message) => {
        console.log(`收到消息: ${message}`);
        const data = JSON.parse(message) || {};
        if (data.type === 'register') {
            // 注册新用户
            if (users.find(user => user.username === data.username)) {
                ws.send(JSON.stringify({ type: 'error', message: '用户名已存在' }));
            } else {
                users.push({ username: data.username, password: data.password });
                fs.writeFileSync(usersFilePath, JSON.stringify(users, null, 2));
                const clientName = data.username;
                clients.set(ws, clientName);
                console.log(`客户端 ${clientName} 注册成功,连接数: `, wss.clients.size);
            }
        } else if (data.type === 'login') {
            // 验证用户
            const user = users.find(user => user.username === data.username && user.password === data.password);
            if (user) {
                const clientName = user.username;
                clients.set(ws, clientName);
                console.log(`客户端 ${clientName} 登录成功,连接数: `, wss.clients.size);

                // 读取并发送历史聊天记录给新连接的客户端
                let messages = [];
                if (fs.existsSync(chatLogFilePath)) {
                    const data = fs.readFileSync(chatLogFilePath, 'utf-8');
                    messages = JSON.parse(data);
                }
                ws.send(JSON.stringify({ type: 'login', code: 200, message: '登录成功', clients: user }));
                ws.send(JSON.stringify(messages));
            } else {
                ws.send(JSON.stringify({ type: 'error', message: '用户名或密码错误' }));
            }
        } else if (data.type === 'message') {
            // 处理客户端发送的消息
            // console.log(`收到消息: ${data.message}`);
            const msg = data.message;
            // console.log(clients)
            // 读取现有消息
            let messages = [];
            if (fs.existsSync(chatLogFilePath)) {
                const data = fs.readFileSync(chatLogFilePath, 'utf-8');
                messages = JSON.parse(data);
            }
            // 添加新消息
            messages.push({
                sender: clients.get(ws),
                message: msg,
                time: new Date().toISOString()
            });
            // 写回文件
            fs.writeFileSync(chatLogFilePath, JSON.stringify(messages, null, 2));
            // 广播消息给所有连接的客户端,除了发送者
            const msgJson = JSON.stringify({ sender: clients.get(ws), message: msg, time: new Date().toISOString() });
            console.log(`广播消息: ${msgJson}`);
            wss.clients.forEach((client) => {
                if (client !== ws && client.readyState === WebSocket.OPEN) {
                    client.send(msgJson);
                }
            });
        }
    });

    // 处理客户端断开连接
    ws.on('close', () => {
        console.log(`客户端 ${clients.get(ws)} 已断开`);
        clients.delete(ws);
    });
});

console.log('WebSocket服务器已启动,监听端口8082');
;