如何用socket.io打造一个web聊天应用

socket.io是什么?

socket.io封装了websocket,同时包含了其它的连接方式,比如Ajax。
原因在于不是所有的浏览器都支持websocket,通过socket.io的封装,你不用关心里面用了什么连接方式。
你在任何浏览器里都可以使用socket.io来建立异步的连接。socket.io包含了服务端和客户端的库,
如果在浏览器中使用了socket.io的js,服务端也必须同样适用。

用到的socket.io服务端api

Using with Express

var app = require('express')();
var server = require('http').Server(app);
var io = require('socket.io')(server);

server.listen(80);
// WARNING: app.listen(80) will NOT work here!

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

io.on('connection', function (socket) {
  socket.emit('news', { hello: 'world' });
  socket.on('my other event', function (data) {
    console.log(data);
  });
});

socket.io事件速查

io.on('connect', onConnect);

function onConnect(socket){

  // 发送给当前客户端
  socket.emit('hello', 'can you hear me?', 1, 2, 'abc');

  // 发送给所有客户端,除了发送者
  socket.broadcast.emit('broadcast', 'hello friends!');

  // 发送给同在 'game' 房间的所有客户端,除了发送者
  socket.to('game').emit('nice game', "let's play a game");

  // 发送给同在 'game1' 或 'game2' 房间的所有客户端,除了发送者
  socket.to('game1').to('game2').emit('nice game', "let's play a game (too)");

  // 发送给同在 'game' 房间的所有客户端,包括发送者
  io.in('game').emit('big-announcement', 'the game will start soon');

  // 发送给同在 'myNamespace' 命名空间下的所有客户端,包括发送者
  io.of('myNamespace').emit('bigger-announcement', 'the tournament will start soon');

  // 发送给指定 socketid 的客户端(私密消息)
  socket.to().emit('hey', 'I just met you');

  // 包含回执的消息
  socket.emit('question', 'do you think so?', function (answer) {});

  // 不压缩,直接发送
  socket.compress(false).emit('uncompressed', "that's rough");

  // 如果客户端还不能接收消息,那么消息可能丢失
  socket.volatile.emit('maybe', 'do you really need it?');

  // 发送给当前 node 实例下的所有客户端(在使用多个 node 实例的情况下)
  io.local.emit('hi', 'my lovely babies');

};

服务端代码

const express = require('express'),
  app = express(),
  server = require('http').createServer(app),
  io = require('socket.io').listen(server),
  //用于保存用户信息的数组
  users = [];
let kit = {
  //判断用户是否存在
  isHaveUser(user) {
    var flag = false;
    users.forEach(function (item) {
      if (item.name == user.name) {
        flag = true;
      }
    })
    return flag;
  },
  //删除某一用户
  delUser(id) {
    users.forEach(function (item, index) {
      if (item.id == id) {
        users.splice(index, 1);
      }
    })
  }
}
//设置静态资源
app.use('/static', express.static(__dirname + '/static'));
//用户访问网站页面会根据浏览器userAgent返回不同的页面
app.get("/", (req, res) => {
    let path = __dirname + '/static/index.html';
    res.sendFile(path);
})
io.sockets.on('connection', function (socket) {
  //创建用户链接
  socket.on('login', (user)=> {
    if (kit.isHaveUser(user)) {
      console.log("登录失败!", user)
      socket.emit('loginFail', "登录失败,昵称已存在!");
    } else {
      socket.user = user;
      user.id = socket.id;
      user.address = socket.handshake.address;
      console.log("登录成功!", user)
      socket.emit('loginSuccess', user, users);
      users.push(user)
      socket.broadcast.emit('system', user, 'join');
    }
    ;
  });
  //用户注销链接
  socket.on('disconnect',()=> {
    if (socket.user != null) {
      kit.delUser(socket.id);
      console.log("用户退出!", socket.user)
      socket.broadcast.emit('system', socket.user, 'logout');
    }
  });
  //群发消息
  socket.on('groupMessage', function (msg, from) {
    //用户登录状态掉线,重置用户登录状态
    if (!socket.user) {
      from.id = socket.id;
      socket.user = from;
      users.push(from);
      socket.broadcast.emit('system', from, 'join');
      socket.emit('loginSuccess', from, []);
    }
    socket.broadcast.emit('groupMessage', socket.user, msg);
  });
  //发送私信
  socket.on('message', function (id, msg, from) {
    //用户登录状态掉线,重置用户登录状态
    if (!socket.user) {
      from.id = socket.id;
      socket.user = from;
      users.push(from);
      socket.broadcast.emit('system', from, 'join');
      socket.emit('loginSuccess', from, []);
    }
    socket.broadcast.to(id).emit('message', socket.user, msg);
  });
});
//启动服务器
server.listen(3000, function () {
  console.log("服务器已启动在:3000端口", "http://localhost:3000")
});

客户端代码

