博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
使用Express + Socket.io + MongoDB实现简单的聊天室
阅读量:6824 次
发布时间:2019-06-26

本文共 9490 字,大约阅读时间需要 31 分钟。

  hot3.png

基础能力

1. 你需要有一定的JavaScript基础知识. 不推荐没有任何JS基础的人学习Node.js及其生态圈. 如果想学习JS,推荐两本书:<JavaScript高级程序设计><JavaScript权威指南>.

2. 你需要有一定的英文阅读能力.针对Node.js及其生态圈的飞速发展,任何书籍都会很快的过时,但官方文档永远是最新的.目前Node.js的生态圈,如Socket.io,可能并没有中文文档.

3. 你需要对Node.js有一定的认识. 不同于HTML/CSS等前端技术,Node.js目前算是一门成熟的后端技术,需要花大量的时间进行学习.如果想学习Node.js, 推荐官网文档和三本书:<Node.js IN ACTION>(看过,入门型书籍,不错,难度低), <Manning Node.js in Practice>(此书较老,只需要看前八章即可,后面几章程序不一定跑的通,而且不一定是目前主流的选择,难度中等), <Node.js Design Patterns>(只看过前两章,但难度有点大,属于工作后需要深入理解的书籍).

前世之因

我目前的工作主要使用的技术是: 基于tornado框架, 使用Python编写后端业务逻辑,使用JS编写前端业务逻辑(使用jQuery). 随着工作的进行, 沟通的比重越来越大,大概会占到了30%的工作量. 但是前后端的分离是必然的趋势, 而我在工作中遇到的一个问题是: 前端工程师往往看不懂后端写的代码(目前我前后端一起写,所以影响不大),导致一旦接口没编写好,前后端没沟通好,会产生一些莫名的隐患.于是我去年十二月份的时候开始寻找答案, 最终选择了学习Node.js.

工作前三年,我始终坚持做三件事: 1.不以赚钱为目的学习任何我感兴趣的技术. 2. 每天坚持学习90分钟. 3. 对技术怀有感恩的心,感谢它带给我的一切快乐和幸福.

随缘, 不是说一切顺其自然,而是尽全力去完成,至于结果如何则不要太在意.

准备工作

1. 确保你安装了Node.js, Express, MongoDB(我曾经多次运行程序,但是忘记了启动MongoDB服务器)

lgtdeMacBook-Pro:multiroom-chat lgt$ node -vv5.6.0lgtdeMacBook-Pro:multiroom-chat lgt$ express --version4.13.1lgtdeMacBook-Pro:multiroom-chat lgt$ mongo --versionMongoDB shell version: 3.0.2

开始编程

1. 构建Express项目

参考: 

lgtdeMacBook-Pro:~ lgt$ express -e multiroom-chat
    如果对
"-e"参数不理解,输入: express --help. "-e"参数说明我们使用
ejs engine来将后端的数据传递到前端操作的方式.

   接着,我们进入multiroom-chat目录,通过npm install来安装必备的库. 由于我们需要用到socket.io来进行通信,使用mongoose来操作MongoDB, 所以我们额外执行如下指令:

lgtdeMacBook-Pro:multiroom-chat lgt$ npm install --save socket.iolgtdeMacBook-Pro:multiroom-chat lgt$ npm install --save mongoose
    最后运行: npm start, 输入:localhost:3000就可以看到结果了.

2. Express项目结构

bogon:multiroom-chat lgt$ tree -L 1.├── app.js├── bin├── node_modules├── package.json├── public├── routes└── views

app.js: 主文件,直接理解为C/C++的main.cpp即可.

bin: 启动目录. 在bin/www的文件中你可以看到HTTP服务是如何启动,如何绑定端口等信息.

node_modules: 存放所安装模块.

package.json: 项目的基本信息, 最主要的就是所安装的模块版本信息.

public: 存放images/javascripts/stylesheets文件. 在这个项目中,我会把jQuery文件,boostrap文件放在这里.

routes: 存储后台逻辑业务代码文件.

views: 存储展现层的代码文件.

3. 编写需求文档

    这里我只编写了数据库的设计文档, 具体请查看GitHub()中的需求文档目录.

