nodejs+mongoose+websocket搭建xxx聊天

2017-06-02  本文已影响0人  hawkzz

简介

本文是由nodejs+mongoose+websocket打造的一个即时聊天系统;本来打算开发一个类似于网页QQ类似功能的聊天系统,但是目前只是开发了一个模块功能 --- 类似群聊的,即一对多的聊天模式;因为时间关系,一对一私聊功能还没有开发,敬请期待!

该聊天室整个页面布局是通过bootstrap框架编写,可能很简陋,请大家多多包涵!

源码及作品

作品在线地址:http://chat.hawkzz.com
源码地址:https://github.com/zhuangZhou/chat.io

本地运行方法:

下面为效果图预览:

环境介绍

准备工作

开始工作##

express框架搭建

express是一个基于 Node.js 平台的极简、灵活的 web 应用开发框架,它提供一系列强大的特性,帮助你创建各种 Web 和移动设备应用。接下来我们在上面创建的app.js 里面搭建express框架的搭建

var express = require('experss');//引入express模块

var app =  new express();//实例化

app.listen(8880);//监听端口8880,这里可以自定义端口

根据以上,其实一个node服务器已经搭好了,通过命令‘node app.js’即可运行,地址是localhost:8880;这只是一个简单的服务器,想要加载页面以及页面交互,这些肯定是远远不够,接下来我们一步步开始;

首先,我们创建三个文件夹,分别为views,public,router;

然后,设置模板,以及静态文件托管,以及加载body-parser和cookie模块

var express = require('express');
var swig = require('swig');
var bodyParser = require('body-parser');
var cookieParser = require('cookie-parser');

var app = new express();

var server = require('http').createServer(app);

//静态文件托管
app.use('/public', express.static(__dirname + '/public'));

//设置模板
app.engine('html', swig.renderFile);
app.set('views', './views');
app.set('view engine', 'html');
swig.setDefaults({
    cache: false
});

//设置body-parser
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));

//设置cookie
app.use(cookieParser());

//加载路由
app.use('/', require('./routers/index')); //首页
app.use('/login', require('./routers/login')); //登录页面
app.use('/register', require('./routers/register'));//注册页面
app.use('/exit', require('./routers/exit'));//退出

server.listen(8880, function () {
    console.log('服务器连接成功!');
});

mongoDB搭建

首先,我们需要下载安装mongoDB环境,https://www.mongodb.com/download-center#community

然后,创建一个文件夹database,存放本项目所需的mongoDB数据库文件;

接着,我们将mongoDB数据库的存放指向database;

1. 通过cmd找到mongoDB安装路径 -->  Server --> bin ;
2. 输入命令 mongod --dbpath  E://chat/database ;
3. 回车;

这里,我们是使用mongoDB的一个对象模型工具--mongoose来对mongoDB数据库进行操作,所以,这里我们需要了解mongoose的机制;

mongoose是mongoDB的一个对象模型工具,是基于node-mongodb-native开发的mongoDB的nodejs驱动,可以在异步的环境下执行。同时它也是针对mongoDB操作的一个对象模型库,封装了mongoDB对文档的一些增删改查等常用方法,让nodejs操作mongoDB数据库变得更加容易。

如果要通过mongoose创建一个集合并对其进行增删改查,就需要用到Schema,Model;所以接下来我们创建两个文件夹Schema,Model来存储Schema和Model文件(这里我是把这两个分开存放,也可以写在一起);

Schema是一种以文件形式存储的数据库模型骨架,无法直接通往数据库端,也就是说它不具备对数据库的操作能力,仅仅只是数据库模型在程序片段中的一种表现,可以说是数据属性模型(传统意义的表结构),又或者是集合的模型骨架。基本属性类型有字符串、日期型、数值型、布尔型、null、数组、内嵌文档等。

Model由Schema构造生成的模型,除了Schema定义的数据库骨架以外,还具有数据库操作的行为,类似于管理数据属性、行为的类。