//创建socket链接
var socket = io.connect();
//登录组件
Vue.component("ui-login", {
  template: "#imLogin",
  data: function () {
    //可供用户登录时选择对的头像,可在此扩展
    var images = [
      'http://q.qlogo.cn/headimg_dl?dst_uin=705597001&spec=100',
      'http://q.qlogo.cn/headimg_dl?dst_uin=956411241&spec=100',
      'http://q.qlogo.cn/headimg_dl?dst_uin=1361514346&spec=100',
      'http://q.qlogo.cn/headimg_dl?dst_uin=624748513&spec=100',
      'http://q.qlogo.cn/headimg_dl?dst_uin=1741841217&spec=100',
      'http://q.qlogo.cn/headimg_dl?dst_uin=157509895&spec=100',
      'http://q.qlogo.cn/headimg_dl?dst_uin=453079985&spec=100',
      'http://q.qlogo.cn/headimg_dl?dst_uin=753678776&spec=100',
    ]
    return {
      name: "",
      isShow: false,
      avatarUrl: images[0],
      images: images,
      errorMsg: ""
    }
  },
  created: function () {
    var _this = this;
    document.addEventListener("click", function (e) {
      _this.isShow = false;
    })
    _this.initSocketEvent();
  },
  methods: {
    //用户登录
    userLogin: function () {
      var _this = this;
      var name = _this.trim(_this.name);
      if (name != "") {
        socket.emit("login", {
          name: name,
          avatarUrl: this.avatarUrl
        })
      } else {
        _this.name = "";
        this.showError("请输入用户昵称!")
      }
    },
    //显示错误信息
    showError: function (err) {
      var _this = this;
      if (this.interval) {
        clearTimeout(_this.interval)
      }
      this.errorMsg = err;
      this.interval = setTimeout(function () {
        _this.errorMsg = "";
      }, 3000)
    },
    //初始化socket监听事件
    initSocketEvent: function () {
      var _this = this;
      socket.on("loginSuccess", function (user, users) {
        _this.$emit("user-login", {
          user: user,
          users: users
        })
      })
      socket.on("loginFail", function (msg) {
        _this.showError(msg)
      })
    },
    //去除空格
    trim: function (string) {
      return string.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
    }
  }
})
new Vue({
  el: "#webChatBox",
  template: "#webChat",
  data: function () {
    return {
      //登录用户信息
      loginUser: {
        id: "u001",
        avatarUrl: "http://q.qlogo.cn/headimg_dl?dst_uin=705597001&spec=100",
        name: "似水流年",
        type: "user"
      },
      //设置tab选项
      tab: "chat",
      //在线会话用户列表
      users: [
        {
          id: "group",
          avatarUrl: "/static/images/group-icon.png",
          name: "聊天室群",
          type: "room"
        }
      ],
      //会话对象,用于保存用户聊天消息
      threads: {},
      //默认选中的频道ID
      channelId: "group",
      //输入框的内容
      text: "",
      //设置
      setting: {
        isShowTime: true,
        isVoice: true,
        isShowName: true
      },
      //在线用户筛选的关键词
      keyWord: "",
      //用户是否登录的标识符
      isLogin: false
    }
  },
  computed: {
    //当前频道的消息列表
    messages: function () {
      var messages = [];
      if (this.threads[this.channelId]) {
        messages = this.threads[this.channelId];
      }
      return messages;
    },
    //当前频道的信息
    channel: function () {
      var user = {}, _this = this;
      this.users.forEach(function (item) {
        if (_this.channelId == item.id) {
          user = item
        }
      })
      return user;
    }
  },
  filters: {
    //格式化时间显示
    time: function (value) {
      function two(str) {
        var s;
        s = "" + str;
        if (s.length === 1) {
          s = "0" + s;
        }
        return s;
      };
      var time = new Date(value);
      var hour = time.getHours();
      var m = time.getMinutes();
      var s = time.getSeconds();
      return two(hour) + ":" + two(m) + ":" + two(s);
    }
  },
  methods: {
    //发送消息
    sendMessage: function (text, to) {
      var isRead = this.channelId == to.id ? true : false;
      var message = {
        threadId: to.id,
        from: this.loginUser,
        to: to,
        content: text,
        time: new Date().getTime(),
        type: "send",
        isRead: isRead
      }
      this.addMessage(message)
    },
    //接收消息
    receiveMessage: function (text, from, channelId) {
      var isRead = this.channelId == channelId ? true : false;
      var message = {
        threadId: channelId,
        from: from,
        to: this.loginUser,
        content: text,
        time: new Date().getTime(),
        type: "receive",
        isRead: isRead
      }
      this.addMessage(message)
      if (this.setting.isVoice && channelId != "group") {
        this.$refs.audio.play();
      }
    },
    //添加消息
    addMessage: function (message) {
      var _this = this;
      if (!this.threads[message.threadId]) {
        this.$set(this.threads, message.threadId, [])
      }
      this.threads[message.threadId].push(message)
      this.$nextTick(function () {
        _this.scrollFooter()
      })
    },
    //发送
    send: function () {
      var text = this.trim(this.text);
      if (text != "" && this.isLogin) {
        this.sendMessage(text, this.channel);
        if (this.channelId == "group") {
          socket.emit("groupMessage", text, this.loginUser)
        } else {
          socket.emit("message", this.channelId, text, this.loginUser)
        }
      }
      this.text = "";
    },
    //消息栏滚动到底部
    scrollFooter: function () {
      var ul = this.$refs.list;
      ul.scrollTop = ul.scrollHeight;
    },
    //获取某一频道最后一条消息
    getLastMsg: function (id) {
      var message = {};
      var messgaes = this.threads[id];
      if (messgaes && (messgaes.length > 0)) {
        message = messgaes[messgaes.length - 1];
      }
      return message;
    },
    //获取某一频道未读消息的条数
    getUnReaderNum: function (id) {
      var num = 0;
      var messgaes = this.threads[id];
      if (messgaes && (messgaes.length > 0)) {
        messgaes.forEach(function (item) {
          if (!item.isRead) {
            num++;
          }
        })
      }
      return num;
    },
    //选择表情
    picker: function (expression) {
      this.text += expression.title;
    },
    //去除空格
    trim: function (string) {
      return string.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
    },
    //本地查找用户
    searchUser: function () {
      var arr = [], _this = this;
      this.users.forEach(function (item) {
        if ((item.name.indexOf(_this.keyWord) != -1) || (item.id.indexOf(_this.keyWord) != -1)) {
          arr.push(item)
        }
      })
      return arr;
    },
    //切换频道
    changeChannel: function (channelId) {
      var _this = this;
      this.channelId = channelId;
      document.querySelector("title").innerHTML = _this.loginUser.name + " | 与" + this.channel.name + "聊天中";
      _this.setMessageReader(channelId);
      this.$nextTick(function () {
        _this.scrollFooter()
      })
    },
    //设置某一频道所有消息为已读
    setMessageReader: function (id) {
      var messgaes = this.threads[id], _this = this;
      if (messgaes && (messgaes.length > 0)) {
        messgaes.forEach(function (item, index) {
          if (!item.isRead) {
            _this.threads[id][index].isRead = true;
          }
        })
      }
    },
    //处理用户登录
    userLogin: function (payload) {
      this.loginUser = payload.user;
      if (!this.isLogin) {
        this.isLogin = true;
        document.querySelector("title").innerHTML = this.loginUser.name + " | 与" + this.channel.name + "聊天中";
        this.users = this.users.concat(payload.users);
        this.initSocketEvent();
      }
    },
    //初始化消息监听事件
    initSocketEvent: function () {
      var _this = this;
      socket.on('system', function (user, type) {
        if (type == "join") {
          user.messages = []
          _this.users.push(user)
        }
        if (type == "logout") {
          _this.users.forEach(function (item, index) {
            if (item.id == user.id) {
              _this.users.splice(index, 1);
            }
          })
          if (user.id == _this.channelId) {
            _this.changeChannel("group");
          }
        }
      })
      socket.on("message", function (user, text) {
        _this.receiveMessage(text, user, user.id)
      })
      socket.on("groupMessage", function (user, text) {
        _this.receiveMessage(text, user, "group")
      })
    }
  }
})

