聊天室入门实战(node,socket.io实现)--第二章(美化,图片发送,登录提示,认证)

来源:互联网 发布:nginx获取header信息 编辑:程序博客网 时间:2024/05/22 00:23

项目已经部署,请访问: "chat.mycollagelife.com"

这是整个实战系列的第二章,建议先阅读第一章,这一章是基于第一章基础改进的,实现了界面的美化,图片的发送,好友登录的提示,以及登录认证等功能,实现的结果如图:



登录认证:




登录:



好友上线通知:



图片发送:

项目源码:

该项目已经上传至Github   https://github.com/neuqzxy/chat  觉得可以的话给个星星吧吐舌头现在github上有original文件夹是我已经完成的项目,而chat文件夹下是与博客同步的

技术要求:首先你需要能够看懂第一章的内容,其次这一章我使用了bootstrap,你需要对bootstrap有一个大致的了解,这一章是第一章的升级,最好先看完第一章。


使用bootstrap美化登录界面

1. 先下载bootstrap文件,我们打开bootstrap官网下载bootstrap,我们下载用于生产环境的:


下载后解压


2. 拷贝我们第一章的全部内容,新建一个chat++文件夹,粘贴进去。然后进入解压后的bootstrap中,拷贝bootstrap.js和bootstrap.css到我们的项目文件中,然后在chat.html中引用,如下图:


3. 美化登录界面
我们进入bootstrap官网,在全局css样式里找到内联表单这一栏,拷贝:

然后修改一下:

由于我们的登录不是提交表单,所以把form改为div标签,避免不必要的麻烦。把button类型改成button 类btn-info是控制按钮颜色的,btn-lg表示最大的按钮,修改一下文字内容和id就可以了,修改后的源码:

