AngularJS+Node.js实现在线聊天室


Posted in Javascript onAugust 28, 2015

不得不说,上手AngularJS比我想象得难多了,把官网提供的PhoneCat例子看完,又跑到慕课网把大漠穷秋的AngularJS实战系列看了一遍,对于基本的使用依然有很多说不清道不明的疑惑,于是决定通过做一个在线聊天室帮助理解。DEMO可以戳→chat room,代码可以戳→ChatRoom-AngularJS。

清晰图可以戳 //f.3water.com/f/6amI1aMS5ueZXQu/03089802d93e6e8e0bc29c3e39c4bc74.gif

AngularJS+Node.js实现在线聊天室

功能

着手开发之前,首先明确一下需要实现的功能:

新用户登入,广播通知其他用户
用户下线,广播通知其他用户
可显示在线人数及列表
可群聊,可私信
用户若发送群消息,广播通知其他所有用户
用户若发送私信,单独通知收方界面

因为自己是个审美渣,所以全靠bootstrap了,另外还模仿了下微信聊天记录里的气泡设计。

界面分左右两个板块,分别用于显示在线列表和聊天内容。

在左侧的在线列表中,点击不同项可以切换右侧板块的聊天对象。

右侧显示与当前聊天对象的对话记录,不过仅显示最近的30条。每一条聊天记录内容包括发送人的昵称及头像、发送时间、消息内容。关于头像,这里做简单处理,用填充了随机色的方块代替。另外,自己发出去的消息与收到的消息样式自然要做不同设计,所有效果可以看下图。

清晰图可以戳 //f.3water.com/f/6amI1aMS5ueZXQu/1c2a55069961acd2672f4e3f73a59bef.png

AngularJS+Node.js实现在线聊天室

服务端

服务端我们用Node.js以及混入express、socket.io来开发,在程序根目录打开终端,执行:

npm init

根据提示,生成一个package.json文件。打开并配置依赖项:

"dependencies": {
  "express": "^4.13.3",
  "socket.io": "^1.3.6"
 }

之后执行 npm install 安装依赖模块。

接下来,我们在根目录下新建app.js,在其中写Server端代码。再新建public文件夹,存放client端代码。

app.js中主要内容如下:

var express = require('express');
var app = require('express')();
var http = require('http').createServer(app);
var io = require('socket.io')(http);

app.use(express.static(__dirname + '/public'));


app.get('/', function (req, res) {
  res.sendfile('index.html');
});


