Socket实现聊天室项目实战

1,195 阅读2分钟

socket实现聊天室

案例:实现聊天室

基于node+express+socket.io+vue+flex布局来实现简易的聊天室

实现功能

登录检测

系统提示在线人员状态(进入/离开)

接收和发送消息

自定义消息字体颜色

支持发送表情

支持发送图片

支持发送窗口震动

项目结构:

image-20190903102238943.png

index.html

<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link rel="stylesheet" href="style/index.css">
    <link rel="stylesheet" href="style/font-awesome-4.7.0/css/font-awesome.min.css">
  </head>

  <body>
    <div id="app">
      <div class="name" v-if='isShow'>
        <!-- <h2>请输入你的昵称</h2> -->
        <input @keyup.enter='handleClick' type="text" id="name" placeholder="请输入昵称..." autocomplete="off"
               v-model='username'>
        <button id="nameBtn" @click='handleClick'>确 定</button>
      </div>
      <div class="main" :class='{shaking:isShake}'>
        <div class="header">
          <img src="image/logo.jpg">
          ❤️聊天室
        </div>
        <div id="container">
          <div class="conversation">
            <ul id="messages">
              <li v-for='(user,index) in userSystem' :class='user.side'>
                <div v-if='user.isUser'>
                  <img :src="user.img" alt="">
                  <div>
                    <span>{{user.name}}</span>
                    <p :style="{color: user.color}" v-html='user.msg'>
                    </p>
                  </div>
                </div>
                <p class='system' v-else>
                  <span>{{nowDate}}</span><br />
                  <span v-if='user.status'>{{user.name}}{{user.status}}了聊天室</span>
                  <span v-else>{{user.name}}发送了一个窗口抖动</span>
                </p>

              </li>
            </ul>
            <form action="">
              <div class="edit">
                <input type="color" id="color" v-model='color'>
                <i title="自定义字体颜色" id="font" class="fa fa-font">
                </i><i @click='handleSelectEmoji' @dblclick='handleDoubleSelectEmoji' title="双击取消选择" class="fa fa-smile-o" id="smile">
                </i><i @click='handleShake' title="单击页面震动" id="shake" class="fa fa-bolt">
                </i>
                <input type="file" id="file">
                <i class="fa fa-picture-o" id="img"></i>
                <div class="selectBox" v-show='isEmojiShow'>
                  <div class="smile" id="smileDiv">
                    <p>经典表情</p>
                    <ul class="emoji">
                      <li v-for='(emojiSrc,i) in emojis' :id='i' :key='i'>
                        <img :src="emojiSrc" :alt="i+1" @click='handleEmojiImg(i+1)'>
                      </li>
                    </ul>
                  </div>
                </div>
              </div>
              <!-- autocomplete禁用自动完成功能 -->
              <textarea id="m" v-model='msgVal' autofocus @keyup.enter='handleSendMsg'></textarea>
              <button class="btn rBtn" id="sub" @click='handleSendMsg'>发送</button>
              <button class="btn" id="clear" @click='handleLogout'>关闭</button>
            </form>
          </div>
          <div class="contacts">
            <h1>在线人员(<span id="num">{{userInfo.length}}</span>)</h1>
            <ul id="users">
              <li v-for='(user,index) in userInfo'>
                <img :src="user.img" alt="">
                <span>{{user.username}}</span>
              </li>
            </ul>
            <p v-if='userInfo.length==0'>当前无人在线哟~</p>
          </div>
        </div>
      </div>
    </div>
    <script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
    <script src='js/client.js'></script>
  </body>

</html>

server.js

const express = require('express');
const app = express();1
const http = require('http').Server(app);
const io = require('socket.io')(http);
const port = process.env.PORT || 5000;

let users = [];//存储登录的用户
let userInfo = [];//存储用户姓名和头像

app.use('/',express.static(__dirname+'/static'));
app.get('/', function (req, res) {
  res.sendFile(__dirname + '/index.html');
});