到目前为止,我们整个项目的骨架,以及所有目录结构已经搭建完成,如下图:

登录与注册

因为我们要求连接的用户首先要注册一个账号,并且这个账号的用户名是唯一的,不能与别人相同,方便用户区分,以及一些数据库操作;

为此在后台中,我们需要创建数据库连接,以及登录注册页面;

1.在app.js连接数据库

var  mongoose = require('mongoose');

...
...
...

mongoose.connect('mongodb://localhost:27017/chat', function (err, data) {
    if (err) {
        console.log('数据库连接失败!');
    } else {
        server.listen(8880, function () {
            console.log('服务器连接成功!');
        });

    }
});

这里我把数据库连接放在服务器监听外面,是为了当数据库启动后,再启动服务器,避免服务器启动,数据库连接不上,导致整个程序跑不动;

2.在Schema文件夹创建user.js文件,创建user的数据属性模型

const mongose = require('mongoose');

const user = mongose.Schema({
    username: String, //用户姓名
    password: String, //用户密码
    image: String,  //用户图像
    state: Boolean  //用户上学状态
});

module.exports = user;


3.在Model文件夹创建User.js 文件,创建user的对象模型

const mongoose = require('mongoose');
const userSchema = require('../schemas/user');
module.exports = mongoose.model('User', userSchema);

4.在views创建注册页面register.html,以及在router创建register.js

前台:

$('#register').on({
        click: function () {
            var username = $('#username').val();
            var password = $('#password').val();
            var rePassowrd = $('#rePassword').val();
            if (username == '') {
                alert('请填写用户名!');
                return false;
            }
            var reg = /^[a-z0-9_-]{6,18}$/;
            if (!reg.test(password)) {
                alert('请填写6-12位密码!');
                return false;
            }

            if (password !== rePassowrd) {
                alert('两次密码不一致!');
                return false;
            }

            $.post('/register/signUp', {username: username, password: password}, function (res) {
                if (res.success == 1) {
                    location.href = 'login';
                } else {
                    alert(res.err);
                }
            }, 'json')
        }
    });

后台:

const express = require('express');
const User = require('../models/User');

const router = express.Router();

router.get('/', function (req, res) {
    res.render('register.html');
});

router.post('/signUp', function (req, res) {
    var username = req.body.username; //获取前台传过来的数据
    var password = req.body.password;
    var resData = {};
    User.findOne({
        username: username
    }).then(function (userInfo) {
        if (userInfo) {
            resData.success = 0;
            resData.err = "该用户名已被注册!";
            res.json(resData);//给前台返回数据状态
            return false;
        } else {
            var user = new User({
                username: username,
                password: password,
                image: '../public/img/people.png', //初始化图像
                state: false  //上线状态
            });
            return user.save();
        }
    }).then(function () {
        resData.success = 1;
        resData.message = "注册成功!";
        res.json(resData);
    })
});

module.exports = router;

5.在views创建注册页面login.html,以及在router创建login.js

由于登录页面和注册页面差不多,这里就不详细描述了,具体看代码;

主页面

主页面是整个页面的核心,它包括:在线用户统计,用户信息修改,以及聊天模块;

1.在线用户统计

在线用户统计这里可以分成三个小模块,分别是:在线用户展示,用户上线提醒,和用户离开提醒;

后台:


const server = require('http').createServer(app);
const io = require('socket.io').listen(server);
const User = require('./models/User');

