本系统主要参考https://juejin.im/post/5a73ddcff265da4e81237429和https://www.cnblogs.com/hawk-zz/p/6934880.html
有用户和管理员两种角色,用户可以随意注册但管理员不能注册,如果数据库中没有管理员信息,后台会自动生成一个管理员。用户成功登陆会进入聊天室,管理员成功登陆会进入管理员界面。
node-chat3/
|
+- models/
| |
| +- Admin.js
| +- User.js
+- node_modules/ <--npm安装的所有依赖包
+- public/
| |
| +- css/
| | |
| | +- css.css <--负责用户聊天室的css
| | +- bootstrap-theme.min.css
| | +- bootstrap.min.css
| +- fonts/
| +- img/
| +- js/
| | |
| | +- js.js <--负责用户聊天室的js
| | +- admin_js.js <--负责管理员界面的js
| | +- bootstrap.min.js
| | +- jquery-1.11.2.min.js
+- routers/
| |
| +- register.js <--管理注册的路由
| +- login.js <--管理登录的路由
| +- index.js <--一个什么都有的路由(我不知道要叫它什么)
| +- exit.js <--管理退出的路由
+- schemas/
| |
| +- admin.js <--定义管理员数据库的结构
| +- user.js <--定义用户数据库的结构
+- views/
| |
| +- register.html <--注册界面
| +- login.html <--登录界面
| +- home.html <--聊天室界面
| +- admin.html <--管理员界面
+- app.js <--入口
+- package-lock.json
+- package.json
1、源代码在此:https://github.com/Clara-hy/node-chat,下载到本地,如果下载后的压缩包以.zip.egt结尾,那就把egt删掉,将后缀变成zip结尾就可以进行解压了。下面我将代码下载到E:盘进行演示,前提是已经下载好Node.js和MongoDB
2、本地运行的方法:在cmd里依次敲入"npm install"和"node app.js",然后在浏览器中打开:http://localhost:8880即可。如果中途遇到有报错,对着报错信息解决
3、详细的操作步骤:
聊天室界面
支持发表情和图片,可以修改昵称、头像和密码。可以进行多用户聊天,我没截出来了。注意:最好用不同的浏览器代表不同的用户,因为如果是同一个浏览器扮演多个用户,虽然效果也能实现,但是在数据库里会看到session一直被替代为最近的那个用户或管理员,只有多个浏览器是才会有多条session。如下图,我用chrome登录“ddd”的账号,qq浏览器登录“kaka”的账号:
管理员界面
允许管理员做的:①添加管理员 ②重置用户状态(用户上线为true,用户下线为false。因为一些不可控的原因会导致用户不正常下线,即用户下线了但状态仍未true,这会使得用户下一次无法登陆到聊天室。这是就要管理员将用户的状态重置一下了)③删除用户 ④修改管理员自己的密码
在管理员界面可以看到用户的密码是加密了的,这里用到的是SHA256加密,后面会讲到。
管理员界面未实现的有:①搜索 ②分页显示
4、如果遇到数据库报错,也许是因为数据库没打开,那应该要先启动MongoDB了。打开它的路径是:D:\Mongo\bin\mongod.exe
如果出现框内的信息则表明成功打开,否则失败。同时我下载了MongoDB Compass Community,方便查看数据库的变化(如果想只看效果,可以先不打开)。总的来说就是遇到报错去百度吧
5、如果遇到npm下载不了或者比较慢的,可以用cnpm。我的解决方法是:
# 第一步:查看自己的安装源(显示为https://registry.npmjs.org/
npm config get registry
# 第二步:更换npm源为国内淘宝镜像
npm config set registry http://registry.npm.taobao.org/
# 第三步:下载需要的东西
npm install [email protected] --save
# 第四步:还原npm源
npm config set registry https://registry.npmjs.org/
讲解的思路分成:①贴参考链接 ②路由 ③一些功能的实现
关于聊天室,有三个完整的可以实现帖子,从易到难:(我都自己实现过,现在这个系统大部分是源于第三个,我只是在它上面加功能而已)
https://www.jianshu.com/p/b608a765519a--->https://juejin.im/post/5a73ddcff265da4e81237429--->https://www.cnblogs.com/hawk-zz/p/6934880.html
要善于用F12来看报错信息
这个路由不是我们上网用的路由,而是一个管理请求入口和页面映射关系的东西。
路由在整个实现中起的作用最重要,我自己也是花了很多的时间去学习怎么配路由。一旦懂路由怎么运行之后,读懂代码或者是往系统里加功能都是很快、很容易上手的。
本系统的路由是用express框架搭建的,首先要去了解express是什么,有以下几个链接:不懂就实操+多看或者找其他的帖子看叭
https://www.w3cschool.cn/nodejs/nodejs-express-framework.html
http://www.expressjs.com.cn/starter/installing.html
对我代码里的路由进行简单的讲解
目的:为了能够调用public文件夹和node_modules文件夹里的资源。不托管的话会显示资源调用失败。
// app.js
//静态文件托管
app.use('/public', express.static(__dirname + '/public'));
app.use('/node_modules', express.static(__dirname + '/node_modules'));
app.use的第一个参数是绑定的路由,第二个参数是访问资源的实际位置,因为这是在app.js里写的,所以这个实际位置要以app.js所处的位置为标准。
//app.js
app.use('/', require('./routers/index'));//任何指向/的http请求都会执行它
app.use('/login', require('./routers/login'));// 挂载至 /login 的中间件,任何指向 /login 的http请求都会执行它
app.use('/register', require('./routers/register'));
app.use('/exit', require('./routers/exit'));
拿“app.use('/', require('./routers/index'));”举例,
首先先看一下app.js和index.js的位置,index.js是在routers文件夹里,app.js在routers文件夹外面,所以app.js要访问index.js的话路径是“./routers/index”。"./"为当前目录,"/"为根目录,"/routers"根目录下的routers目录。“./routers/index”的解释就是:app.js当前目录下的routers目录里的index.js文件,app.js的当前目录是node-chat3这个总文件。
“app.use('/', require('./routers/index'));”的效果是:将“/”映射到require('./routers/index')这个路由。说的直白点就是,当我输入网址:localhost:8880/是等价于localhost:8880/index。所以,下面这三个的也是这样。
app.use('/login', require('./routers/login'));// localhost:8880/login <=> login.js
app.use('/register', require('./routers/register'));// localhost:8880/register <=> register.js
app.use('/exit', require('./routers/exit'));// localhost:8880/exit <=> exit.js
话说回来,index.js是什么呢?截取它里面的一部分代码:
// index.js
router.get('/', function (req, res) {
res.redirect('/login');
});
router.get会处理来自'/'的路由,处理的结果是重定向到'/login'路由,意味着localhost:8880/会定向到localhost:8880/login,而/login对应的是login.js,截取它里面的一部分代码:
// login.js
router.get('/', function (req, res) {
res.render('login.html');
});
router.get是处理来自'/'的路由,会返回login.html文件,这是一个登录页面。所以登录界面就是这么产生的啦
// login.html
$('#login').on({
$.post('/login/signIn', {username: username, password: password, kind: kind, captcha:captcha}, function (res) {
if (res.success == 1) {
location.href = 'home';
}else if(res.success == 2){
location.href = 'admin';
}
else {
alert(res.err);
}
}, 'json');
})
// login.js
router.post('/signIn', function (req, res, next) {
// 如果满足用户的登录条件,则将res.success赋值为1(详细请看源码)
})
login.html里的$.post的第一个参数'/login/signIn'实际对应的是localhost:8880/login/signIn,即 将第二个参数里的数据对象post到localhost:8880/login/signIn里,所以在 login.js里就要写一个处理来自/signIn的请求。(router.post的第一个参数是'/signIn',不是'/login.signIn',因为 login.js本身就代表'/login'这个路由)
当login.js处理完之后,res.success = 1,返回到 login.html,判断res.success是否等于1,判断为true,所以路由会定位到'/home',即localhost:8880/home。但是看app.js那里,我并没有绑定/home的路由,/home的实现是在index.js那里:
// index.js
router.get('/home', function (req, res) {
//...其他的代码
res.render('home', {
username: userInfo.username,
image: userInfo.image
});
});
通过res.render,渲染出home.html,并且将用户名和头像传给了home.html,使得页面能调用两个变量。(用swig模板引擎调用变量,但是swig已经淘汰了,现在一般会用ejs)
管理员的话就是从登录界面到管理员界面,思路大致相同。
具体看https://www.jianshu.com/p/b608a765519a和https://juejin.im/post/5a73ddcff265da4e81237429吧,emit、on什么的都比较详细。我的理解是,on是绑定事件,emit是触发事件。比如在服务器端(app.js)绑定一个login(登录)事件,当用户成功登陆时客户端(js.js)会触发login事件。
// js.js
socket.emit('login', {username: username});
// app.js
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);
i = i-1;
}
}
socket.emit('loginSuccess', data);// 服务器端触发loginSuccess事件
socket.broadcast.emit('user_list', data);
socket.broadcast.emit('userIn', username);
socket.emit('user_list', data);
socket.emit('userIn', username);
});
});
// js.js
socket.on('loginSuccess', function (data) {// 绑定在客户端的loginSuccess事件
userUpdate(data);
});
function userUpdate(data) {
var len = data.length;
var str = '';
for (var i = 0; i < len; i++) {
str += '';
str += '';
str += '' + data[i].username + ''
}
$('#peopleList').html(str);
$('#list-count span').html(len);
}
每一个用户登录成功后,服务端会先建立一个包含所有在线人员的数组,然后触发绑定在客户端的“loginSuccess”事件和“user_list”事件,这两个事件都将调用userUpdate函数,该函数是用来渲染列表的。
包含以下几个内容:用户上线/下线提醒、发送/接收文字信息、表情功能和图片功能。
用户上线/下线提醒:用户登录成功后,服务端还会触发“userIn”事件,该事件也是绑定在客户端的。用户下线则触发“userOut”事件。
// app.js
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);
i = i-1;
}
}
socket.emit('loginSuccess', data);// 服务器端触发loginSuccess事件
socket.broadcast.emit('user_list', data);
socket.broadcast.emit('userIn', username);// 服务器端触发userIn事件
socket.emit('user_list', data);
socket.emit('userIn', username);
});
});
// js.js
socket.on('userIn', function (data) { //有人加入
var html = '@ ' + data + ' @上线 ';
$('#MsgList').append(html);
});
发送/接收文字信息:
发送信息是在按下“sendBtn”之后发生的。先检查输入框是否为空,不可以发送空信息。然后存入发送方的昵称和头像,调用meSendMsg(msg,0)和触发绑定在服务器端的“postNewMsg”事件,最后将输入框置空。meSendMsg的第二个参数是将文字和图片区分开,0为文字发送,1为图片发送。meSendMsg是实现聊天框中右侧的显示。postNewMsg会触发newMsg事件,该事件调用getMsg函数,实现聊天框中左侧的显示。发送/接收信息的思路:
表情功能:思路是先将表情显示到页面,然后点击某一表情能解析对应的格式,最后按下发送按钮之后客户端能解析表情并将表情显示出来。具体实现方法:先将表情下载到本地,用“emoji(X)”的格式进行编排。调用init函数在页面渲染表情,点击表情时将表情解析为“[emojiX]”的格式进行发送。表情详解
// js.js
// 渲染表情
init();
function init() {
for(var i = 0; i < 141; i++) {
$('.emoji').append(``);
}
}
// 用户点击表情
$('.emoji li img').click((ev)=>{
ev = ev||window.event;
var src = ev.target.src;
var emoji = src.replace(/\D*/g, '').substr(6,3);/*将src非数字的字符转为空字符。substr的第二个参数3是指长度为3*/
var old = $('#msgInput').val();
$('#msgInput').val(old+'[emoji'+emoji+']');
$('.selectBox').css('display', "none");
});
// 我方发送信息时调用该函数。n=0为发送文字or表情,n=1为发送图片
function meSendMsg(msg, n) {
var src = $('#userImage').attr('src');
var name = $('#username').text();
var html = ' ';
html += '';
html += ' ';
html += '';
html += '' + name + '
';
html += '';
html += '';
if (n == 0) {
// 将msg进行过滤(即表情转换)
let content = '';
while(msg.indexOf('[') > -1) {
var start = msg.indexOf('[');
var end = msg.indexOf(']');
content += `${msg.substr(0, start)}`;
content += ``
msg = msg.substr(end+1);
}
if(msg.length !== 0)content += `${msg.substr(0)}`;
// alert(content);//用来测试
html += content;
} else if (n == 1) {
html += '';
}
html += ' ';
$('#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;
}
管理员界面:用的是bootstrap-table插件将用户的数据显示出来。参考链接1,参考链接2
加密:用户密码加密指用户登录时对用户密码的加密,防内部攻击和外部攻击。防内部攻击是指避免管理员拿到用户的明文密码冒充用户,防外部攻击是指如果网站被黑客入侵,黑客也只能拿到加密后的密码,而不是用户的明文密码。
加密手段是密码加盐,原理是在密码尾部插入用户唯一的id的后五位数字,再对修改后的字符串进行SHA256运算。SHA256运算用到Node.js里的crypto[ˈkrɪptoʊ]模块。
使用SHA256而不使用MD5或者SHA-1的原因是MD5是一个弱哈希算法,相对容易发生碰撞,SHA-1是已经被破解的哈希算法,这两种不适合再用来加密。参考链接
因为用户密码是加了密的,所以如果用户忘记密码可以在登陆界面点击“重置密码”进行重置。
验证码:使用的模块是svg-captcha[ˈkæptʃə]。需要用到session:什么是session,express-session设置session详解
效果:进入登录界面时,会产生一个包含正确验证码信息的session,这个session保存在数据库中,用户或管理员在点击登录按钮后,先对输入的验证码与数据库中的验证码进行匹配,如果正确则去检查用户名和密码是否正确,如果验证码不匹配则要求重新出入验证码。
// index.js
router.get('/captcha',(req,res)=>{
const cap = captcha.create({
inverse: false,// 翻转颜色
fontSize: 36,// 字体大小
noise: 3,// 噪声线条数
width: 80,// 宽度
height: 30,// 高度
size: 4,
ignoreChars: '0o1ilOIL',
color: true,
background: '#cc9966'
});
req.session.captcha = cap.text.toLowerCase(); // session 存储
res.type('svg'); // 响应的类型
res.send(cap.data);
});
// login.html
1、实现图片发送、修改头像等与图片相关的
// 用户更换头像时调用的函数
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);
}
}
// 点击“发送图片”按钮时调用的函数
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);
}
}
}
2、实现表情发送
3、Bootstrap的Model(模态框)插件。Model的目的是显示来自一个单独的源的内容,可以在不离开父窗体的情况下有一些互动。子窗体可提供信息、交互等。
本系统用Model的地方很多,比如用户修改密码、修改昵称头像,好处:不用写页面就能实现这些功能。
4、Mongoose的整个过程:在 Mongoose 中,所有数据都由Schema[ˈskiːmə] 开始创建。Schema定义了数据库的数据结构,比如管理员数据库中,有管理员名字、密码和登录状态,而且这三个都是String。 Model是由Schema编译而成的构造器,具有抽象属性和行为,可以对数据库进行增删查改。
Mongooose中,有三个比较重要的概念,分别是Schema、Model、Entity。它们的关系是:Schema生成Model,Model创造Document,Model和Document都可对数据库操作造成影响,但Model比Document更具操作性
Schema用于定义数据库的结构。类似创建表时的数据定义(不仅仅可以定义文档的结构和属性,还可以定义文档的实例方法、静态模型方法、复合索引等),每个Schema会映射到mongodb中的一个collection,Schema不具备操作数据库的能力
Model是由Schema编译而成的构造器,具有抽象属性和行为,可以对数据库进行增删查改。Model的每一个实例(instance)就是一个文档document
Document是由Model创建的实体,它的操作也会影响数据库
MongoDB数据库的资料:Mongoose基础入门,初入 mongo,第一次 nosql,MongoDB入门指南,详细图解mongodb下载、安装、配置与使用,mongoose入门(我反复看了挺多遍的)
5、Mongoose的增删查改 代码参考:Mongoose基础入门
// 增 save()、create()、insertMany()
var mongoose = require('mongoose');
mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
if(!err){
var schema = new mongoose.Schema({ age:Number, name: String});
var temp = mongoose.model('temp', schema);
//使用链式写法
new temp({age:10,name:'save'}).save(function(err,doc){
//[ { _id: 59720bc0d2b1125cbcd60b3f, age: 10, name: 'save', __v: 0 } ]
console.log(doc);
});
}
});
// 删 remove()
temp.find({name:/huo/},function(err,doc){
doc.forEach(function(item,index,arr){
item.remove(function(err,doc){ //文档的remove()方法的回调函数参数可以省略
//{ _id: 5971f93be6f98ec60e3dc86c, name: 'huochai', age: 30 }
//{ _id: 5971f93be6f98ec60e3dc86e, name: 'huo', age: 60 }
console.log(doc);
})
})
})
//查 find()、findById()、findOne()
var mongoose = require('mongoose');
mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
if(!err){
var schema = new mongoose.Schema({ age:Number, name: String});
var temp = mongoose.model('temp', schema);
temp.find(function(err,docs){
//[ { _id: 5971f93be6f98ec60e3dc86c, name: 'huochai', age: 27 },
//{ _id: 5971f93be6f98ec60e3dc86d, name: 'wang', age: 18 },
//{ _id: 5971f93be6f98ec60e3dc86e, name: 'huo', age: 30 },
//{ _id: 5971f93be6f98ec60e3dc86f, name: 'li', age: 12 } ]
console.log(docs);
})
}
});
// 改 update()、updateMany()
var mongoose = require('mongoose');
mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
if(!err){
var schema = new mongoose.Schema({ age:Number, name: String});
var temp = mongoose.model('temp', schema);
temp.update({age:{$gte:20}},{age:40},function(err,raw){
//update()方法中的回调函数不能省略,否则数据不会被更新。如果回调函数里并没有什么有用的信息,则可以使用exec()简化代码
//{ n: 1, nModified: 1, ok: 1 }
console.log(raw);
})
}
});
6、socket是什么:客户端<=>服务器双向通信的技术
// 很简单就可以跑起来了
var app = require('express');
app.get('/', function(req, res){
res.send("hello express");
})
app.listen(3001);
// 请求参数一般有四种:路径参数、查询字符串、表单、json
// 响应有两种:纯文本(res.send) json(res.json)
var express = require('express');
var app = express();
//使用中间件
app.use(express.urlencoded({extended: false}))//解析表单的中间件
app.use(express.json())//解析json的中间件
//添加静态路径
app.use("/static", express.static("static"))
app.get('/', function(req, res){
console.log(req.headers);//请求头
res.header("b","c");//设置响应头部,b属性的值为c(跨域有用到)
res.send("hello express");
})
//路径参数 localhost:3001/path/1
app.get('/path/:id', function(req, res){
console.log(req.params.id);//1
res.send(req.params.id);
})
//查询字符串 localhost:3001/querystring?username=haha
app.get('/querystring', function(req, res){
console.log(req.query.username);//haha
res.send(req.query.username);
})
//表单(需要添加中间件来解析表单)
app.post('/form', function(req, res){
console.log(req.body);//表单提交一般会放在body里
res.send(req.body.username);
})
//json(需要添加中间件来解析json)
app.post('/json0', function(req, res){
console.log(req.body);
res.send(req.body.username);
//res.json(req.body);res.json后面接的是对象
})
app.listen(3001);
关于Bootstrap
关于加密
关于Mongodb
关于Node
websocket的几个方法
用户修改密码等用了bootstrap的模态框
bootstrap-table与mongodb的交互
$.post和$.get的使用(ajax的封装)