客户端用到的表情插件是自己写的这里与客户端的交互逻辑没有关系就不在贴出来了,具体可以到我github中看具体代码,页面结构和样式。

功能说明

[✔] 支持表情发送
[✔] 支持键盘回车发送信息
[✔] 支持在线用户统计和用户列表
[✔] 支持在线用户搜索
[✔] 支持在线用户的私聊
[✔] 支持消息声音提示
[✔] 支持消息显示设置
[✔] 未读消息条数显示
*注:用户可选择的头像用的是QQ的头像,可以在登录组件中做修改,用户登录只需填写用户名,选择头像后即可登录。用户名不能重复。

项目预览截图

screen_01.png
screen_02.png
screen_03.png
screen_04.png
screen_05.png

相关技术站点


Vue官方文档
Socket.io官方文档

总结

socket.io的API比较简单,这里只用到了一些常用的API,客户端界面和交互逻辑用的是Vue做的。服务器端只对用户的登录信息做了保存,用户的聊天记录没有做保存。暂时没有做用户退出功能。可以通过关闭页面的方式退出。

后记

项目地址:https://github.com/cleverqin/node-websocket-Chatroom

如果你觉得该项目不错欢迎Star,你的支持是我前进最大的动力。
如果你有任何问题欢迎留言,或者邮件联系我。
邮箱地址:[email protected]

你可能感兴趣的:(如何用socket.io打造一个web聊天应用)