4. 实现登陆注册页面

1) 界面效果图

mongoose参考: 

登陆界面效果图如下:

注册界面效果如下:

2) 数据库表存储结构

    首先,在编写登陆/注册的后台代码时,我们需要使用mongoose来操作数据库:

var mongoose = require('mongoose');// 连接数据库库的users数据表.var db = mongoose.createConnection('localhost', 'multiroom');db.on('error', function(err) {  console.error(err);});var Schema = mongoose.Schema;// 用户表var UserSchema = new Schema({  username: String,  nickname: String,  password: String,  status: String});var UserModel = db.model('users', UserSchema);// 用户关联表var ChatinfoSchema = new Schema({  users: Array,  mapusers: Array});var ChatinfoModel = db.model('chatinfos', ChatinfoSchema);
这里简单讲解一下这段代码:

1. 之所以要使用mongoose来操作数据库,是因为JavaScript作为"非正统"的后端语言,在操作数据库上本身就先天不足.如果涉及复杂的数据库操作,则直接使用MongoDB则不太明智.

2. 使用createConnection创建一个连接本地数据库multiroom(要保证已经开了MongoDB服务)

lgtdeMacBook-Pro:~ lgt$ sudo mongod
3. Schema只是一个数据结构表示,我们使用db.model('users', UserSchema)将这个数据结构和数据表users关联起来. 后期我们可以直接对关联的UserModel对象(需要通过new实例化)进行复制和save操作.

3) 登陆和注册

// 用户登录router.post('/login', function(req, res) {  UserModel.findOne({username: req.body.loginname, password: req.body.loginpwd}, function(err, user) {    if (!user) {      console.error(err);      res.send({status: false, data: '登录失败!', loginname: req.body.loginname});    } else {      res.send({status: true, data: '登录成功!', loginname: req.body.loginname});    }  });});

1. 我们使用findOne查询数据库是否存在此用户.

2. 这里不能判断err,因为无论是否有此用户都不会发生错误,err永远为null.

3. 无论登陆成功还是失败,都要给前端传递具体的信息.所以使用res.send()将数据发送到前端.

// 用户注册router.post('/reg', function(req, res) {  var registername = req.body.registername;  var registerpwd = req.body.registerpwd;  UserModel.findOne({username: registername}, function(err, user) {    if (user) {      res.send({status: false, data: '此用户已经存在!'});    } else {      var user = new UserModel();      user.username = registername;      user.password = registerpwd;      user.save(function(err) {        if (err) throw err;        updateChatinfo(registername);        res.send({status: true, data: '注册成功!', registername: registername});      });    }  });});
1. 这里得预先判断注册的用户是否已经存在.如果没有存在,则执行save操作,将注册的用户存储进数据库.

备注: 这里密码是明文存储的,实际的项目中是绝对不允许的,需要通过一些加密的库进行加密即可.

// 更新chatinfos信息function updateChatinfo(username) {  ChatinfoModel.findOne({}, function(err, data) {    if (err) {      console.error(err);      return;    }    var users = [username],        mapusers = [];    if (data && data.users) users = data.users;    if (data && data.mapusers) mapusers = data.mapusers;    if (data && data.users) {      for (var oneuser of users) {        mapusers.push(oneuser + '_' + username);      }      users.push(username);    }    ChatinfoModel.remove({}, function(err, data) {      if (err) throw err;      var chatinfo = new ChatinfoModel();      chatinfo.users = users;      chatinfo.mapusers = mapusers;      chatinfo.save(function(err) {        if (err) throw err;      });    });  });}
    这里对chatinfo数据表的操作异常的重要(chatinfos表的作用,具体查看"数据库设计文档"). 假设我一次注册了:leicj1, leicj2, leicj3,则数据表chatinfos的信息如下(只存储一条数据):
users: [leicj1, leicj2, leicj3]mapusers: [leicj1_leicj2, leicj1_leicj3, leicj2_leicj3]
这样,我就能保证任意两个人(A和B)的聊天,则其聊天记录会存储在数据表A_B或者B_A.如果A先于B注册,则存储在数据表A_B,如果B先于A注册,则存储在数据表B_A.