io.on('connection', function (socket) {
    socket.on('login', function (data) {//用户登录
        var username = data.username;
        socket.username = username;
        User.find().then(function (data) {
            for (var i = 0; i < data.length; i++) {
                if (!data[i].state) {
                    data.splice(i, 1);
                }
            }
            socket.emit('loginSuccess', data);
            socket.broadcast.emit('user_list', data);
            socket.broadcast.emit('userIn', username);
        });

    });
    socket.on('disconnect', function (data) {//用户离开
        var username = data.username || socket.username;
        User.findOne({
            username: username
        }).then(function (userInfo) {
            if (userInfo) {
                return User.update({
                    _id: userInfo._id
                }, {
                    state: false
                })
            }
        }).then(function () {
            return User.find();
        }).then(function (data) {
            for (var i = 0; i < data.length; i++) {
                if (!data[i].state) {
                    data.splice(i, 1);
                }
            }
            socket.broadcast.emit('user_list', data);
            socket.broadcast.emit('userOut', username);
        })
    });
});

前台:

<span style="color: #f66;">用户上线提醒</span>

var username = $('#username').text();
var socket = io.connect('http://localhost:8880');
socket.emit('login', {username: username});

//有人加入
socket.on('userIn', function (data) {
    var html = '<li class="tip"><div class="text-center">@ ' + data + ' @上线</div></li>';
    $('#MsgList').append(html);
});

当用户登录成功,跳转到主页面后,前台通过io.connect()与服务器建立websocket连接,开始即时通信;并且同时通过socket.emit('login')向服务器发送用户上线的通知;

服务器通过socket.on('login')接收到前台发送的用户上线消息,然后通过 socket.broadcast.emit('userIn', username)向其他在线用户广播发送,该用户上线的消息;其他用户通过socket.on('userIn')接收消息,并向聊天模块上,添加提示;

<span style="color: #f66;">用户离线提醒</span>

前台:

//有人退出
socket.on('userOut', function (data) {
    var html = '<li class="tip"><div class="text-center">@ ' + data + ' @离开</div></li>';
    $('#MsgList').append(html);
});

//退出
$('#exitBtn').on('click', function () {
    var username = $('#username').text();
    location.href = 'exit';
});

在这里我把用户离线分为三种情况:一是,点击“退出”按钮;二是,关闭浏览器;三是,刷新该页面;但是不管是哪一种情况,只要页面改变了,都会触发服务器的socket.on('disconnect'),更改用户状态,然后服务器进行命令发送,这里和用户上线提醒的操作是一样的;

当在写刷新浏览器的时候,遇到了一个问题;我原本的想法是通过js监控浏览器的事件操作,然而发现这根本是不可能的,虽然在网上有很多帖子,但是通过实现都不能实现;

于是,我就想到了一个方法,当页面第一次通过login页面到主页面的时候,设置两个cookie,一个为user,来判断用户登录的;一个为flag,来判断页面已经加载过了;当用户刷新的时候,判断是否存在cookie-flag,如果存在,证明已经加载过了,就直接退出,用户离线;否则就是用户上线;这个方法可能不是很好,希望大神们有什么好方法,来解决这个问题;

后台:

index.js 主页面

router.get('/home', function (req, res) {
    if (!req.cookies.user) {
        res.redirect('/login');
    } else {
        if (!req.cookies.flag) {
            User.findOne({
                username: req.cookies.user
            }).then(function (userInfo) {
                res.render('home', {
                    username: userInfo.username,
                    image: userInfo.image
                });
            });
        } else {
            res.redirect('/exit');
        }

    }
});

eixt.js 退出

router.get('/', function (req, res) {
    User.update({
        username: req.cookies.user
    }, {
        state: false
    }).then(function () {
        res.clearCookie('user');
        res.clearCookie('flag');
        res.redirect('/login');
    });
});

<span style="color: #f66;">在线用户展示</span>

socket.on('loginSuccess', function (data) {
    userUpdate(data);
});

//更新在线人数列表
socket.on('user_list', function (data) {
    userUpdate(data);
});
function userUpdate(data) {
    var len = data.length;
    var str = '';
    for (var i = 0; i < len; i++) {
        str += '<li>';
        str += '![](' + data[i].image + ')';
        str += '<span>' + data[i].username + '</span>'
    }
    $('#peopleList').html(str);
    $('#list-count span').html(len);
}