io.on('connection', function (socket) {
  console.log('连接成功');
  socket.on('login', function (user) {
    const {username} = user;
    console.log(users.indexOf(username))

    if (users.indexOf(username)>-1){            
      socket.emit('loginError');

    }else{
      // 存储用户名
      users.push(username);
      userInfo.push(user);
      io.emit('loginSuc');
      socket.nickName = username;
      // 系统通知
      io.emit('system', {
        name: username,
        status: '进入'
      })

      // 显示在线人员
      io.emit('disUser',userInfo);
      console.log('一个用户登录');

    }

  });
  // 发送窗口事件
  socket.on('shake',()=>{
    socket.emit('shake',{
      name:'您'
    })
    // 广播消息
    socket.broadcast.emit('shake',{
      name:socket.nickName
    })
  });
  // 发送消息事件
  socket.on('sendMsg',(data)=>{

    let img = '';
    for(let i = 0; i < userInfo.length;i++){
      if(userInfo[i].username === socket.nickName){
        img = userInfo[i].img;
      }
    }
    // 广播
    socket.broadcast.emit('receiveMsg', {
      name: socket.nickName,
      img: img,
      msg: data.msgVal,
      color: data.color,
      type: data.type,
      side: 'left',
      isUser:true
    });
    socket.emit('receiveMsg', {
      name: socket.nickName,
      img: img,
      msg: data.msgVal,
      color: data.color,
      type: data.type,
      side: 'right',
      isUser: true
    });

  })

  // 断开连接时
  socket.on('disconnect',()=>{
    console.log('断开连接');

    let index = users.indexOf(socket.nickName);        
    if(index > -1){
      users.splice(index,1);//删除用户信息
      userInfo.splice(index,1);//删除用户信息

      io.emit('system',{
        name:socket.nickName,
        status:'离开'
      })
      io.emit('disUser,userInfo'); //重新渲染
      console.log('一个用户离开');

    }
  })

});


http.listen(3000, () => {
  console.log('listen on 3000端口');
})

client.js


const vm = new Vue({
  el: '#app',
  data() {
    return {
      username: '',
      msgVal: '',
      isShow: true,
      nowDate: new Date().toTimeString().substr(0, 8),
      userHtml: '',
      userInfo: [],
      isShake: false,
      timer: null,
      userSystem: [],
      color: '#000000',
      emojis: [],
      isEmojiShow: false
    }
  },
  methods: {
    handleClick() {
      var imgN = Math.floor(Math.random() * 4) + 1; // 随机分配头像
      if (this.username) {
        this.socket.emit('login', {
          username: this.username,
          img: 'image/user' + imgN + '.jpg'
        });
      }

    },
    shake() {
      this.isShake = true;
      clearTimeout(this.timer);
      this.timer = setTimeout(() => {
        this.isShake = false;
      }, 500);
    },
    handleShake(e) {
      this.socket.emit('shake');
    },
    // 发送消息
    handleSendMsg(e) {
      e.preventDefault();
      if (this.msgVal) {
        this.socket.emit('sendMsg', {
          msgVal: this.msgVal,
          color: this.color,
          type: 'text'
        })
        this.msgVal = '';
      }
    },
    scrollBottom() {
      this.$nextTick(() => {
        const div = document.getElementById('messages');
        div.scrollTop = div.scrollHeight;
      })
    },
    initEmoji() {
      for (let i = 0; i < 141; i++) {
        this.emojis.push(`image/emoji/emoji (${i + 1}).png`);
      }
    },
    // 点击微笑 弹出表情
    handleSelectEmoji() {
      this.isEmojiShow = true;
    },
    handleDoubleSelectEmoji() {
      this.isEmojiShow = false;
    },
    // 用户点击发送表情
    handleEmojiImg(index) {
      this.isEmojiShow = false;
      this.msgVal = this.msgVal + `[emoji${index}]`;
    },
    handleLogout(e) {
      e.preventDefault();
      this.socket.emit('disconnect')
    }
  },
  created() {
    const socket = io();
    this.socket = socket;
    this.socket.on('loginSuc', () => {
      this.isShow = false;
    });
    this.socket.on('loginError', () => {
      alert('用户名已存在,请重新输入');
      this.username = '';
    })
    // 系统提示消息
    this.socket.on('system', (user) => {
      console.log(user);
      this.userSystem.push(user);
      this.scrollBottom()
    })
    // 显示在线人员
    this.socket.on('disUser', (userInfo) => {
      this.userInfo = userInfo;
    })
    //监听抖动事件
    this.socket.on('shake', (user) => {
      this.userSystem.push(user);
      this.shake();
      this.scrollBottom();
    })
    this.socket.on('receiveMsg', (obj) => {
      let msg = obj.msg;
      let content = ''
      if (obj.type === 'img') {
      }
      // 提取文字中的表情加以渲染
      while (msg.indexOf('[') > -1) {  // 其实更建议用正则将[]中的内容提取出来
        var start = msg.indexOf('[');
        var end = msg.indexOf(']');
        content += '<span>' + msg.substr(0, start) + '</span>';
        content += '<img src="image/emoji/emoji%20(' + msg.substr(start + 6, end - start - 6) + ').png">';
        msg = msg.substr(end + 1, msg.length);
      }
      content += '<span>' + msg + '</span>';
      obj.msg = content;
      this.userSystem.push(obj);
      // 滚动条总是在最底部
      this.scrollBottom();
    })
    // 渲染表情
    this.initEmoji();
  }

})