而前端的代码查看文件: index.ejs. 在登陆/注册成功后,页面会进行跳转.

4) routes和views文件的关联

    routes编写后台逻辑业务,而views为前端展现:

/* GET home page. */router.get('/', function(req, res, next) {  res.render('index');});

这里使用res.render('index')展现views中的index.ejs文件.这里index的后缀名去掉了.我们添加上也无所谓(但请不要这么无聊...):

/* GET home page. */router.get('/', function(req, res, next) {  res.render('index.ejs');});
我们查看app.js中的一段代码:
app.use('/', routes);app.use('/users', users);
    假设我们在users.js中编写一个post('/chat'),则实际的URL为: /users/chat.

备注: 将GitHub上的public里面的JS/CSS文件拷贝到当前目录中,运行npm start, 在localhost:3000下就可以看到效果.

5. 实现聊天界面

    我在学习Node.js的时候,最惊艳到我的不是它的fs模块,也不是http模块,而是其中的event.EventEmitter思想(QT中就使用emit/on组合进行编程,这是否是它可跨平台开发运行的原因呢?).

    而Socket.io充分使用了emit/on的思想.

Socket.io参考: 

1) 聊天界面效果图

    这里我们需要在routes中新建chat.js来处理聊天界面的后台逻辑,在views中新建chat.ejs来展现聊天界面. 首先我们在app.js中增加两行代码:

var chat = require('./routes/chat');app.use('/chat', chat);
    而chat.js的功能很简单,就是读取所有的用户并将数据发送到前端(具体查看chat.js文件):
router.get('/', function(req, res, next) {  console.log(req.query.currentname);  res.render('chat', {"users": chatinfo.users, 'currentname' : req.query.currentname});});

2) Socket.io的使用

    首先先简单讲解下Socket.io的原理. 操作系统有一个非常伟大的设计就是轮询机制,而Node.js中的callback机制正是基于此机制:

JS的异步编程就是这么来的.但是对于类似聊天这种应用,使用轮询机制明显不合理.轮询机制在于你触发了一个事件后异步处理,但这里异步本身就是硬伤,毕竟聊天要实时的.

    而Node.js中有另外一种伟大的模型: 观察者模式. 即我就一直监听,监听到的某个事件后,执行相应的处理函数.

我们首先在bin/www文件中增加以下代码:

var io = require('socket.io')(server);/**  * socket.io数据处理模块  * */var mongoose = require('mongoose');// 连接数据库var db = mongoose.createConnection('localhost', 'multiroom');db.on('error', function(err) { console.error(err);});var Schema = mongoose.Schema;// 聊天信息表var ChatSchema = new Schema({ from: String, to: String, time: Number, msg: String, status: String});// 获取用户关联表的信息var ChatinfoSchema = new Schema({ users: Array, mapusers: Array});var ChatinfoModel = db.model('chatinfos', ChatinfoSchema);var chatinfo = {};ChatinfoModel.findOne({}, function(err, data) { if (err) throw err; chatinfo = data;});io.on('connection', function(socket) {  console.log('a user connect');  // 处理所有的聊天信息,所传递的参数包括: from(发送者), to(接收者), msg(聊天信息)  socket.on('chat message', function(from, to, msg) {    var time = Date.now();    // 将聊天信息存入数据库中    if (chatinfo.users) {      var ChatModel = db.model(from + "_" + to, ChatSchema);      if (chatinfo.users.indexOf(from) > chatinfo.users.indexOf(to)) {        ChatModel = db.model(to + "_" + from, ChatSchema);      }      var chat = new ChatModel();      chat.from = from;      chat.to = to;      chat.time = Date.now();      chat.msg = msg;      chat.save(function(err) {        if (err) throw err;      });    }    // 将信息发送给to(接收者)    io.emit(to + '_message', from, msg, time);  });});

1. 假设A和B聊天,而且A先于B注册,则A和B的所有聊天信息均存储在数据表A_B中.