io.on('connection',function(socket){
  socket.on('addUser',function(data){ //有新用户进入聊天室
  });

  socket.on('addMessage',function(data){ //有用户发送新消息
  });
  
  socket.on('disconnect', function () { //有用户退出聊天室
  );
});

http.listen(3002, function () {
  console.log('listening on *:3002');
});

在上面的代码中,我们为以下事件添加了监听:

-addUser,有新用户进入聊天室

该事件由客户端输入昵称后触发,服务端收到后对昵称是否已存在进行判断,如果已存在,通知客户端昵称无效:

socket.emit('userAddingResult',{result:false});

反之,通知客户端昵称有效以及当前所有已连接的用户信息,并把新用户信息广播给其他已连接用户:

socket.emit('userAddingResult',{result:true});
allUsers.push(data);//allUsers保存了所有用户
socket.emit('allUser',allUsers);//将所有在线用户发给新用户
socket.broadcast.emit('userAdded',data);//广播欢迎新用户,除新用户外都可看到

其中需要注意'socket.emit'与'socket.broadcast.emit'的区别,可以查看这篇博文socket.io emit的几种用法解释:

// send to current request socket client
socket.emit('message', "this is a test");
// sending to all clients except sender
socket.broadcast.emit('message', "this is a test");

-addMessage,有用户发送新消息

在此事件监听里,需要分成两类情况处理:

1.私信
如果消息是发给特定用户A,那么就需要获取A对应的socket实例,然后调用其emit方法。所以每当一个客户端连接到Server端时,我们得把其socket实例保存起来,以备后续之需。

connectedSockets[nickname]=socket;//以昵称作下标,保存每个socket实例,发私信需要用

需要发私信时,取出socket实例做操作即可:

connectedSockets[nickname].emit('messageAdded',data)

2.群发
群发就比较简单了,用broadcast方法即可:

socket.broadcast.emit('messageAdded',data);//广播消息,除原发送者外都可看到

-disconnect,有用户退出聊天室
需要做三件事情:

1.通知其他用户“某用户下线”

socket.broadcast.emit('userRemoved', data);

2.将用户从保存了所有用户的数组中移除

3.将其socket实例从保存了所有客户端socket实例的数组中移除

delete connectedSockets[nickname]; //删除对应的socket实例

运行一下服务端代码,观察有无错误:

node app.js

若没什么问题,继续编写客户端的代码。

客户端

在public目录下新建'index.html',客户端需要用到bootstrap、angularjs、socket.io、jQuery以及我们自己的js和css文件,先把这些文件用标签引入。

<!DOCTYPE html>
<html>
<head lang="en">
  <meta charset="UTF-8">
  <title></title>
  <link href="http://cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
  <link rel="stylesheet" href="./assets/style/app.css"/>
  <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
  <script src="/socket.io/socket.io.js"></script>
  <script src="//cdn.bootcss.com/angular.js/1.4.3/angular.min.js"></script>
  <script src="./assets/js/app.js"></script>
</head>
<body></body>
</html>

我们并不立即深入逻辑细节,把框架搭好先。
首先,在body上加上ng-app属性,标记一下angularjs的“管辖范围”。这个练习中我们只用到了一个控制器,同样将ng-controller属性加到body标签。

<body ng-app="chatRoom" ng-controller="chatCtrl">

接下来在js中,我们来创建module及controller。

var app=angular.module("chatRoom",[]);
app.controller("chatCtrl",['$scope','socket','randomColor',function($scope,socket,randomColor){}]);

注意这里,我们用内联注入添加了socket和randomColor服务依赖。这里我们不用推断式注入,以防部署的时候用uglify或其他工具进行了混淆,变量经过了重命名导致注入失效。
在这个练习中,我们自定义了两个服务,socket和randomColor,前者是对socket.io的包装,让其事件进入angular context,后者是个可以生成随机色的服务,用来给头像指定颜色。

//socket服务
app.factory('socket', function($rootScope) {
  var socket = io(); //默认连接部署网站的服务器
  return {
    on: function(eventName, callback) {...},
    emit: function(eventName, data, callback) {...}
  };
});

//randomcolor服务
app.factory('randomColor', function($rootScope) {
  return {
    newColor: function() {
      return '#'+('00000'+(Math.random()*0x1000000<<0).toString(16)).slice(-6);//返回一个随机色
    }
  };
});

注意socket服务中连接的语句“var socket = io();”,我们并没有传入任何url,是因为其默认连接部署这个网站的服务器。

考虑到聊天记录以及在线人员列表都是一个个逻辑及结构重复的条目,且html结构较复杂,为了其复用性,我们把它们封装成两个指令:

app.directive('message', ['$timeout',function($timeout) {}])
  .directive('user', ['$timeout',function($timeout) {}]);

注意这里两个指令都注入了'$timeout'依赖,其作用后文会解释。

这样一个外层框架就搭好了,现在我们来完成内部的细节。

登录

页面刚加载时只显示登录界面,只有当输入昵称提交后且收到服务端通知昵称有效方可跳转到聊天室。我们将ng-show指令添加到登录界面和聊天室各自的dom节点上,来帮助我们显示或隐藏元素。用'hasLogined'的值控制是显示或隐藏。

<!-- chat room -->
<div class="chat-room-wrapper" ng-show="hasLogined">
...
</div>
<!-- end of chat room -->

<!-- login form -->
<div class="userform-wrapper" ng-show="!hasLogined">
...
</div>
<!-- end of login form -->

JS部分

$scope.login = function() { //登录
   socket.emit("addUser", {...});
 }

 //收到登录结果
 socket.on('userAddingResult', function(data) {
   if (data.result) {
     $scope.hasLogined = true;
   } else { //昵称被占用
     $scope.hasLogined = false;
   }
 });

这里监听了socket连接上的'userAddingResult'事件,接收服务端的通知,确认是否登录成功。

socket连接监听

成功登录以后,我们还监听socket连接上的其他事件:

//接收到欢迎新用户消息,显示系统欢迎辞,刷新在线列表

socket.on('userAdded', function(data) {});

//接收到所有用户信息,初始化在线列表

socket.on('allUser', function(data) {});

//接收到用户退出消息,刷新在线列表

socket.on('userRemoved', function(data) {});

//接收到新消息,添加到聊天记录

socket.on('messageAdded', function(data) {});

接收到事件以后,做相应的刷新动作,这里的socket是socket.io经过包装的服务,内部仅包装了我们需要用到的两个函数on和emit。我们在事件监听里对model做的修改,都会在AngularJS内部得到通知和处理,UI才会得到及时刷新。
监听内做的事情太具体和琐碎了,这里就不列出了,接下来介绍一下message指令。

message 指令

最后分享一下我在写message指令时遇到的问题。首先看一下其代码:

app.directive('message', ['$timeout',function($timeout) {
  return {
    restrict: 'E',
    templateUrl: 'message.html',
    scope:{
      info:"=",
      self:"=",
      scrolltothis:"&"
    },
    link:function(scope, elem, attrs){
        $timeout(scope.scrolltothis);
    }
  };
}])

以及其模板message.html:

<div ng-switch on="info.type">
  <!-- 欢迎消息 -->
  <div class="system-notification" ng-switch-when="welcome">系统{{info.text}}来啦,大家不要放过他~</div>
  <!-- 退出消息 -->
  <div class="system-notification" ng-switch-when="bye">系统:byebye,{{info.text}}</div>
  <!-- 普通消息 -->
  <div class="normal-message" ng-switch-when="normal" ng-class="{others:self!==info.from,self:self===info.from}">
    <div class="name-wrapper">{{info.from}} @ {{time | date: 'HH:mm:ss' }}</div>
    <div class="content-wrapper">{{info.text}}<span class="avatar"></span></div>
  </div>
</div>

模板中我们用ng-switch指令监听info.type变量的值,根据其值的不同显示不同内容。比如,当info.type值为"welcome"时,创建第一个dom节点,删除下方另外两个div。
另外,普通消息下,为了在UI上区分自己发出去的和收到的消息,需要给他们应用不同的样式,这里用ng-class指令实现。

ng-class="{others:self!==info.from,self:self===info.from}"

当'self===info.from'返回true时,应用'self'类,否则,应用'others'类。
在此指令中,我们创建了独立作用域,并绑定了三个属性,绑定完后还必须在父作用域的HTML标签上添加相应属性。

scope:{
    info:"=",
    self:"=",
    scrolltothis:"&"
}

<message self="nickname" scrolltothis="scrollToBottom()" info="message" ng-repeat="message in messages"></message>

在link函数中,执行一个动作:每当一个message被加到页面上时,将聊天记录滚动到最下方,一开始我是这样写的:

link:function(scope, elem, attrs){ scope.scrolltothis();}

结果发生了一个很奇怪的现象,总是滚动到上一条位置,而不是最新这条。调试之后发现是因为'scrolltothis'函数执行的时候,DOM还没渲染,所以在函数内部获取scrollHeight的时候获得的总是添加DOM节点之前的状态。这时候,可以把代码放到$timeout里延迟0秒执行,延迟0秒并不意味着会立即执行,因为js的单线程特性,代码实际会等到dom渲染完再执行。

$timeout(scope.scrolltothis);

完整代码可以戳我的GitHub→ChatRoom-AngularJS,DEMO可以戳→chat room

有任何不妥之处或错误欢迎各位指出,不胜感激~

Javascript 相关文章推荐
window.event快达到全浏览器支持了,以后使用就方便了
Nov 30 Javascript
JS动态添加与删除select中的Option对象(示例代码)
Dec 20 Javascript
js判断手机号运营商的方法
Oct 23 Javascript
基于JavaScript实现移除(删除)数组中指定元素
Jan 04 Javascript
jqPlot jQuery绘图插件的使用
Jun 18 Javascript
微信小程序去哪里找 小程序到底如何使用(附小程序名单)
Jan 09 Javascript
详解微信小程序 wx.uploadFile 的编码坑
Jan 23 Javascript
Node.js调试技术总结分享
Mar 12 Javascript
Vue DevTools调试工具的使用
Dec 05 Javascript
小程序实现单选多选功能
Nov 04 Javascript
快速了解Vue父子组件传值以及父调子方法、子调父方法
Jul 15 Javascript
解决vue单页面应用打包后相对路径、绝对路径相关问题
Aug 14 Javascript
JS实现仿苹果底部任务栏菜单效果代码
Aug 28 #Javascript
jquery实现弹出层登录和全屏层注册特效
Aug 28 #Javascript
jquery实现多条件筛选特效代码分享
Aug 28 #Javascript
jquery实现的用户注册表单提示操作效果代码分享
Aug 28 #Javascript
js实现横向伸展开的二级导航菜单代码
Aug 28 #Javascript
谈谈JavaScript中function多重理解
Aug 28 #Javascript
jquery衣服颜色选取插件效果代码分享
Aug 28 #Javascript
You might like
phpmyadmin操作流程
2006/10/09 PHP
PHP字符编码问题之GB2312 VS UTF-8解决方法
2011/06/23 PHP
解析PHP高效率写法(详解原因)
2013/06/20 PHP
简单谈谈PHP中strlen 函数
2016/02/27 PHP
redis+php实现微博(二)发布与关注功能详解
2019/09/23 PHP
菜鸟javascript基础资料整理2
2010/12/06 Javascript
jquery获取复选框被选中的值
2014/04/10 Javascript
JavaScript charCodeAt方法入门实例(用于取得指定位置字符的Unicode编码)
2014/10/17 Javascript
jQuery+jRange实现滑动选取数值范围特效
2015/03/14 Javascript
jQuery Mobile页面返回不需要重新get
2016/04/26 Javascript
layui 实现table翻页滚动条位置保持不变的例子
2019/09/05 Javascript
详解webpack的文件监听实现(热更新)
2020/09/11 Javascript
Python 2.7.x 和 3.x 版本的重要区别小结
2014/11/28 Python
python基于Tkinter库实现简单文本编辑器实例
2015/05/05 Python
Python中的迭代器与生成器高级用法解析
2016/06/28 Python
python中的计时器timeit的使用方法
2017/10/20 Python
python模块之paramiko实例代码
2018/01/31 Python
python 查找文件名包含指定字符串的方法
2018/06/05 Python
判断python字典中key是否存在的两种方法
2018/08/10 Python
对python pandas 画移动平均线的方法详解
2018/11/28 Python
TensorFlow:将ckpt文件固化成pb文件教程
2020/02/11 Python
django model 条件过滤 queryset.filter(**condtions)用法详解
2020/05/20 Python
python初步实现word2vec操作
2020/06/09 Python
运行Python编写的程序方法实例
2020/10/21 Python
HTML5头部标签的一些常用信息小结
2016/10/23 HTML / CSS
GLAMGLOW格莱魅美国官网:美国知名的面膜品牌
2016/12/31 全球购物
Pop In A Box英国:Funko POP搪胶公仔
2019/05/27 全球购物
下面关于"联合"的题目的输出是什么
2013/08/06 面试题
某公司C#程序员面试题笔试题
2014/05/26 面试题
2014年清明节寄语
2014/04/03 职场文书
2014年“四风”问题个人整改措施
2014/09/17 职场文书
作风整顿剖析材料
2014/09/30 职场文书
寒暑假实习证明书模板
2014/11/29 职场文书
诗词赏析-(浣溪沙)
2019/08/13 职场文书
vue Element-ui表格实现树形结构表格
2021/06/07 Vue.js
css3新特性的应用示例分析
2022/03/16 HTML / CSS