这里在线用户展示分两种情况,一是,用户刚刚登陆时,需要在侧边栏展示已在线用户;二是,当有新用户登陆时,其他用户更新在线用户;

第一种情况,在服务器收到socket.on('login')时,通过socket.emit('loginSuccess')向新用户发送在线用户;

第二种情况,在服务器收到socket.on('login')时,通过socket.broadcast.emit('user_list')向其他用户发送更新在线用户的命令;

无论是哪种情况,当服务器在发送命令之前,都需要通过User.find()查询在线用户的信息,才能将消息发送给用户;然而当用户收到命令时,虽然是不同的命令,但是所要做的操作是一样的,所以执行相同的方法userUpdate(),展示在侧边栏;

2.聊天模块

当没有写过之前,能够写个向qq一样能即时聊天的功能,感觉好高大上,真牛逼;但是当真正开始写过之后,就感觉也就那么一会事;

闲话不多说;聊天模块的功能无非就是:发送消息和接收消息;当然这里分了发送接收文字信息和图片;虽然分的是两种,但是操作是一样的,又由于我把接收的信息和自己发送信息的样式做了区别,所以我这里把发送信息和接收信息分别封装了两个方法meSendMsg(msg,n)和getMsg(msg, n);参数“msg”是聊天内容,“n”的值为0和1,分类代表发送的是文字消息和图片消息。

前台:

//发送消息
$('#sendBtn').on('click', function () {
    var msg = $('#msgInput').val();
    if (msg == '') {
        alert('发送内容不能为空!');
        return false;
    }
    var username = $('#username').text();
    var img = $('#userImage').attr('src');
    meSendMsg(msg, 0);
    socket.emit('postNewMsg', {msg: msg, username: username, image: img});
    $('#msgInput').val('');
});

//接收消息
socket.on('newMsg', function (data) {
    getMsg(data, 0);
});

//发送照片
$('#addImage').on('click', function (e) {
    var e = e || window.event;
    e.stopPropagation();
    $('#files').trigger('click');
});

function changeFiles(e) {
    var e = e || window.event;
    var files = e.target.files || e.dataTransfer.files;
    var len = files.length;
    if (len === 0) return false;
    for (var i = 0; i < len; i++) {
        var fs = new FileReader();
        fs.readAsDataURL(files[i]);
        fs.onload = function () {
            var username = $('#username').text();
            var img = $('#userImage').attr('src');
            socket.emit('postImg', {imgData: this.result, username: username, image: img});
            meSendMsg(this.result, 1);
        }
    }
}

//接收照片
socket.on('newImg', function (data) {
    getMsg(data, 1);
});
//自己发送消息
function meSendMsg(msg, n) {
    var src = $('#userImage').attr('src');
    var name = $('#username').text();
    var html = ' <li class="me">';
    html += '<div class="row">';
    html += ' <div class="userInfo col-sm-1 col-md-1 pull-right">';
    html += '![](' + src + ')';
    html += '<p class="text-center">' + name + '</p>';
    html += '</div>';
    html += '<div class="msgInfo col-sm-5 col-md-5 pull-right">';
    if (n == 0) {
        html += msg;
    } else if (n == 1) {
        html += '![](' + msg + ')';
    }

    html += '</div></div></li>';
    $('#MsgList').append(html);
    var Li = $('#MsgList li');
    var len = Li.length;
    var LiH = Li.eq(len - 1).height();
    var h = document.getElementById('MsgList').scrollHeight;
    document.getElementById('MsgList').scrollTop = h + LiH;
}