2. 我们使用socket.on('chat message', function(from, to, msg))捕获项目中任何地方使用socket.emit('chat message', from, to, msg)发射数据. 任何一次聊天只要发射(emit)"chat message"事件即可,只要传递from(发送者), to(接收者), msg(聊天信息)即可.

3. 我们通过save函数将聊天信息存储起来.并且将此条信息通过io.emit(to + '_message', from, msg, time)发射出去. 如果是A在聊天,则A只要捕获socket.on("A_message"),就可以获取任何人发送给A的信息.

    我们来看下前端chat.ejs的关键代码:

// 发送信息  $('.sendmsg').on('click', function() {    var to = $(this).attr('to');    var msg = $('.message').val();    if (from && to && msg) {      socket.emit('chat message', from, to, msg);      var message = from + " " + new Date().toLocaleString() + "\n" + msg + "\n";      updateMsgForm(message);      $('.message').val('');    }  });  // 接收信息  socket.on(from + '_message', function(from, msg, time) {    var message = from + "  " + new Date(time).toLocaleString() + "\n" + msg + '\n';    updateMsgForm(message);  });

1. 发送信息时候只要socket.emit即可.

2. 这里from指的是当前的用户. 这里接收信息只要socket.on(from + "_message")即可.

6. 项目不足之处和可扩展的功能模块

不足之处:

1) 密码使用明文,应该进行加密操作.

2) 界面很单调(我不会写CSS......)

3) 异步编程不严谨,例如操作B必须在操作A成功后才能执行,但是代码中并未严格遵守.实际项目中要使用promise库.

可扩展的功能模块:

1) 实现未读信息功能.

2) 实现聊天记录的查看和查询.

3) 实现类似QQ讨论组的功能(也可以通过Socket.io来实现,使用broadcast即可)

后记

    我曾经使用tornado + Python + websocket实现了一个聊天室. 但相信我, 那个程序写的又臭又长. 而使用Node.js + Socket.io,就可以轻易写出一个一个聊天室. 我大概花了半天复习下Express的基础知识,再花两个小时那里过一遍Socket.io的官方文档和demo,然后就花了一天半实现了这个聊天室.

    这里使用jQuery而非React.js/Angular.js的原因是: 我对React.js/Angular.js不熟悉.

    我觉得学好前端最重要的是要学好JS.在这里粘贴我在周老师评论下的一个回复,关于学习JS的:

JS是一门语言,需要大量的时间用来专研......很可悲的是:很多人做前端完全是奔着钱去的.他们以很快的速度学习完HTML/CSS,却以同样的心态学习JS. 你跟他们解释说:JS是一门语言,而HTML/CSS仅仅是一门技术.如果想成为好的前端,HTML/CSS仅仅学习一本书后去实践即可,而JS却需要你花大量的时间去专研......

参考网址:

GitHub地址:

Socket.io:

Express:

mongoose:         

转载于:https://my.oschina.net/voler/blog/626226

你可能感兴趣的文章
【redis使用全解析】常见运维操作
查看>>
hdu2377Bus Pass(构建更复杂的图+spfa)
查看>>
2015第29周三
查看>>
CCBValue
查看>>
C#一些知识点:委托和事件的区别
查看>>
Cocos2d-js-v3.2 在 mac 上配置环境以及编译到 Andorid 的注意事项(转)
查看>>
android开源项目学习
查看>>
提升Mac os x 10.10+xcode6.1之后,Cocoapods发生故障的解决方案
查看>>
标准API使用小技巧
查看>>
jQuery Validate插入 reomte使用详细的说明
查看>>
前端设计js+Tab切换可关闭+添加并自动判断是否已打开自动切换当前状态(转载)...
查看>>
for循环,如何结束多层for循环
查看>>
段树 基于单点更新 敌人阵容
查看>>
java中取得上下文路径的方法
查看>>
Tomcat通过配置一个虚拟路径管理web工程
查看>>
Spring、Hello Spring
查看>>
统计学常见分布、概念
查看>>
java的PrintStream(打印输出流)详解(java_io)
查看>>
Redis Keys 命令 - 查找所有符合给定模式( pattern)的 key
查看>>
canvas绘图,html5 k线图,股票行情图
查看>>