实时的数据通信在新的H5的JavascriptAPI出来以前,都是一个让人头疼的问题,实现实时数据展示的几种方式:
- 传统方式:用ajax设置定时器不断的请求后端,发现数据改变时更新前端显示
- Comet方式:一次ajax请求,后端设置一个死循环不返回结果以保持连接,不断的监听数据变化,一旦变化则跳出循环返回数据
- webSocket方式:采用了一些特殊的报头(Header),使得浏览器和服务器只需要做一个握手的动作,就可以在浏览器和服务器之间建立一条连接通道,而毋须消耗大量服务器资源。
功能
- 自动分配聊天室
- 提示添加昵称
- 修改并验证昵称
- 防止xss攻击
- 加入、离开时发送系统消息
- 显示在线人员和个数
- 发送表情
- 为每个用户随机分配颜色
- 当前用户的消息显示在右侧,其它用户消息显示在左则
安装
环境
安装全局的express
npm install express -g
使用express项目生成器快速创建express应用
npm install express-generator -g
创建项目
express -e –git chat
参数说明:-e
使用ejs模板 --git
添加.gitignore文件 项目名称
chat
创建成功后目录结构如下:.
├── app.js
├── bin
│ └── www
├── package.json
├── public
│ ├── images
│ ├── javascripts
│ └── stylesheets
│ └── style.css
├── routes
│ ├── index.js
│ └── users.js
└── views
├── error.ejs
└── index.ejs
添加依赖
打开 package.json{
"name": "chat",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"body-parser": "~1.13.2",
"cookie-parser": "~1.3.5",
"debug": "~2.2.0",
"ejs": "~2.3.3",
"express": "~4.13.1",
"morgan": "~1.6.1",
"serve-favicon": "~2.3.0",
"socket.io": "^1.0.6", #添加socket.io通信模块
"xss-escape": "0.0.5" #添加处理xss的模块
}
}
安装依赖
cd chat & npm install
配置调试项目 并启动预览
set DEBUG = chat & npm start
预览成功
编写前端页面
代码编辑
打开 views/index.ejs
, 编辑内容如下:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Socket聊天室v0.0.1</title>
<!-- Bootstrap-->
<link href="http://libs.baidu.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/css/index.css">
</head>
<body class="container">
<div class="row">
<!-- 聊天区 -->
<div class="col-sm-8"></div>
<!-- 个人信息 -->
<div class="col-sm-4"></div>
<!-- 在线列表 -->
<div class="col-sm-4"></div>
</div>
<!-- 添加或修改昵称弹窗 -->
<div class="modal fade" id="login-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog">
</div>
</div>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="http://cdn.staticfile.org/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
<script src="http://libs.baidu.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
<script src="/js/socket.io.js"></script>
<script src="/js/emotion.js"></script>
<script src="/js/index.js"></script>
</body>
</html>
引入的文件bootstrap
bootstrop主题相关socket.io.js
通过websocket方式处理与服务器的的通信jquery.cookie.js
用于存储用户信息emotion.js
表情插件
客户端文件socket.io.js
来源:node_modules\socket.io\node_modules\socket.io-client\socket.io.js
将其复制到 public\js\
文件夹内(个人习惯:将public文件下的三个文件重新命令成css/img/js三个文件夹)
效果预览
页面逻辑编写
public/js/index.js
/**
* index.js 聊天室页面逻辑
* deps:[jquery.js, socket.io.js, emotion.js]
* email:935486956@qq.com
*/
var chat_Utils, //聊天室 工具类
chat_UI, //聊天室 界面逻辑
chat_Socket; //聊天室 数据逻辑
// 与后台服务器建立websocket连接
var chat_server = "http://" + location.hostname + ':3000';
var socket = io.connect(chat_server);
chat_Utils = {
getLocalHMS: function(){}, //获取当前时间
getUserColor:function(){} //获取随机颜色
}
chat_UI = {
init:function(){
this.initEmotion(); //初始化表情插件
this.nameEditModalShowEv(); //注册 点击弹出修改昵称弹窗事件
this.sendMsgEv(); //注册 发送消息事件
this.subNameEv(); //注册 提交昵称事件
this.loginModalShowEv(); //弹窗打开时调整弹窗样式
this.loginModalShownEv(); //弹窗打开后input获取焦点
},
initEmotion:function(){},
chatBodyToBottom: function(){},
addMessage: function(_time, _content, _name, clr){},
removeListUser: function(_user){},
addUserToList: function(_user){},
useUserList: function(_user_list){},
updateListCount: function(){},
sendMessage: function(){},
applyNickname: function(){},
sendMsgEv:function(){},
nameEditModalShowEv:function(){},
subNameEv:function(){},
loginModalShowEv:function(){},
loginModalShownEv:function(){}
};
chat_Socket = {
init:function(){
console.log('server:' + chat_server);
this.needNicknameEv(); //监听后端 需要昵称 事件
this.serverMessageEv(); //监听后端 用户新消息 事件
this.changeNicknameErrorEv(); //监听后端 昵称错误 事件
this.changeNicknameDoneEv(); //监听后端 昵称添加成功 事件
this.sayDoneEv(); //监听后端 用户新消息 事件
this.userListEv(); //监听后端 显示用户列表 事件
this.userChangeNicknameEv(); //监听后端 修改昵称提示 事件
this.userJoinEv(); //监听后端 新用户加入 广播
this.userQuitEv(); //监听后端 用户离开 广播
this.userSayEv(); //监听后端 其它用户消息 广播
},
changeNickname:function(_nickname, clr){},
say:function(_content){},
needNicknameEv:function(){},
serverMessageEv:function(){},
changeNicknameErrorEv:function(){},
changeNicknameDoneEv:function(){},
sayDoneEv:function(){},
userListEv:function(){},
userChangeNicknameEv:function(){},
userJoinEv:function(){},
userQuitEv:function(){},
userSayEv:function(){}
}
chat_UI.init();
chat_Socket.init();
几个要注意的地方:
- 当前用户的昵称和颜色在后端保存于当前的socket对象中
- 判断当前连接的用户是否与页面中的用户相同,以便判断消息对齐方式
编写服务端
app.js
不用修改,因为是单页面,注释掉users相关代码即可// var users = require('./routes/users');
// app.use('/users', users);
根目录下新建 chat_server.js
var io = require('socket.io')();
var xssEscape = require('xss-escape');
var nickname_list = [];
// 检查是昵称是否已经存在
function HasNickname(_nickname){
for(var i=0; i<nickname_list.length; i++){
if(nickname_list[i] == _nickname){
return true;
}
}
}
// 删除昵称
function RemoveNickname(_nickname){
for(var i=0; i< nickname_list.length; i++){
if(nickname_list[i] == _nickname){
nickname_list.splice(i, 1);
}
}
}
io.on('connection', function(_socket){
console.log(_socket.id + ':connection');
// 向当前用户发送命令和消息
_socket.emit('user_list', nickname_list);
_socket.emit('need_nickname');
_socket.emit('server_message','欢迎来到聊天室 :)');
// 监听当前用户的请求和数据
// 离开
_socket.on('disconnect', function(){
console.log(_socket.id + ':disconnect');
if(_socket.nickname != null && _socket.nickname != ""){
// 广播 用户退出
_socket.broadcast.emit('user_quit', _socket.nickname);
RemoveNickname(_socket.nickname);
}
});
// 添加 和 修改 昵称
_socket.on('change_nickname', function(_nickname, clr){
console.log(_socket.id + ': change_nickname('+_nickname+')');
_nickname = xssEscape(_nickname.trim());
// 半角替换为tt,模拟为全角字符判断长度
var name_len = _nickname.replace(/[^\u0000-\u00ff]/g, "tt").length;
// 字符长度必须在4到16个字符之间
if(name_len < 4 || name_len > 16){
return _socket.emit('change_nickname_error', '请填写正确的用户昵称,应在4到16个字符之间。')
}
// 昵称重复
if(_socket.nickname == _nickname){
return _socket.emit('change_nickname_error', '你本来就叫这个名字。')
}
// 昵称已经被占用
if(HasNickname(_nickname)){
return _socket.emit('change_nickname_error', '此昵称已经被占用。')
}
var old_name = '';
if(_socket.nickname != '' && _socket.nickname != null){
old_name = _socket.nickname;
RemoveNickname(old_name);
}
nickname_list.push(_nickname);
_socket.nickname = _nickname;
_socket.color = clr;
console.log(nickname_list);
_socket.emit('change_nickname_done', old_name, _nickname, clr);
if(old_name == ''){
// 广播 用户加入
return _socket.broadcast.emit('user_join', _nickname);
}else{
// 广播 用户改名
return _socket.broadcast.emit('user_change_nickname', old_name, _nickname);
}
});
// 说话
_socket.on('say', function(_content){
if('' == _socket.nickname || null == _socket.nickname){
return _socket.emit('need_nickname');
}
_content = _content.trim();
console.log(_socket.nickname + ': say('+_content+')');
// 广播 用户新消息
_socket.broadcast.emit('user_say', _socket.nickname, xssEscape(_content), _socket.color);
return _socket.emit('say_done', _socket.nickname, xssEscape(_content), _socket.color);
});
})
// 这里的listen函数在 bin/www 文件中被调用
exports.listen = function(_server){
return io.listen(_server);
}
去掉 routes/index.js
中返回到页面的 title
参数/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index');
});
下一节 将介绍koa.io 聊天室的具体实现