簡介
本文是由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文件,作為整個項目的入口文件;
- 安裝整個項目的依賴,如下:
- express框架
- mongoose,是mongoDB的一個對象模型工具
- cookie-parser
- body-parser
- swig模板
- 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