美文网首页node.jsWeb前端之路前端开发那些事
Node.js+socket.io即时通讯+HTML5桌面提醒(

Node.js+socket.io即时通讯+HTML5桌面提醒(

作者: 㱎䖘䵈䶁䘔䶑䘓鋱䩳䵷㒪䪉䉥 | 来源:发表于2017-01-31 17:19 被阅读563次

    接着上次的文章
    Node.js+socket.io即时通讯+HTML5桌面提醒(notification(上)
    写的下篇:

    1.打开node_modules文件,打开socket.io文件,找到socket.io.js,在package.json所在的目录,新建www文件夹.
    2.在www文件夹里,建立自己的 index.html ,css样式,js文件,以及socket.io.js,文件目录如下:

    Snip20170131_8.png Snip20170131_9.png

    3.文件内容为:
    index.html:

    <!doctype html>
    <html>
        <head>
            <meta charset="utf-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
            <meta name="viewport" content="width=device-width, initial-scale=1">
            <title>hichat</title>
            <link rel="stylesheet" href="styles/main.css">
            <link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
            <link rel="icon" href="favicon.ico" type="image/x-icon">
        </head>
        <body>
            <div class="wrapper">
                <div class="banner">
                    <h1>HiChat :)</h1>
                    <span id="status"></span>
                </div>
                <div id="historyMsg">
                </div>
                <div class="controls" >
                    <div class="items">
                        <input id="colorStyle" type="color" placeHolder='#000' title="font color" />
                        <input id="emoji" type="button" value="emoji" title="emoji" />
                        <label for="sendImage" class="imageLable">
                            <input type="button" value="image"  />
                            <input id="sendImage" type="file" value="image"/>
                        </label>
                        <input id="clearBtn" type="button" value="clear" title="clear screen" />
                        <input id="notifBtn" type="button" value="通知" title="Notification">
                    </div>
                    <textarea id="messageInput" placeHolder="enter to send"></textarea>
                    <input id="sendBtn" type="button" value="SEND">
                    <div id="emojiWrapper">
                    </div>
                </div>
            </div>
            <div id="loginWrapper">
                <p id="info">connecting to server...</p>
                <div id="nickWrapper">
                    <input type="text" placeHolder="nickname" id="nicknameInput" />
                    <input type="button" value="OK" id="loginBtn" />
                </div>
            </div>
            <script src="/socket.io/socket.io.js"></script>
            <script src="scripts/hichat.js"></script>
        </body>
    </html>
    

    main.css

    html, body {
        margin: 0;
        background-color: #efefef;
        font-family: sans-serif;
    }
    .wrapper {
        width: 500px;
        height: 640px;
        padding: 5px;
        margin: 0 auto;
        background-color: #ddd;
    }
    #loginWrapper {
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        background-color: rgba(5, 5, 5, .6);
        text-align: center;
        color: #fff;
        display: block;
        padding-top: 200px;
    }
    #nickWrapper {
        display: none;
    }
    .banner {
        height: 80px;
        width: 100%;
    }
    .banner p {
        float: left;
        display: inline-block;
    }
    .controls {
        height: 100px;
        margin: 5px 0px;
        position: relative;
    }
    #historyMsg {
        height: 400px;
        background-color: #fff;
        overflow: auto;
        padding: 2px;
    }
    #historyMsg img {
        max-width: 99%;
    }
    .timespan {
        color: #ddd;
    }
    .items {
        height: 30px;
    }
    #colorStyle {
        width: 50px;
        border: none;
        padding: 0;
    }
    /*custom the file input*/
    
    .imageLable {
        position: relative;
    }
    #sendImage {
        position: absolute;
        width: 52px;
        left: 0;
        opacity: 0;
        overflow: hidden;
    }
    /*end custom file input*/
    
    #messageInput {
        width: 440px;
        max-width: 440px;
        height: 90px;
        max-height: 90px;
        float:left;
    }
    #sendBtn {
        width: 50px;
        height: 96px;
        float: right;
    }
    #emojiWrapper {
        display: none;
        width: 500px;
        bottom: 105px;
        position: absolute;
        background-color: #aaa;
        box-shadow: 0 0 10px #555;
    }
    #emojiWrapper img {
        margin: 2px;
        padding: 2px;
        width: 25px;
        height: 25px;
    }
    #emojiWrapper img:hover {
        background-color: blue;
    }
    .emoji{
        display: inline;
    }
    footer {
        text-align: center;
    }
    

    main.js

    window.onload = function() {
        var hichat = new HiChat();
        hichat.init();
        if(window.Notification){
                if(Notification.permission === 'granted'){
                }else{
                    Notification.requestPermission();
                }
            }
    };
    var HiChat = function() {
        this.socket = null;
    };
    HiChat.prototype = {
        init: function() {
            var that = this;
            this.socket = io.connect();
            this.socket.on('connect', function() {
                document.getElementById('info').textContent = 'get yourself a nickname :)';
                document.getElementById('nickWrapper').style.display = 'block';
                document.getElementById('nicknameInput').focus();
            });
            this.socket.on('nickExisted', function() {
                document.getElementById('info').textContent = '!nickname is taken, choose another pls';
            });
            this.socket.on('loginSuccess', function() {
                document.title = 'hichat | ' + document.getElementById('nicknameInput').value;
                document.getElementById('loginWrapper').style.display = 'none';
                document.getElementById('messageInput').focus();
            });
            this.socket.on('error', function(err) {
                if (document.getElementById('loginWrapper').style.display == 'none') {
                    document.getElementById('status').textContent = '!fail to connect :(';
                } else {
                    document.getElementById('info').textContent = '!fail to connect :(';
                }
            });
            this.socket.on('system', function(nickName, userCount, type) {
                var msg = nickName + (type == 'login' ? ' joined' : ' left');
                that._displayNewMsg('system ', msg, 'red');
                document.getElementById('status').textContent = userCount + (userCount > 1 ? ' users' : ' user') + ' online';
            });
            this.socket.on('newMsg', function(user, msg, color) {
                that._displayNewMsg(user, msg, color);
            });
            this.socket.on('newImg', function(user, img, color) {
                that._displayImage(user, img, color);
            });
    
            this.socket.on('notification',function(user,config){
                that._consoleText(user,config);
            });
    
            document.getElementById('loginBtn').addEventListener('click', function() {
                var nickName = document.getElementById('nicknameInput').value;
                if (nickName.trim().length != 0) {
                    that.socket.emit('login', nickName);
                } else {
                    document.getElementById('nicknameInput').focus();
                };
            }, false);
            document.getElementById('nicknameInput').addEventListener('keyup', function(e) {
                if (e.keyCode == 13) {
                    var nickName = document.getElementById('nicknameInput').value;
                    if (nickName.trim().length != 0) {
                        that.socket.emit('login', nickName);
                    };
                };
            }, false);
            document.getElementById('sendBtn').addEventListener('click', function() {
                var messageInput = document.getElementById('messageInput'),
                    msg = messageInput.value,
                    color = document.getElementById('colorStyle').value;
                messageInput.value = '';
                messageInput.focus();
                if (msg.trim().length != 0) {
                    that.socket.emit('postMsg', msg, color);
                    that._displayNewMsg('me', msg, color);
                    return;
                };
            }, false);
            document.getElementById('messageInput').addEventListener('keyup', function(e) {
                var messageInput = document.getElementById('messageInput'),
                    msg = messageInput.value,
                    color = document.getElementById('colorStyle').value;
                if (e.keyCode == 13 && msg.trim().length != 0) {
                    messageInput.value = '';
                    that.socket.emit('postMsg', msg, color);
                    that._displayNewMsg('me', msg, color);
                };
            }, false);
            document.getElementById('clearBtn').addEventListener('click', function() {
                document.getElementById('historyMsg').innerHTML = '';
            }, false);
            document.getElementById('sendImage').addEventListener('change', function() {
                if (this.files.length != 0) {
                    var file = this.files[0],
                        reader = new FileReader(),
                        color = document.getElementById('colorStyle').value;
                    if (!reader) {
                        that._displayNewMsg('system', '!your browser doesn\'t support fileReader', 'red');
                        this.value = '';
                        return;
                    };
                    reader.onload = function(e) {
                        this.value = '';
                        that.socket.emit('img', e.target.result, color);
                        that._displayImage('me', e.target.result, color);
                    };
                    reader.readAsDataURL(file);
                };
            }, false);
            this._initialEmoji();
            document.getElementById('emoji').addEventListener('click', function(e) {
                var emojiwrapper = document.getElementById('emojiWrapper');
                emojiwrapper.style.display = 'block';
                e.stopPropagation();
            }, false);
            document.body.addEventListener('click', function(e) {
                var emojiwrapper = document.getElementById('emojiWrapper');
                if (e.target != emojiwrapper) {
                    emojiwrapper.style.display = 'none';
                };
            });
    document.getElementById('emojiWrapper').addEventListener('click', function(e) {
                var target = e.target;
                if (target.nodeName.toLowerCase() == 'img') {
                    var messageInput = document.getElementById('messageInput');
                    messageInput.focus();
                    messageInput.value = messageInput.value + '[emoji:' + target.title + ']';
                };
            }, false);  document.getElementById('notifBtn').addEventListener('click',function(){
              var config = {
                              body: '我发起了一个描述~', //正文内容
                              dir: 'auto',//文本显示方向 auto ,ltr,rtl
                              lang:'en', //文本语言
                              icon: 'http://img4.imgtn.bdimg.com/it/u=196405673,1932175745&fm=23&gp=0.jpg' //图片的URL,将被用于显示通知的图标
                          };
              that.socket.emit('noti',config);
              return;
            }),false;
        },
        _initialEmoji: function() {
            var emojiContainer = document.getElementById('emojiWrapper'),
                docFragment = document.createDocumentFragment();
            for (var i = 69; i > 0; i--) {
                var emojiItem = document.createElement('img');
                emojiItem.src = '../content/emoji/' + i + '.gif';
                emojiItem.title = i;
                docFragment.appendChild(emojiItem);
            };
            emojiContainer.appendChild(docFragment);
        },
        _displayNewMsg: function(user, msg, color) {
            var container = document.getElementById('historyMsg'),
                msgToDisplay = document.createElement('p'),
                date = new Date().toTimeString().substr(0, 8),
                //determine whether the msg contains emoji
                msg = this._showEmoji(msg);
            msgToDisplay.style.color = color || '#000';
            msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span>' + msg;
            container.appendChild(msgToDisplay);
            container.scrollTop = container.scrollHeight;
        },
        _displayImage: function(user, imgData, color) {
            var container = document.getElementById('historyMsg'),
                msgToDisplay = document.createElement('p'),
                date = new Date().toTimeString().substr(0, 8);
            msgToDisplay.style.color = color || '#000';
            msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span> <br/>' + '<a href="' + imgData + '" target="_blank">![](' + imgData + ')</a>';
            container.appendChild(msgToDisplay);
            container.scrollTop = container.scrollHeight;
        },
        _showEmoji: function(msg) {
            var match, result = msg,
                reg = /\[emoji:\d+\]/g,
                emojiIndex,
                totalEmojiNum = document.getElementById('emojiWrapper').children.length;
            while (match = reg.exec(msg)) {
                emojiIndex = match[0].slice(7, -1);
                if (emojiIndex > totalEmojiNum) {
                    result = result.replace(match[0], '[X]');
                } else {
                    result = result.replace(match[0], '![](../content/emoji/' + emojiIndex + '.gif)');//todo:fix this in chrome it will cause a new request for the image
                };
            };
            return result;
        },
        _consoleText: function(user,config){
          if(window.Notification){
                  if(Notification.permission === 'granted'){
                      var notification = new Notification(user,config);
                      delete notification;
                  }else{
                      Notification.requestPermission();
                  }
              }
        }
    };
    
    1. socket.io
      我们要求连接的用户需要首先设置一个昵称,且这个昵称还要唯一,也就是不能与别人同名。一是方便用户区分,二是为了统计在线人数,同时也方便维护一个保存所有用户昵称的数组。

    为此在后台server.js中,我们创建一个名叫users的全局数组变量,当一个用户设置好昵称发送到服务器的时候,将昵称压入users数组。同时注意,如果用户断线离开了,也要相应地从users数组中移除以保证数据的正确性。

    在前台,输入昵称点击OK提交后,我们需要发起一个设置昵称的事件以便服务器侦听到。将以下代码添加到之前的init方法中。

    //昵称设置的确定按钮
    document.getElementById('loginBtn').addEventListener('click', function() {
        var nickName = document.getElementById('nicknameInput').value;
        //检查昵称输入框是否为空
        if (nickName.trim().length != 0) {
            //不为空,则发起一个login事件并将输入的昵称发送到服务器
            that.socket.emit('login', nickName);
        } else {
            //否则输入框获得焦点
            document.getElementById('nicknameInput').focus();
        };
    }, false);
    

    服务器端:

    //socket部分
    io.on('connection', function(socket) {
        //昵称设置
        socket.on('login', function(nickname) {
            if (users.indexOf(nickname) > -1) {
                socket.emit('nickExisted');
            } else {
                socket.userIndex = users.length;
                socket.nickname = nickname;
                users.push(nickname);
                socket.emit('loginSuccess');
                io.sockets.emit('system', nickname); //向所有连接到服务器的客户端发送当前登陆用户的昵称 
            };
        });
    });
    
    在线统计

    这里实现显示在线用户数及在聊天主界面中以系统身份显示用户连接离开等信息。

    上面server.js中除了loginSuccess事件,后面还有一句代码,通过io.sockets.emit 向所有用户发送了一个system事件,传递了刚登入用户的昵称,所有人接收到这个事件后,会在聊天窗口显示一条系统消息'某某加入了聊天室'。同时考虑到在前端我们无法得知用户是进入还是离开,所以在这个system事件里我们多传递一个数据来表明用户是进入还是离开。

    将server.js中login事件更改如下:

    server.js

    socket.on('login', function(nickname) {
         if (users.indexOf(nickname) > -1) {
             socket.emit('nickExisted');
         } else {
             socket.userIndex = users.length;
             socket.nickname = nickname;
             users.push(nickname);
             socket.emit('loginSuccess');
             io.sockets.emit('system', nickname, users.length, 'login');
         };
     });
    

    较之前,多传递了一个login字符串。

    同时再添加一个用户离开的事件,这个可能通过socket.io自带的disconnect事件完成,当一个用户断开连接,disconnect事件就会触发。在这个事件中,做两件事情,一是将用户从users数组中删除,一是发送一个system事件通知所有人'某某离开了聊天室'。

    将以下代码添加到server.js中connection的回调函数中。

    server.js

    //断开连接的事件
    socket.on('disconnect', function() {
        //将断开连接的用户从users中删除
        users.splice(socket.userIndex, 1);
        //通知除自己以外的所有人
        socket.broadcast.emit('system', socket.nickname, users.length, 'logout');
    });
    
    发送消息

    用户连接以及断开我们需要显示系统消息,用户还要频繁的发送聊天消息,所以可以考虑将消息显示到页面这个功能单独写一个函数方便我们调用。为此我们向HiChat类中添加一个_displayNewMsg的方法,它接收要显示的消息,消息来自谁,以及一个颜色共三个参数。因为我们想系统消息区别于普通用户的消息,所以增加一个颜色参数。同时这个参数也方便我们之后实现让用户自定义文本颜色做准备。

    将以下代码添加到的我的HiChat类当中。

    //向原型添加业务方法
    HiChat.prototype = {
        init: function() { //此方法初始化程序
            //...
        },
        _displayNewMsg: function(user, msg, color) {
            var container = document.getElementById('historyMsg'),
                msgToDisplay = document.createElement('p'),
                date = new Date().toTimeString().substr(0, 8);
            msgToDisplay.style.color = color || '#000';
            msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span>' + msg;
            container.appendChild(msgToDisplay);
            container.scrollTop = container.scrollHeight;
        }
    };
    

    在_displayNewMsg方法中,我们还向消息添加了一个日期。我们也判断了该方法在调用时有没有传递颜色参数,没有传递颜色的话默认使用#000即黑色。

    同时修改我们在system事件中显示系统消息的代码,让它调用这个_displayNewMsg方法。

    this.socket.on('system', function(nickName, userCount, type) {
        var msg = nickName + (type == 'login' ? ' joined' : ' left');
        //指定系统消息显示为红色
        that._displayNewMsg('system ', msg, 'red');
        document.getElementById('status').textContent = userCount + (userCount > 1 ? ' users' : ' user') + ' online';
    });
    

    有了这个显示消息的方法后,下面就开始实现用户之间的聊天功能了。

    做法也很简单,如果你掌握了上面所描述的emit发送事件,on接收事件,那么用户聊天消息的发送接收也就轻车熟路了。

    首先为页面的发送按钮写一个click事件处理程序,我们通过addEventListner来监听这个click事件,当用户点击发送的时候,先检查输入框是否为空,如果不为空,则向服务器发送postMsg事件,将用户输入的聊天文本发送到服务器,由服务器接收并分发到除自己外的所有用户。

    将以下代码添加到main.js的inti方法中。

    document.getElementById('sendBtn').addEventListener('click', function() {
        var messageInput = document.getElementById('messageInput'),
            msg = messageInput.value;
        messageInput.value = '';
        messageInput.focus();
        if (msg.trim().length != 0) {
            that.socket.emit('postMsg', msg); //把消息发送到服务器
            that._displayNewMsg('me', msg); //把自己的消息显示到自己的窗口中
        };
    }, false);
    

    在server.js中添加代码以接收postMsg事件。

    io.on('connection', function(socket) {
        //其他代码。。。
    
        //接收新消息
        socket.on('postMsg', function(msg) {
            //将消息发送到除自己外的所有用户
            socket.broadcast.emit('newMsg', socket.nickname, msg);
        });
    });
    

    然后在客户端接收服务器发送的newMsg事件,并将聊天消息显示到页面。

    将以下代码显示添加到main.js的init方法中了。

    this.socket.on('newMsg', function(user, msg) {
        that._displayNewMsg(user, msg);
    });
    
    发送图片

    图片不同于文字,但通过将图片转化为字符串形式后,便可以像发送普通文本消息一样发送图片了,只是在显示的时候将它还原为图片。

    在这之前,我们已经将图片按钮在页面放好了,其实是一个文件类型的input,下面只需在它身上做功夫便可。

    用户点击图片按钮后,弹出文件选择窗口供用户选择图片。之后我们可以在JavaScript代码中使用FileReader来将图片读取为base64格式的字符串形式进行发送。而base64格式的图片直接可以指定为图片的src,这样就可以将图片用img标签显示在页面了。

    为此我们监听图片按钮的change事件,一但用户选择了图片,便显示到自己的屏幕上同时读取为文本发送到服务器。

    将以下代码添加到main.js的init方法中。

    document.getElementById('sendImage').addEventListener('change', function() {
        //检查是否有文件被选中
         if (this.files.length != 0) {
            //获取文件并用FileReader进行读取
             var file = this.files[0],
                 reader = new FileReader();
             if (!reader) {
                 that._displayNewMsg('system', '!your browser doesn\'t support fileReader', 'red');
                 this.value = '';
                 return;
             };
             reader.onload = function(e) {
                //读取成功,显示到页面并发送到服务器
                 this.value = '';
                 that.socket.emit('img', e.target.result);
                 that._displayImage('me', e.target.result);
             };
             reader.readAsDataURL(file);
         };
     }, false);
    

    上面图片读取成功后,调用_displayNImage方法将图片显示在自己的屏幕同时向服务器发送了一个img事件,在server.js中,我们通过这个事件来接收并分发图片到每个用户。同时也意味着我们还要在前端写相应的代码来接收。

    这个_displayNImage还没有实现,将会在下面介绍。

    将以下代码添加到server.js的socket回调函数中。

    server.js

    //接收用户发来的图片
     socket.on('img', function(imgData) {
        //通过一个newImg事件分发到除自己外的每个用户
         socket.broadcast.emit('newImg', socket.nickname, imgData);
     });
    

    main.js

    this.socket.on('newImg', function(user, img) {
         that._displayImage(user, img);
     });
    

    有个问题就是如果图片过大,会破坏整个窗口的布局,或者会出现水平滚动条,所以我们对图片进行样式上的设置让它最多只能以聊天窗口的99%宽度来显示,这样过大的图片就会自己缩小了。

    #historyMsg img {
        max-width: 99%;
    }
    

    但考虑到缩小后的图片有可能失真,用户看不清,我们需要提供一个方法让用户可以查看原尺寸大小的图片,所以将图片用一个链接进行包裹,当点击图片的时候我们打开一个新的窗口页面,并将图片按原始大小呈现到这个新页面中让用户查看。

    所以最后我们实现的_displayNImage方法应该是这样的。

    将以下代码添加到main.js的HiChat类中。

    _displayImage: function(user, imgData, color) {
        var container = document.getElementById('historyMsg'),
            msgToDisplay = document.createElement('p'),
            date = new Date().toTimeString().substr(0, 8);
        msgToDisplay.style.color = color || '#000';
        msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span> <br/>' + '<a href="' + imgData + '" target="_blank">![](' + imgData + ')</a>';
        container.appendChild(msgToDisplay);
        container.scrollTop = container.scrollHeight;
    }
    
    发送表情

    规定了一种格式来代表表情,[emoji:xx],中括号括起来然后'emoji'加个冒号,后面跟一个数字,这个数字表示某个gif图片的编号。程序中,如果我们点击表情按扭,然后呈现所有可用的表情图片,当用户选择一个表情后,生成对应的代码插入到当前待发送的文字消息中。发出去后,每个人接收到的也是代码形式的消息,只是在将消息显示到页面前,我们将表情代码提取出来,获取图片编号,然后用相应的图片替换。

    文字颜色

    _displayNewMsg方法可以接收一个color参数,现在要做的就是每次发送消息到服务器的时候,多加一个color参数就可以了,同时,在显示消息时调用_displayNewMsg的时候将这个color传递过去。

    下面是修改main.js中消息发送按钮代码的示例:

    document.getElementById('sendBtn').addEventListener('click', function() {
        var messageInput = document.getElementById('messageInput'),
            msg = messageInput.value,
            //获取颜色值
            color = document.getElementById('colorStyle').value;
        messageInput.value = '';
        messageInput.focus();
        if (msg.trim().length != 0) {
            //显示和发送时带上颜色值参数
            that.socket.emit('postMsg', msg, color);
            that._displayNewMsg('me', msg, color);
        };
    }, false);
    

    同样socket的绑定事件 newMsg

    this.socket.on('newMsg', function(user, msg, color) {
         that._displayNewMsg(user, msg, color);
     });
    

    这只是展示了发送按钮的修改,改动非常小,只是每次消息发送时获取一下颜色值,同时emit事件到服务器的时候也带上这个颜色值,这样前端在显示时就可以根据这个颜色值为每个不两只用户显示他们自己设置的颜色了。
    三.HTML5桌面提醒(Notification)
    在聊天中,虽然已经有显示,某位用户登录.但如果用户不在该页面时,如果A用户要通知B用户,例如QQ的抖一抖;
    HTML5提供最新的API: Notification
    微信用Notification绑定socket,来做即时通讯,通知用户.Notification的兼容最新浏览器,如图

    2016-07-05_221531.png

    同理,server.js:

    //new Notification
        socket.on('noti',function(config){
            socket.broadcast.emit('notification',socket.nickname,config);
        });
    

    main.js:

    this.socket.on('notification',function(user,config){
                that._consoleText(user,config);
            });
    
    document.getElementById('notifBtn').addEventListener('click',function(){
              var config = {
                              body: '我发起了一个描述~', //正文内容
                              dir: 'auto',//文本显示方向 auto ,ltr,rtl
                              lang:'en', //文本语言
                              icon: 'http://img4.imgtn.bdimg.com/it/u=196405673,1932175745&fm=23&gp=0.jpg' //图片的URL,将被用于显示通知的图标
                          };
              that.socket.emit('noti',config);
              return;
            }, false);
    

    同样,需要把_consoleText添加到HiChat类中:

    _consoleText: function(user,config){
          if(window.Notification){
                  if(Notification.permission === 'granted'){
                      var notification = new Notification(user,config);
                      delete notification;
                  }else{
                      Notification.requestPermission();
                  }
              }
        }
    

    相关文章

      网友评论

      本文标题:Node.js+socket.io即时通讯+HTML5桌面提醒(

      本文链接:https://www.haomeiwen.com/subject/hlrkittx.html