使用原生js + websocket 做一个聊天室

595 阅读4分钟

前言

websocket是 html5 新增的一项api,实现客户端与服务器之间的即时通信。今天用它来实现一个聊天室demo,这里选择原生js来实现,因为用惯了vue和react的舒适框架,是时候复习一下原生的api了。毕竟现在前端技术更新很快,掌握好底层的东西才能做到以不变应万变

demo在线预览:http://101.42.108.39:90/chat/

思路

后台使用node搭建一个websocket服务器,客户端连接此服务器完成握手,客户端每次发送消息,后台就向所有握手的客户端广播消息

关键api

前端

  • sorket = new WebSocket("ws://localhost:3000") 【初始化WebSocket对象】

  • this.sorket.onopen 【与服务端建立连接触发】

  • this.sorket.send 【向服务器发送消息】

  • this.sorket.onmessage 【收到服务端推送消息触发】

后台

  • ws.createServer=(conn=>{}).listen(3000) 【创建ws服务器】

  • conn.on("text", function (obj) {}) 【接收消息】

  • conn.sendText() 【向所有握手的客户端广播消息】

html部分

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>心念--云聊天室</title>
    <link rel="stylesheet" href="./style.css" />
  </head>
  <body>
    <div class="container">
      <header>在线人数:0</header>
      <main></main>
      <footer>
        <input
          type="text"
          placeholder="请输入要发送的内容..."
          id="messageInput"
        />
        <button id="send">发 送</button>
      </footer>

      <div id="modal">
        <div class="modal-content">
          <h2>请输入昵称</h2>
          <input type="text" />
          <div><button>开始聊天</button></div>
        </div>
      </div>
    </div>
    <script src="./chat.js"></script>
  </body>
</html>

css样式

* {
  margin: 0;
  padding: 0;
}

body,
html {
  height: 100%;
}

.container {
  height: 100%;
  display: flex;
  flex-direction: column;
}

header {
  height: 7vh;
  border-bottom: 2px solid #555;
  text-align: center;
  line-height: 7vh;
  font-size: 18px;
  font-weight: bold;
}

main {
  height: 85vh;
  padding: 10px;
  padding-bottom: 70px;
  overflow: auto;
}

main .join-tip {
  text-align: center;
  color: #999;
  margin: 5px;
}

main .mesItem {
  position: relative;
  display: flex;
  padding: 25px 0;
  color: #fff;
}

main .mesItem-me {
  position: relative;
  display: flex;
  padding: 25px 0;
  color: #fff;
  justify-content: flex-end;
}

main .nickname {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  background: #999;
  text-align: center;
  line-height: 50px;
}
main .content {
  position: relative;
  padding: 0 10px;
  text-align: center;
  line-height: 50px;
  border-radius: 5px;
}

main p {
  color: gray;
  position: absolute;
  bottom: -5px;
}

main .mesItem .content {
  background-color: rgb(88, 179, 212);
  margin-left: 15px;
}

main .mesItem-me .content {
  background-color: rgb(21, 136, 21);
  margin-right: 15px;
}

main .mesItem .content::before {
  position: absolute;
  left: -20px;
  top: 50%;
  transform: translateY(-50%);
  height: 0;
  width: 0;
  content: "";
  border: 10px solid rgba(255, 255, 255, 0);
  border-top: 6px solid rgba(255, 255, 255, 0);
  border-bottom: 6px solid rgba(255, 255, 255, 0);
  border-right-color: rgb(88, 179, 212);
}

main .mesItem-me .content::before {
  position: absolute;
  right: -20px;
  top: 50%;
  transform: translateY(-50%);
  height: 0;
  width: 0;
  content: "";
  border: 10px solid rgba(255, 255, 255, 0);
  border-top: 6px solid rgba(255, 255, 255, 0);
  border-bottom: 6px solid rgba(255, 255, 255, 0);
  border-left-color: rgb(21, 136, 21);
}

footer {
  height: 8vh;
  width: 100%;
  display: flex;
  border-top: 1px solid #999;
  position: fixed;
  bottom: 0;
}
#messageInput {
  font-size: 18px;
  border: none;
  padding-left: 20px;
  outline: none;
  line-height: 8vh;
  width: 100%;
}
#send {
  width: 100px;
  cursor: pointer;
  background-color: aquamarine;
  border-top: 1px solid #999;
  font-size: 18px;
}

#modal {
  height: 100%;
  width: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  position: fixed;
  display: none;
}