<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <title>聊天室</title>    <script src="JavaScripts/jquery-3.2.1.js"></script>    <link rel="stylesheet" href="stylesheets/bootstrap.css">    <script src="JavaScripts/bootstrap.js"></script>    <script src="JavaScripts/main.js"></script>    <script src="/socket.io/socket.io.js"></script>    <style>        html,body {            background: #ccc;            padding: 20px 0 20px 0;        }        h1 {            text-align: center;        }        .login {            text-align: center;        }        .chatinput {            display: block;            width: 100%;            position: absolute;            min-height: 30px;            bottom: 0;        }        #content {            height: auto;        }    </style></head><body><div id="loginbox">    <h1>登录</h1>    <hr>    <div class="login">        <div class="form-inline">            <div class="form-group">                <label>用户名</label>                <input type="text" class="form-control" id="name" placeholder="请输入用户名">            </div>            <br><br>            <button type="button" class="btn btn-info btn-lg" id="loginbutton">登录</button>        </div>    </div></div><div style="display: none;" id="chatbox">    <div id="content"></div>    <input type="text" placeholder="saying somgthing" class="chatinput" id="chatinput"></div></body></html>


登录界面的美化就暂时到这里了,我们来美化欢迎文字


美化欢迎提醒

我们现在的欢迎只是一个p标签很丑,还永远保留在那,我们希望可以有一个弹窗的提醒,然后再chat界面还有一行文字显示是谁的聊天室界面,我们来完成吧。
bootstrap组件中的警告框非常合适:


我们就使用可关闭的警告框,显示2s后关闭。点击《bootstrap警告框插件》找到用法,我们选择蓝色的那一款警告框,对应的类是alert-info
因为我们需要自动控制它的关闭,所以要加上一些属性,在《bootstrap警告框插件》中已经很清楚的说明了需要额外添加的属性:

我们最终的样式如下:
<div style="display: none;" id="chatbox">    <div class="alert alert-info alert-dismissible fade in" role="alert" id="myalert">        <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>    </div>    <div id="content"></div>    <input type="text" placeholder="saying somgthing" class="chatinput" id="chatinput"></div>

将其放在#content的上面,接下来就是控制显示了,如上图有两个方法alert()和alert("close"),我们就使用这两个方法控制。
因为这是一开始的欢迎提醒,所以自然要在beginChat这个登录成功后就调用的函数里写,修改我们原来的欢迎:

    let beginChat = function () {        /**         * 1.隐藏登录框,取消它绑定的事件         * 2.显示聊天界面         */        $("#loginbox").hide('slow');        _$inputname.off('keyup');        _$loginButton.off('click');        /**         * 显示聊天界面,并显示一行文字,表示是谁的聊天界面         * 一个2s的弹框,显示欢迎字样         * 这里我使用了ES6的语法``中可以使用${}在里面写的变量可以直接被浏览器渲染         */        $(`<h2 style="text-align: center">${_username}的聊天室</h2>`).insertBefore($("#content"));        $(`<strong>欢迎你</strong><span>${_username}!</span>`).insertAfter($('#myalert button'));        $('#myalert').alert();        setTimeout(function () {            $('#myalert').alert('close');        },2000);        $("#chatbox").show('slow');    };
到了这里我们打开浏览器,登录一下试试,发现样式有点问题,我们改进一下,我们给弹窗设置绝对定位,让他始终在顶部,然后让他覆盖我们的“聊天室”字样:

        .alert {            z-index: 100;            position: absolute;            top: 0;            width: 100%;        }

现在再看一下,是不是好看多了呢?

登录检查

我们要保证登录时用户名唯一,这就要在服务器端匹配用户名了。我们先将用户发送过来的的用户名保存在这次的socket中,然后再和所有用户数组匹配,如果没有发现该用户名,该用户名保留并保存到所有用户数组中,反之,如果发现重复,将保存在socket中的用户名删除,并发送err给客户端,并且不触发loginSuccess。客户端接收到err后,提醒用户。
这就是完整的检查的逻辑,下面我们来实现。
    socket.on('login',(data)=>{        /**         * 先保存在socket中         * 循环数组判断用户名是否重复,如果重复,则触发usernameErr事件         * 将用户名删除,之后的事件要判断用户名是否存在         */        socket.username = data.username;        for (let user of users) {            if(user.username === data.username){                socket.emit('usernameErr',{err: '用户名重复'});                socket.username = null;                break;            }        }        //如果用户名存在。将该用户的信息存进数组中        if(socket.username){            users.push({                username: data.username,                message: []            });            //然后触发loginSuccess事件告诉浏览器登陆成功了            socket.emit('loginSuccess',data);   //将data原封不动的再发给该浏览器        }    });
现在客户端还需要监听usernameErr事件来判断是否重复,我们先写一个alert看一下能不能运行:
    socket.on('usernameErr',(data)=>{        alert('用户名重复');    });
发现能够正常提醒而且不会触发loginSuccess事件,接下来我们美化一下,我们在bootstrap全局css样式里找到表单=》校验状态:



input with err 就是我们需要的,我们来使用它,通过观察代码我们发现我们只需要添加一个has-error类和一个label就行:
    socket.on('usernameErr',(data)=>{        /**         * 我们给外部div添加 .has-error         * 拷贝label插入         * 控制显示的时间为1.5s         */        $(".login .form-inline .form-group").addClass("has-error");        $('<label class="control-label" for="inputError1">用户名重复</label>').insertAfter($('#name'));        setTimeout(function() {            $('.login .form-inline .form-group').removeClass('has-error');            $("#name + label").remove();        }, 1500)    });
这里要控制显示时间,否则消不掉。


删除用户

我们必须要在用户退出的时候将他从所有用户数组中删除。应该在disconnect中写:
    //断开连接后做的事情    socket.on('disconnect',()=>{          //注意,该事件不需要自定义触发器,系统会自动调用        usersNum --;        console.log(`当前有${usersNum}个用户连接上服务器了`);        //删除用户        users.forEach(function (user,index) {            if(user.username === socket.username) {                users.splice(index,1);       //找到该用户,删除            }        })    })
现在我们的登录功能就能正常实现了。


好友上线提醒

当有用户上线时,我们需要接受一个友好的提醒,警告框就很合适。我们再设置一个警告框。
<div style="display: none;" id="chatbox">    <div class="alert alert-info alert-dismissible fade in" role="alert" id="myalert">        <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>    </div>    <div class="alert alert-info alert-dismissible fade in" role="alert" id="myalert1">        <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>        <span></span>    </div>    <div id="content"></div>    <input type="text" placeholder="saying somgthing" class="chatinput" id="chatinput"></div>
现在我们有两个警告框了,第一个只在登陆成功显示欢迎,第二个在每次好友登录都会显示,我们通过jquery的show()来控制第二个的显示。

首先,我们需要在登陆成功时隐藏第二个警告框,在beginChat里添加一行代码:
    let beginChat = function () {        /**         * 1.隐藏登录框,取消它绑定的事件         * 2.显示聊天界面         */        $("#loginbox").hide('slow');        _$inputname.off('keyup');        _$loginButton.off('click');        /**         * 显示聊天界面,并显示一行文字,表示是谁的聊天界面         * 一个2s的弹框,显示欢迎字样         * 这里我使用了ES6的语法``中可以使用${}在里面写的变量可以直接被浏览器渲染         */        $(`<h2 style="text-align: center">${_username}的聊天室</h2>`).insertBefore($("#content"));        $(`<strong>欢迎你</strong><span>${_username}!</span>`).insertAfter($('#myalert button'));        $("#myalert1").hide();   //隐藏第二个警告框        $('#myalert').alert();        setTimeout(function () {            $('#myalert').alert('close');        },2000);        $("#chatbox").show('slow');    };

然后修改app.js中的loginSuccess事件,改成广播形式触发
        //如果用户名存在。将该用户的信息存进数组中        if(socket.username){            users.push({                username: data.username,                message: []            });            //然后触发loginSuccess事件告诉浏览器登陆成功了,广播形式触发            io.emit('loginSuccess',data);   //将data原封不动的再发给该浏览器        }

最后到main.js里显示警告框:

    socket.on('loginSuccess',(data)=>{        /**         * 如果服务器返回的用户名和刚刚发送的相同的话,就登录         * 否则说明有地方出问题了,拒绝登录         */        if(data.username === _username) {            beginChat(data);        }else {            $('#myalert1 span').html(`<span>您的好友<strong>${data.username}</strong>上线了!</span>`);            setTimeout(function() {                $("#myalert1").hide();            }, 1000);            $("#myalert1").show();        }    });

看到这段你可能有疑问,为什么show写在hide()下面呢?其实写在上面下面都行,这里定时器是异步的,这个涉及到了js中的事件队列,你可以玩一个很简单的例子,写一个脚本,把定时器写在第一个,定时为0,然后下面输出很多东西,最后你可以发现,定时器还是最后执行。如果听不懂就跳过这段吧,底层原理我也不是特别清楚。
现在你就可以看到好友登录提醒了。


发送图片

写样式

我们开始实现发送图片的功能。为了美观我们使用button按钮,间接控制input 文件控件。
在html里写修改input为:
    <div id="inputgrop">        <div class="col-md-10">            <input type="text" placeholder="saying somgthing" class="form-control chatinput" id="chatinput">        </div>        <form action="" style="display: none;" id="resetform"><input type="file" style="display: none" id="imginput"></form>        <div class="col-md-2">            <button type="button" class="btn btn-primary" id="imgbutton">发送图片</button>        </div>    </div>
改一下css:
        #inputgrop {            display: block;            width: 100%;            position: absolute;            min-height: 40px;            bottom: 0;        }

解释一下,这里要把他们包在一个div里方便设置样式,定位,这里 col-md-10是bootstrap的栅格式布局,建议其官网详细了解一下。有人会问,为什么要加一个form标签呢?
这里先卖一个关子,过会就知道了。
我们去修改一下布局:
    let setInputPosition = function () {        let height = $(window).height()>$('#content p:last').offset().top+$('#content p:last').height()*2?$(window).height():$('#content p:last').offset().top+$('#content p:last').height()*2;        _$inputGroup.css({'top': height});    };

button控制input

虽然我们点击的是button,但是实际作用还是input,我们写事件来控制:

    //点击图片按钮触发input    _$imgButton.on('click',function (event) {        _$imgInput.click();        return false;    });
这里的变量我都提前声明好了:
    const url = 'http://127.0.0.1:3000';    let _username = null;    let _$inputname = $("#name");    let _$loginButton = $("#loginbutton");    let _$chatinput = $("#chatinput");    let _$inputGroup = $("#inputgrop");    let _$imgButton = $("#imgbutton");    let _$imgInput = $("#imginput");

点击一下看看,起作用了

app.js:
/** * Created by zhouxinyu on 2017/8/6. */const express = require('express');  // const是ES6的语法,代表常量,准确来说就是指向不发生改变。如果不习惯就用var代替const app = express();               // express官网就是这么写的就是用来创建一个express程序,赋值给app。如果不理解就当公式记住const server = require('http').Server(app);const path = require('path');        // 这是node的路径处理模块,可以格式化路径const io = require('socket.io')(server);     //将socket的监听加到app设置的模块里。const users = [];                    //用来保存所有的用户信息let usersNum = 0;server.listen(3000,()=>{                // ()=>是箭头函数,ES6语法,如果不习惯可以使用 function() 来代替 ()=>    console.log("server running at 127.0.0.1:3000");       // 代表监听3000端口,然后执行回调函数在控制台输出。});/** * app.get(): express中的一个中间件,用于匹配get请求,所谓中间件就是在该轮http请求中依次执行的一系列函数。 * '/': 它匹配get请求的根路由 '/'也就是 127.0.0.1:3000/就匹配到他了 * (req,res): ES6语法的箭头函数,你暂时可以理解为function(req,res){}。 * req带表浏览器的请求对象,res代表服务器的返回对象 */app.get('/',(req,res)=>{    res.redirect('/chat.html');       // express的重定向函数。如果浏览器请求了根路由'/',浏览器就给他重定向到 '127.0.0.1:3000/chat.html'路由中});/** * __dirname表示当前文件所在的绝对路径,所以我们使用path.join将app.js的绝对路径和public加起来就得到了public的绝对路径。 * 用path.join是为了避免出现 ././public 这种奇怪的路径 * express.static就帮我们托管了public文件夹中的静态资源。 * 只要有 127.0.0.1:3000/XXX 的路径都会去public文件夹下找XXX文件然后发送给浏览器。 */app.use('/',express.static(path.join(__dirname,'./public')));        //一句话就搞定。/*socket*/io.on('connection',(socket)=>{              //监听客户端的连接事件    /**     * 所有有关socket事件的逻辑都在这里写     */    usersNum ++;    console.log(`当前有${usersNum}个用户连接上服务器了`);    socket.on('login',(data)=>{        /**         * 先保存在socket中         * 循环数组判断用户名是否重复,如果重复,则触发usernameErr事件         * 将用户名删除,之后的事件要判断用户名是否存在         */        socket.username = data.username;        for (let user of users) {            if(user.username === data.username){                socket.emit('usernameErr',{err: '用户名重复'});                socket.username = null;                break;            }        }        //如果用户名存在。将该用户的信息存进数组中        if(socket.username){            users.push({                username: data.username,                message: []            });            //然后触发loginSuccess事件告诉浏览器登陆成功了,广播形式触发            io.emit('loginSuccess',data);   //将data原封不动的再发给该浏览器        }    });    /**     * 监听sendMessage,我们得到客户端传过来的data里的message,并存起来。     * 我使用了ES6的for-of循环,和ES5 的for-in类似。     * for-in是得到每一个key,for-of 是得到每一个value     */    socket.on('sendMessage',(data)=>{        for(let _user of users) {            if(_user.username === data.username) {                _user.message.push(data.message);                //信息存储之后触发receiveMessage将信息发给所有浏览器                io.emit('receiveMessage',data);                break;            }        }    });    //断开连接后做的事情    socket.on('disconnect',()=>{          //注意,该事件不需要自定义触发器,系统会自动调用        usersNum --;        console.log(`当前有${usersNum}个用户连接上服务器了`);        //删除用户        users.forEach(function (user,index) {            if(user.username === socket.username) {                users.splice(index,1);       //找到该用户,删除            }        })    })});


main.js:
/** * Created by zhouxinyu on 2017/8/6. */$(function(){    const url = 'http://127.0.0.1:3000';    let _username = null;    let _$inputname = $("#name");    let _$loginButton = $("#loginbutton");    let _$chatinput = $("#chatinput");    let _$inputGroup = $("#inputgrop");    let _$imgButton = $("#imgbutton");    let _$imgInput = $("#imginput");    let socket = io.connect(url);    //设置用户名,当用户登录的时候触发    let setUsername = function () {        _username = _$inputname.val().trim();    //得到输入框中用户输入的用户名        //判断用户名是否存在        if(_username) {            socket.emit('login',{username: _username});   //如果用户名存在,就代表可以登录了,我们就触发登录事件,就相当于告诉服务器我们要登录了        }    };    let beginChat = function () {        /**         * 1.隐藏登录框,取消它绑定的事件         * 2.显示聊天界面         */        $("#loginbox").hide('slow');        _$inputname.off('keyup');        _$loginButton.off('click');        /**         * 显示聊天界面,并显示一行文字,表示是谁的聊天界面         * 一个2s的弹框,显示欢迎字样         * 这里我使用了ES6的语法``中可以使用${}在里面写的变量可以直接被浏览器渲染         */        $(`<h2 style="text-align: center">${_username}的聊天室</h2>`).insertBefore($("#content"));        $(`<strong>欢迎你</strong><span>${_username}!</span>`).insertAfter($('#myalert button'));        $("#myalert1").hide();        $('#myalert').alert();        setTimeout(function () {            $('#myalert').alert('close');        },2000);        $("#chatbox").show('slow');    };    let sendMessage = function () {        /**         * 得到输入框的聊天信息,如果不为空,就触发sendMessage         * 将信息和用户名发送过去         */        let _message = _$chatinput.val();        if(_message) {            socket.emit('sendMessage',{username: _username, message: _message});        }    };    let setInputPosition = function () {        let height = $(window).height()>$('#content p:last').offset().top+$('#content p:last').height()*2?$(window).height():$('#content p:last').offset().top+$('#content p:last').height()*2;        _$inputGroup.css({'top': height});    };    let showMessage = function (data) {        //先判断这个消息是不是自己发出的,然后再以不同的样式显示        if(data.username === _username){            $("#content").append(`<p style='background: lightskyblue'><span>${data.username} : </span> ${data.message}</p>`);        }else {            $("#content").append(`<p style='background: lightpink'><span>${data.username} : </span> ${data.message}</p>`);        }        setInputPosition();    };    /*       前端事件         */    /*登录事件*/    _$loginButton.on('click',function (event) {    //监听按钮的点击事件,如果点击,就说明用户要登录,就执行setUsername函数        setUsername();    });    _$inputname.on('keyup',function (event) {     //监听输入框的回车事件,这样用户回车也能登录。        if(event.keyCode === 13) {                //如果用户输入的是回车键,就执行setUsername函数            setUsername();        }    });    /*聊天事件*/    _$chatinput.on('keyup',function (event) {        if(event.keyCode === 13) {            sendMessage();            _$chatinput.val('');        }    });    //点击图片按钮触发input    _$imgButton.on('click',function (event) {        _$imgInput.click();        return false;    });    /*        socket.io部分逻辑        */    socket.on('loginSuccess',(data)=>{        /**         * 如果服务器返回的用户名和刚刚发送的相同的话,就登录         * 否则说明有地方出问题了,拒绝登录         */        if(data.username === _username) {            beginChat(data);        }else {            $('#myalert1 span').html(`<span>您的好友<strong>${data.username}</strong>上线了!</span>`);            setTimeout(function() {                $("#myalert1").hide();            }, 1000);            $("#myalert1").show();        }    });    socket.on('receiveMessage',(data)=>{        /**         * 监听到事件发生,就显示信息         */        showMessage(data);    });    socket.on('usernameErr',(data)=>{        /**         * 我们给外部div添加 .has-error         * 拷贝label插入         * 控制显示的时间为1.5s         */        $(".login .form-inline .form-group").addClass("has-error");        $('<label class="control-label" for="inputError1">用户名重复</label>').insertAfter($('#name'));        setTimeout(function() {            $('.login .form-inline .form-group').removeClass('has-error');            $("#name + label").remove();        }, 1500)    });});




入门FileReader

FileReader先了解一下,这是HTML5 的对象,用于异步的读取文件,所以旧版本的浏览器可能不兼容。
1. 我们通过
let reader = new FileReader();
来创建一个FileReader实例,之后的操作都会在reader中进行。

2. reader有几个方法:
void abort();void readAsArrayBuffer(in Blob blob);void readAsBinaryString(in Blob blob);void readAsDataURL(in Blob blob);void readAsText(in Blob blob, [optional] in DOMString encoding)


他们分别是不同的读取文件的方式,我们采用的是readAsDataURL方式,该方式可以将读取的字符串放在<img>标签里的src中浏览器就能解析,十分方便。

3. 我们只需要将图片传入函数,之后操作的结果在result中获取。

看不懂没关系,在项目中一用就清楚了。

发送图片

到这里大家可能有一个疑问,这个发送图片怎么触发呀?实际上emit不用我们写,jquery提供了change函数:
    _$imgInput.change(function (event) {        sendImg();    });

接下来我们写sendImg函数
    let sendImg = function (event) {        /**         * 先判断浏览器是否支持FileReader         */        if (typeof FileReader === 'undefined') {            alert('您的浏览器不支持,该更新了');            //使用bootstrap的样式禁用Button            _$imgButton.attr('disabled', 'disabled');        } else {            let file = event.target.files[0];  //先得到选中的文件            //判断文件是否是图片            if(!/image\/\w+/.test(file.type)){   //如果不是图片                alert ("请选择图片");                return false;            }            /**             * 然后使用FileReader读取文件             */            let reader = new FileReader();            reader.readAsDataURL(file);            /**             * 读取完自动触发onload函数,我们触发sendImg事件给服务器             */            reader.onload = function (e) {                socket.emit('sendImg',{username: _username, dataUrl: this.result});            }        }    };

我们将用户名和读取到的内容一起发给服务器了,现在,我们在app.js中来监听sendImg事件:

    /**     * 仿照sendMessage监听sendImg事件     */    socket.on("sendImg",(data)=>{        for(let _user of users) {            if(_user.username === data.username) {                _user.dataUrl.push(data.dataUrl);                //存储后将图片广播给所有浏览器                io.emit("receiveImg",data);                break;            }        }    });
我们还需要在初始化的时候将dataURL声明为空数组:
        //如果用户名存在。将该用户的信息存进数组中        if(socket.username){            users.push({                username: data.username,                message: [],                dataUrl:[]            });

然后回到浏览器监听receiveImg

    socket.on('receiveImg',(data)=>{        /**         * 监听到receiveImg发生,就显示图片         */        showImg(data);    });

接下来仿照showMessage写showImg
    let showImg = function (data) {        //先判断这个消息是不是自己发出的,然后再以不同的样式显示        if(data.username === _username) {            $('#content').append(`<p class="bg-primary" style=' text-align:center;'><strong style="font-size: 1.5em;">${_username} </strong>:  <img src="${data.dataUrl}" style="max-height: 100px"/></p>`);        } else {            $('#content').append(`<p class="bg-info" style=''><strong style="font-size: 1.5em;">${_username} </strong>:  <img src="${data.dataUrl}" style="max-height: 100px"/></p>`);        }        setInputPosition();    };
可以看到我只是将dataUrl写入到了src属性中,图片就可以显示了。
现在我们在bootstrap中找到图片样式,给他美化一下吧,我们添加
class="img-thumbnail"

解决无法连续发两张相同图片(部分浏览器)

现在我们连续发两张相同的图片看看,你可能发现发不了了,这是因为有些浏览器连续发相同的图片无法触发input的change事件,有两种解决方法:
1. 发完后删除该input,再重新生成一个新的input
2. 在input外围加一个form标签,发完一张图后reset一下该form。
我们选择第二种方式:

        //重置一下form元素,否则如果发同一张图片不会触发change事件        $("#resetform")[0].reset();




贴一下源码:
chat.html:
<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <title>聊天室</title>    <script src="JavaScripts/jquery-3.2.1.js"></script>    <link rel="stylesheet" href="stylesheets/bootstrap.css">    <script src="JavaScripts/bootstrap.js"></script>    <script src="JavaScripts/main.js"></script>    <script src="/socket.io/socket.io.js"></script>    <style>        html,body {            background: #ccc;            padding: 20px 0 20px 0;        }        h1 {            text-align: center;        }        .login {            text-align: center;        }        #inputgrop {            display: block;            width: 100%;            position: absolute;            min-height: 40px;            bottom: 0;        }        #content {            height: auto;        }        .alert {            z-index: 100;            position: absolute;            top: 0;            width: 100%;        }    </style></head><body><div id="loginbox">    <h1>登录</h1>    <hr>    <div class="login">        <div class="form-inline">            <div class="form-group">                <label>用户名</label>                <input type="text" class="form-control" id="name" placeholder="请输入用户名">            </div>            <br><br>            <button type="button" class="btn btn-info btn-lg" id="loginbutton">登录</button>        </div>    </div></div><div style="display: none;" id="chatbox">    <div class="alert alert-info alert-dismissible fade in" role="alert" id="myalert">        <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>    </div>    <div class="alert alert-info alert-dismissible fade in" role="alert" id="myalert1">        <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>        <span></span>    </div>    <div id="content"></div>    <div id="inputgrop">        <div class="col-md-10">            <input type="text" placeholder="saying somgthing" class="form-control chatinput" id="chatinput">        </div>        <form action="" style="display: none;" id="resetform"><input type="file" style="display: none" id="imginput"></form>        <div class="col-md-2">            <button type="button" class="btn btn-primary" id="imgbutton">发送图片</button>        </div>    </div></div></body></html>

main.js:
/** * Created by zhouxinyu on 2017/8/6. */$(function(){    const url = 'http://127.0.0.1:3000';    let _username = null;    let _$inputname = $("#name");    let _$loginButton = $("#loginbutton");    let _$chatinput = $("#chatinput");    let _$inputGroup = $("#inputgrop");    let _$imgButton = $("#imgbutton");    let _$imgInput = $("#imginput");    let socket = io.connect(url);    //设置用户名,当用户登录的时候触发    let setUsername = function () {        _username = _$inputname.val().trim();    //得到输入框中用户输入的用户名        //判断用户名是否存在        if(_username) {            socket.emit('login',{username: _username});   //如果用户名存在,就代表可以登录了,我们就触发登录事件,就相当于告诉服务器我们要登录了        }    };    let beginChat = function () {        /**         * 1.隐藏登录框,取消它绑定的事件         * 2.显示聊天界面         */        $("#loginbox").hide('slow');        _$inputname.off('keyup');        _$loginButton.off('click');        /**         * 显示聊天界面,并显示一行文字,表示是谁的聊天界面         * 一个2s的弹框,显示欢迎字样         * 这里我使用了ES6的语法``中可以使用${}在里面写的变量可以直接被浏览器渲染         */        $(`<h2 style="text-align: center">${_username}的聊天室</h2>`).insertBefore($("#content"));        $(`<strong>欢迎你</strong><span>${_username}!</span>`).insertAfter($('#myalert button'));        $("#myalert1").hide();        $('#myalert').alert();        setTimeout(function () {            $('#myalert').alert('close');        },2000);        $("#chatbox").show('slow');    };    let sendMessage = function () {        /**         * 得到输入框的聊天信息,如果不为空,就触发sendMessage         * 将信息和用户名发送过去         */        let _message = _$chatinput.val();        if(_message) {            socket.emit('sendMessage',{username: _username, message: _message});        }    };    let setInputPosition = function () {        let height = $(window).height()>$('#content p:last').offset().top+$('#content p:last').height()*2?$(window).height():$('#content p:last').offset().top+$('#content p:last').height()*2;        _$inputGroup.css({'top': height});    };    let showMessage = function (data) {        //先判断这个消息是不是自己发出的,然后再以不同的样式显示        if(data.username === _username){            $("#content").append(`<p style='background: lightskyblue'><span>${data.username} : </span> ${data.message}</p>`);        }else {            $("#content").append(`<p style='background: lightpink'><span>${data.username} : </span> ${data.message}</p>`);        }        setInputPosition();    };    let sendImg = function (event) {        /**         * 先判断浏览器是否支持FileReader         */        if (typeof FileReader === 'undefined') {            alert('您的浏览器不支持,该更新了');            //使用bootstrap的样式禁用Button            _$imgButton.attr('disabled', 'disabled');        } else {            let file = event.target.files[0];  //先得到选中的文件            //判断文件是否是图片            if(!/image\/\w+/.test(file.type)){   //如果不是图片                alert ("请选择图片");                return false;            }            /**             * 然后使用FileReader读取文件             */            let reader = new FileReader();            reader.readAsDataURL(file);            /**             * 读取完自动触发onload函数,我们触发sendImg事件给服务器             */            reader.onload = function (e) {                socket.emit('sendImg',{username: _username, dataUrl: this.result});            }        }    };    let showImg = function (data) {        //先判断这个消息是不是自己发出的,然后再以不同的样式显示        if(data.username === _username) {            $('#content').append(`<p class="bg-primary" style=' text-align:center;'><strong style="font-size: 1.5em;">${_username} </strong>:  <img class="img-thumbnail" src="${data.dataUrl}" style="max-height: 100px"/></p>`);        } else {            $('#content').append(`<p class="bg-info" style=''><strong style="font-size: 1.5em;">${_username} </strong>:  <img class="img-thumbnail" src="${data.dataUrl}" style="max-height: 100px"/></p>`);        }        setInputPosition();    };    /*       前端事件         */    /*登录事件*/    _$loginButton.on('click',function (event) {    //监听按钮的点击事件,如果点击,就说明用户要登录,就执行setUsername函数        setUsername();    });    _$inputname.on('keyup',function (event) {     //监听输入框的回车事件,这样用户回车也能登录。        if(event.keyCode === 13) {                //如果用户输入的是回车键,就执行setUsername函数            setUsername();        }    });    /*聊天事件*/    _$chatinput.on('keyup',function (event) {        if(event.keyCode === 13) {            sendMessage();            _$chatinput.val('');        }    });    //点击图片按钮触发input    _$imgButton.on('click',function (event) {        _$imgInput.click();        return false;    });    _$imgInput.change(function (event) {        sendImg(event);        //重置一下form元素,否则如果发同一张图片不会触发change事件        $("#resetform")[0].reset();    });    /*        socket.io部分逻辑        */    socket.on('loginSuccess',(data)=>{        /**         * 如果服务器返回的用户名和刚刚发送的相同的话,就登录         * 否则说明有地方出问题了,拒绝登录         */        if(data.username === _username) {            beginChat(data);        }else {            $('#myalert1 span').html(`<span>您的好友<strong>${data.username}</strong>上线了!</span>`);            setTimeout(function() {                $("#myalert1").hide();            }, 1000);            $("#myalert1").show();        }    });    socket.on('receiveMessage',(data)=>{        /**         * 监听到事件发生,就显示信息         */        showMessage(data);    });    socket.on('usernameErr',(data)=>{        /**         * 我们给外部div添加 .has-error         * 拷贝label插入         * 控制显示的时间为1.5s         */        $(".login .form-inline .form-group").addClass("has-error");        $('<label class="control-label" for="inputError1">用户名重复</label>').insertAfter($('#name'));        setTimeout(function() {            $('.login .form-inline .form-group').removeClass('has-error');            $("#name + label").remove();        }, 1500)    });    socket.on('receiveImg',(data)=>{        /**         * 监听到receiveImg发生,就显示图片         */        showImg(data);    });});



app.js
/** * Created by zhouxinyu on 2017/8/6. */const express = require('express');  // const是ES6的语法,代表常量,准确来说就是指向不发生改变。如果不习惯就用var代替const app = express();               // express官网就是这么写的就是用来创建一个express程序,赋值给app。如果不理解就当公式记住const server = require('http').Server(app);const path = require('path');        // 这是node的路径处理模块,可以格式化路径const io = require('socket.io')(server);     //将socket的监听加到app设置的模块里。const users = [];                    //用来保存所有的用户信息let usersNum = 0;server.listen(3000,()=>{                // ()=>是箭头函数,ES6语法,如果不习惯可以使用 function() 来代替 ()=>    console.log("server running at 127.0.0.1:3000");       // 代表监听3000端口,然后执行回调函数在控制台输出。});/** * app.get(): express中的一个中间件,用于匹配get请求,所谓中间件就是在该轮http请求中依次执行的一系列函数。 * '/': 它匹配get请求的根路由 '/'也就是 127.0.0.1:3000/就匹配到他了 * (req,res): ES6语法的箭头函数,你暂时可以理解为function(req,res){}。 * req带表浏览器的请求对象,res代表服务器的返回对象 */app.get('/',(req,res)=>{    res.redirect('/chat.html');       // express的重定向函数。如果浏览器请求了根路由'/',浏览器就给他重定向到 '127.0.0.1:3000/chat.html'路由中});/** * __dirname表示当前文件所在的绝对路径,所以我们使用path.join将app.js的绝对路径和public加起来就得到了public的绝对路径。 * 用path.join是为了避免出现 ././public 这种奇怪的路径 * express.static就帮我们托管了public文件夹中的静态资源。 * 只要有 127.0.0.1:3000/XXX 的路径都会去public文件夹下找XXX文件然后发送给浏览器。 */app.use('/',express.static(path.join(__dirname,'./public')));        //一句话就搞定。/*socket*/io.on('connection',(socket)=>{              //监听客户端的连接事件    /**     * 所有有关socket事件的逻辑都在这里写     */    usersNum ++;    console.log(`当前有${usersNum}个用户连接上服务器了`);    socket.on('login',(data)=>{        /**         * 先保存在socket中         * 循环数组判断用户名是否重复,如果重复,则触发usernameErr事件         * 将用户名删除,之后的事件要判断用户名是否存在         */        socket.username = data.username;        for (let user of users) {            if(user.username === data.username){                socket.emit('usernameErr',{err: '用户名重复'});                socket.username = null;                break;            }        }        //如果用户名存在。将该用户的信息存进数组中        if(socket.username){            users.push({                username: data.username,                message: [],                dataUrl:[]            });            //然后触发loginSuccess事件告诉浏览器登陆成功了,广播形式触发            io.emit('loginSuccess',data);   //将data原封不动的再发给该浏览器        }    });    /**     * 监听sendMessage,我们得到客户端传过来的data里的message,并存起来。     * 我使用了ES6的for-of循环,和ES5 的for-in类似。     * for-in是得到每一个key,for-of 是得到每一个value     */    socket.on('sendMessage',(data)=>{        for(let _user of users) {            if(_user.username === data.username) {                _user.message.push(data.message);                //信息存储之后触发receiveMessage将信息发给所有浏览器                io.emit('receiveMessage',data);                break;            }        }    });    /**     * 仿照sendMessage监听sendImg事件     */    socket.on("sendImg",(data)=>{        for(let _user of users) {            if(_user.username === data.username) {                _user.dataUrl.push(data.dataUrl);                //存储后将图片广播给所有浏览器                io.emit("receiveImg",data);                break;            }        }    });    //断开连接后做的事情    socket.on('disconnect',()=>{          //注意,该事件不需要自定义触发器,系统会自动调用        usersNum --;        console.log(`当前有${usersNum}个用户连接上服务器了`);        //删除用户        users.forEach(function (user,index) {            if(user.username === socket.username) {                users.splice(index,1);       //找到该用户,删除            }        })    })});



到这里,第二章的内容就结束了,本章项目源码也已上传至github上   https://github.com/neuqzxy/chat   欢迎浏览拷贝,如果觉的不错,给个星星吧吐舌头


阅读全文
0 0
原创粉丝点击