原文地址:http://my.oschina.net/voler/blog/626226#OSC_h4_11
1. 确保你安装了Node.js, Express, MongoDB(我曾经多次运行程序,但是忘记了启动MongoDB服务器)
lgtdeMacBook-Pro:multiroom-chat lgt$ node -v
v5.6.0
lgtdeMacBook-Pro:multiroom-chat lgt$ express --version
4.13.1
lgtdeMacBook-Pro:multiroom-chat lgt$ mongo --version
MongoDB shell version: 3.0.2
参考: http://expressjs.com/
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.io
lgtdeMacBook-Pro:multiroom-chat lgt$ npm install --save mongoose
最后运行: npm start, 输入:localhost:3000就可以看到结果了.
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: 存储展现层的代码文件.
这里我只编写了数据库的设计文档, 具体请查看GitHub(https://github.com/leicj/multiroom-chat)中的需求文档目录.
mongoose参考: http://mongoosejs.com/
登陆界面效果图如下:
注册界面效果如下:
首先,在编写登陆/注册的后台代码时,我们需要使用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操作.
// 用户登录
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. 在登陆/注册成功后,页面会进行跳转.
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下就可以看到效果.
我在学习Node.js的时候,最惊艳到我的不是它的fs模块,也不是http模块,而是其中的event.EventEmitter思想(QT中就使用emit/on组合进行编程,这是否是它可跨平台开发运行的原因呢?).
而Socket.io充分使用了emit/on的思想.
Socket.io参考: http://socket.io/get-started/chat/
这里我们需要在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});
});
首先先简单讲解下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")即可.
不足之处:
1) 密码使用明文,应该进行加密操作.
2) 界面很单调(我不会写CSS......)
3) 异步编程不严谨,例如操作B必须在操作A成功后才能执行,但是代码中并未严格遵守.实际项目中要使用promise库.
可扩展的功能模块:
1) 实现未读信息功能.
2) 实现聊天记录的查看和查询.
3) 实现类似QQ讨论组的功能(也可以通过Socket.io来实现,使用broadcast即可)