#modal .modal-content {
  border-radius: 5px;
  padding: 10px;
  background-color: #fff;
  border: 1px solid #555;
  height: 200px;
  width: 300px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

#modal .modal-content h2 {
  text-align: center;
}

#modal .modal-content input {
  text-align: center;
  width: 100%;
  height: 40px;
  margin: 15px 0;
}

#modal .modal-content button {
  margin: 0 auto;
  display: block;
  background-color: aquamarine;
  border: 1px solid #eee;
  padding: 10px;
  font-size: 18px;
  cursor: pointer;
}

js部分

class Chat {
  header = document.querySelector("header");
  modal = document.querySelector("#modal");
  modalInput = document.querySelector("#modal input");
  modalButton = document.querySelector("#modal button");
  msgInput = document.querySelector("#messageInput");
  msgSendBtn = document.querySelector("#send");
  main = document.querySelector("main");
  user = {};
  msg = "";
  sorket = new WebSocket("ws://localhost:3000");
  // sorket = new WebSocket("ws://101.42.108.39:3000");
  msgList = [];

  constructor() {
    // 如果localstorge存在用户信息,直接引用
    // 不存在,就弹窗注册
    const user = localStorage.getItem("user");
    if (!user) {
      this.modal.style.display = "block";
      this.modalButton.onclick = () => {
        if (!this.modalInput.value) return alert("昵称不能为空");
        const name = this.modalInput.value;
        const uid = "chat_user_" + Date.now();
        const userInfo = { name, uid };
        this.user = userInfo;
        // localStorage存一下
        localStorage.setItem("user", JSON.stringify(userInfo));
        this.modal.style.display = "none";

        // 广播入场通知
        this.send({ ...this.user, type: 1 });
      };
    } else {
      this.user = JSON.parse(user);
    }

    // 消息输入与发送事件+回车发送事件
    this.msgInput.oninput = (e) => {
      this.msg = e.target.value;
    };
    this.msgSendBtn.onclick = () => {
      if (!this.msg) return alert("不能发送空的内容");
      this.send({ ...this.user, msg: this.msg, type: 2 });
    };
    document.onkeydown = (event) => {
      var e = event || window.event;
      if (e && e.keyCode == 13) {
        //回车键的键值为13
        if (!this.msg) return alert("不能发送空的内容");
        this.send({ ...this.user, msg: this.msg, type: 2 });
      }
    };

    this.sorket.onopen = () => {
      console.log("连接服务器成功");

      // 如果是注册过的用户,发送入场广播
      if (this.user.name) this.send({ ...this.user, type: 1 });
    };

    // 消息接收监听
    this.sorket.onmessage = (e) => {
      let message = JSON.parse(e.data);
      this.msgList.push(message);
      this.render();
    };
  }

  // 发送消息
  send(data) {
    this.sorket.send(JSON.stringify(data));
    this.msgInput.value = "";
    this.msg = "";
  }

  render() {
    let html = "";
    this.msgList.forEach(({ type, uid, name, msg, time, userTotal }) => {
      if (type === 1) {
        html += `<div class="join-tip">${name} 加入了聊天</div>`;
      }

      if (type === 2 && uid !== this.user.uid) {
        html += `<div class="mesItem">
            <div class="nickname">${name}</div>
            <div class="content">${msg}</div>
            <p>${time}</p>
          </div>`;
      }

      if (type === 2 && uid === this.user.uid) {
        html += ` <div class="mesItem-me">
        <div class="content">${msg}</div>
        <div class="nickname">${name}</div>
        <p>${time}</p>
      </div>`;
      }

      this.header.innerText = `在线人数:${userTotal}`;
    });
    this.main.innerHTML = html;

    // 保持滚动到最底部
    this.main.scrollTop = this.main.scrollHeight;
  }
}

new Chat();

node部分

const ws = require("nodejs-websocket");
const moment = require("moment");

var server = ws
  .createServer(function (conn) {
    conn.on("text", function (obj) {
      obj = {
        ...JSON.parse(obj),
        time: moment().format("YYYY-MM-DD HH:mm:ss"),
      };

      // 连接用户数
      obj.userTotal = server.connections.length;

      // 发送广播
      server.connections.forEach((conn) => {
        conn.sendText(JSON.stringify(obj));
      });
    });

    conn.on("close", function (code, reason) {
      console.log("关闭连接");
    });
    conn.on("error", function (code, reason) {
      console.log("异常关闭");
    });
  })
  .listen(3000);
console.log("WebSocket建立完毕");