//接收消息
function getMsg(data, n) {
    var html = ' <li >';
    html += '<div class="row">';
    html += ' <div class="userInfo col-sm-1 col-md-1">';
    html += '![](' + data.image + ')';
    html += '<p class="text-center">' + data.username + '</p>';
    html += '</div>';
    html += '<div class="msgInfo col-sm-5 col-md-5 ">';
    if (n == 0) {
        html += data.msg;
    } else if (n == 1) {
        html += '![](' + data.imgData + ')';
    }
    html += '</div></div></li>';
    $('#MsgList').append(html);
    var Li = $('#MsgList li');
    var len = Li.length;
    var LiH = Li.eq(len - 1).height();
    var h = document.getElementById('MsgList').scrollHeight;
    document.getElementById('MsgList').scrollTop = h + LiH;
}

后台:

 socket.on('postNewMsg', function (data) {//接收到新消息
    socket.broadcast.emit('newMsg', data);
});
socket.on('postImg', function (data) {//接收到图片
    socket.broadcast.emit('newImg', data);
});

在这里,我来说说图片上传和发送;图片不同于文字的传递,但是如果将图片转化为字符串形式后,便可以像发送普通文字消息一样发送图片了,只是在展示的时候将其还原为图片就行;

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

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

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

//发送照片
$('#addImage').on('click', function (e) {
    var e = e || window.event;
    e.stopPropagation();
    $('#files').trigger('click');
});

function changeFiles(e) {
    var e = e || window.event;
    var files = e.target.files || e.dataTransfer.files;
    var len = files.length;
    if (len === 0) return false;
    for (var i = 0; i < len; i++) {
        var fs = new FileReader();
        fs.readAsDataURL(files[i]);
        fs.onload = function () {
            var username = $('#username').text();
            var img = $('#userImage').attr('src');
            socket.emit('postImg', {imgData: this.result, username: username, image: img});
            meSendMsg(this.result, 1);
        }
    }
}

3.用户修改信息

用户修改信息用到的方法,在上面的编写中都用到了;比如:图片上传和发送,用户列表的更新等,这里我就不详细讲了,大家看代码吧,有什么问题请及时call我;

前台:

//修改信息
$('#editImage').on('click', function (e) {
    var e = e || window.event;
    e.stopPropagation();
    $('#fileImg').trigger('click');
});

$('#editBtn').on('click', function () {
    var newName = $('#newName').val();
    var newImage = $('#editImage').attr('src');
    $('#userImage').attr('src', newImage);
    $('#username').html(newName);
    $('#changeInfo').modal('hide');
    socket.emit('edit', {newName: newName, newImage: newImage, username: username});
});
function editImageFn(e) {
    var e = e || window.event;
    var files = e.target.files || e.dataTransfer.files;
    var fs = new FileReader();
    fs.readAsDataURL(files[0]);
    fs.onload = function () {
        $('#editImage').attr('src', this.result);
    }
}

后台:

socket.on('edit', function (data) {
        var username = data.username || socket.username;
        User.findOne({
            username: username
        }).then(function (userInfo) {
            return User.update({
                _id: userInfo._id
            }, {
                username: data.newName,
                image: data.newImage
            })
        }).then(function () {
            socket.username = data.newName;
            return User.find();
        }).then(function (data) {
            for (var i = 0; i < data.length; i++) {
                if (!data[i].state) {
                    data.splice(i, 1);
                }
            }
            socket.emit('user_list', data);
            socket.broadcast.emit('user_list', data);
        });
    })

小结

到此为止,一个简单的即时聊天室完成了;其实,做这个聊天室的时候,有很多设想,比如发送表情和一对一聊天等,这些都是在其中,虽然现在还没有实现,但是请敬请期待;或者你们自己已经实现了,大家可以交流交流;

在这里说说为什么叫“xxx聊天室”,是因为本人语言能力实在是比较差,想不出什么好名字来(哈哈),大家有什么好名字可以自己添加;

本文可能很多地方用词用句都不是很恰当,也有很多地方讲解不清楚,请大家多多原谅,本人的语言表达能力实在是不怎么样,在以后的文章中,会改善的;

原文: http://blog.hawkzz.com/2017/06/01/xxx聊天室/  作者: hawk_zz

上一篇下一篇

猜你喜欢

热点阅读