nodejs+mongoose+websocket搭建xxx聊天室


簡介

本文是由nodejs+mongoose+websocket打造的一個即時聊天系統;本來打算開發一個類似於網頁QQ類似功能的聊天系統,但是目前只是開發了一個模塊功能 --- 類似群聊的,即一對多的聊天模式;因為時間關系,一對一私聊功能還沒有開發,敬請期待!

該聊天室整個頁面布局是通過bootstrap框架編寫,可能很簡陋,請大家多多包涵!

源碼及作品

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

本地運行方法:

  • 命令下載:npm install
  • 啟動node服務器:node app.js
  • 在瀏覽器中打開:http://localhost:8880

下面為效果圖預覽:

環境介紹

  • Windows 7 PC
  • nodejs
  • mongoDB
  • websocket
  • boostrap

准備工作

  • 安裝node(廢話,這肯定是必須的),安裝地址:https://nodejs.org/en/
  • 安裝mongoDB環境,地址:https://www.mongodb.com/download-center#community
  • 創建一個文件夾(這里你隨意);
  • 初始化文件夾,使其變成一個node項目文件夾 npm init 或者 npm init -y;這里講一下這兩種的區別,npm init是初始化但是要自己選擇初始化的條件,npm init -y則是默認選擇初始化內容;
  • 創建app.js文件,作為整個項目的入口文件;
  • 安裝整個項目的依賴,如下:
    1. express框架
    2. mongoose,是mongoDB的一個對象模型工具
    3. cookie-parser
    4. body-parser
    5. swig模板
    6. socket.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;

  • views文件夾:存放html模板文件;
  • public文件夾:存放靜態文件,如:js,css,image
  • 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);
        })
    });
});

前台:

用戶上線提醒

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')接收消息,並向聊天模塊上,添加提示;

用戶離線提醒

前台:

//有人退出
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');
    });
});

在線用戶展示

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 += '<img src="' + data[i].image + '" class="userImg">';
        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 += '<img src="' + src + '" style="width: 100%;margin-bottom: 5px">';
    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 += '<img src="' + msg + '" alt="" style="max-width:100%; ">';
    }

    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 += '<img src="' + data.image + '" style="width: 100%;margin-bottom: 5px">';
    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 += '<img src="' + data.imgData + '" alt="" style="max-width:100%; ">';
    }
    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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM