AngularJS+Node.js實(shí)現(xiàn)在線聊天室
不得不說,上手AngularJS比我想象得難多了,把官網(wǎng)提供的PhoneCat例子看完,又跑到慕課網(wǎng)把大漠窮秋的AngularJS實(shí)戰(zhàn)系列看了一遍,對(duì)于基本的使用依然有很多說不清道不明的疑惑,于是決定通過做一個(gè)在線聊天室?guī)椭斫狻EMO可以戳→chat room,代碼可以戳→ChatRoom-AngularJS。
清晰圖可以戳 //img.jbzj.com/file_images/article/201508/201508281040051.gif
功能
著手開發(fā)之前,首先明確一下需要實(shí)現(xiàn)的功能:
新用戶登入,廣播通知其他用戶
用戶下線,廣播通知其他用戶
可顯示在線人數(shù)及列表
可群聊,可私信
用戶若發(fā)送群消息,廣播通知其他所有用戶
用戶若發(fā)送私信,單獨(dú)通知收方界面
因?yàn)樽约菏莻€(gè)審美渣,所以全靠bootstrap了,另外還模仿了下微信聊天記錄里的氣泡設(shè)計(jì)。
界面分左右兩個(gè)板塊,分別用于顯示在線列表和聊天內(nèi)容。
在左側(cè)的在線列表中,點(diǎn)擊不同項(xiàng)可以切換右側(cè)板塊的聊天對(duì)象。
右側(cè)顯示與當(dāng)前聊天對(duì)象的對(duì)話記錄,不過僅顯示最近的30條。每一條聊天記錄內(nèi)容包括發(fā)送人的昵稱及頭像、發(fā)送時(shí)間、消息內(nèi)容。關(guān)于頭像,這里做簡單處理,用填充了隨機(jī)色的方塊代替。另外,自己發(fā)出去的消息與收到的消息樣式自然要做不同設(shè)計(jì),所有效果可以看下圖。
清晰圖可以戳 //img.jbzj.com/file_images/article/201508/201508281040052.png
服務(wù)端
服務(wù)端我們用Node.js以及混入express、socket.io來開發(fā),在程序根目錄打開終端,執(zhí)行:
npm init
根據(jù)提示,生成一個(gè)package.json文件。打開并配置依賴項(xiàng):
"dependencies": { "express": "^4.13.3", "socket.io": "^1.3.6" }
之后執(zhí)行 npm install 安裝依賴模塊。
接下來,我們?cè)诟夸浵滦陆╝pp.js,在其中寫Server端代碼。再新建public文件夾,存放client端代碼。
app.js中主要內(nèi)容如下:
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){ //有新用戶進(jìn)入聊天室 }); socket.on('addMessage',function(data){ //有用戶發(fā)送新消息 }); socket.on('disconnect', function () { //有用戶退出聊天室 ); }); http.listen(3002, function () { console.log('listening on *:3002'); });
在上面的代碼中,我們?yōu)橐韵率录砑恿吮O(jiān)聽:
-addUser,有新用戶進(jìn)入聊天室
該事件由客戶端輸入昵稱后觸發(fā),服務(wù)端收到后對(duì)昵稱是否已存在進(jìn)行判斷,如果已存在,通知客戶端昵稱無效:
socket.emit('userAddingResult',{result:false});
反之,通知客戶端昵稱有效以及當(dāng)前所有已連接的用戶信息,并把新用戶信息廣播給其他已連接用戶:
socket.emit('userAddingResult',{result:true}); allUsers.push(data);//allUsers保存了所有用戶 socket.emit('allUser',allUsers);//將所有在線用戶發(fā)給新用戶 socket.broadcast.emit('userAdded',data);//廣播歡迎新用戶,除新用戶外都可看到
其中需要注意'socket.emit'與'socket.broadcast.emit'的區(qū)別,可以查看這篇博文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,有用戶發(fā)送新消息
在此事件監(jiān)聽里,需要分成兩類情況處理:
1.私信
如果消息是發(fā)給特定用戶A,那么就需要獲取A對(duì)應(yīng)的socket實(shí)例,然后調(diào)用其emit方法。所以每當(dāng)一個(gè)客戶端連接到Server端時(shí),我們得把其socket實(shí)例保存起來,以備后續(xù)之需。
connectedSockets[nickname]=socket;//以昵稱作下標(biāo),保存每個(gè)socket實(shí)例,發(fā)私信需要用
需要發(fā)私信時(shí),取出socket實(shí)例做操作即可:
connectedSockets[nickname].emit('messageAdded',data)
2.群發(fā)
群發(fā)就比較簡單了,用broadcast方法即可:
socket.broadcast.emit('messageAdded',data);//廣播消息,除原發(fā)送者外都可看到
-disconnect,有用戶退出聊天室
需要做三件事情:
1.通知其他用戶“某用戶下線”
socket.broadcast.emit('userRemoved', data);
2.將用戶從保存了所有用戶的數(shù)組中移除
3.將其socket實(shí)例從保存了所有客戶端socket實(shí)例的數(shù)組中移除
delete connectedSockets[nickname]; //刪除對(duì)應(yīng)的socket實(shí)例
運(yùn)行一下服務(wù)端代碼,觀察有無錯(cuò)誤:
node app.js
若沒什么問題,繼續(xù)編寫客戶端的代碼。
客戶端
在public目錄下新建'index.html',客戶端需要用到bootstrap、angularjs、socket.io、jQuery以及我們自己的js和css文件,先把這些文件用標(biāo)簽引入。
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> <link 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="http://cdn.bootcss.com/angular.js/1.4.3/angular.min.js"></script> <script src="./assets/js/app.js"></script> </head> <body></body> </html>
我們并不立即深入邏輯細(xì)節(jié),把框架搭好先。
首先,在body上加上ng-app屬性,標(biāo)記一下angularjs的“管轄范圍”。這個(gè)練習(xí)中我們只用到了一個(gè)控制器,同樣將ng-controller屬性加到body標(biāo)簽。
<body ng-app="chatRoom" ng-controller="chatCtrl">
接下來在js中,我們來創(chuàng)建module及controller。
var app=angular.module("chatRoom",[]); app.controller("chatCtrl",['$scope','socket','randomColor',function($scope,socket,randomColor){}]);
注意這里,我們用內(nèi)聯(lián)注入添加了socket和randomColor服務(wù)依賴。這里我們不用推斷式注入,以防部署的時(shí)候用uglify或其他工具進(jìn)行了混淆,變量經(jīng)過了重命名導(dǎo)致注入失效。
在這個(gè)練習(xí)中,我們自定義了兩個(gè)服務(wù),socket和randomColor,前者是對(duì)socket.io的包裝,讓其事件進(jìn)入angular context,后者是個(gè)可以生成隨機(jī)色的服務(wù),用來給頭像指定顏色。
//socket服務(wù) app.factory('socket', function($rootScope) { var socket = io(); //默認(rèn)連接部署網(wǎng)站的服務(wù)器 return { on: function(eventName, callback) {...}, emit: function(eventName, data, callback) {...} }; }); //randomcolor服務(wù) app.factory('randomColor', function($rootScope) { return { newColor: function() { return '#'+('00000'+(Math.random()*0x1000000<<0).toString(16)).slice(-6);//返回一個(gè)隨機(jī)色 } }; });
注意socket服務(wù)中連接的語句“var socket = io();”,我們并沒有傳入任何url,是因?yàn)槠淠J(rèn)連接部署這個(gè)網(wǎng)站的服務(wù)器。
考慮到聊天記錄以及在線人員列表都是一個(gè)個(gè)邏輯及結(jié)構(gòu)重復(fù)的條目,且html結(jié)構(gòu)較復(fù)雜,為了其復(fù)用性,我們把它們封裝成兩個(gè)指令:
app.directive('message', ['$timeout',function($timeout) {}]) .directive('user', ['$timeout',function($timeout) {}]);
注意這里兩個(gè)指令都注入了'$timeout'依賴,其作用后文會(huì)解釋。
這樣一個(gè)外層框架就搭好了,現(xiàn)在我們來完成內(nèi)部的細(xì)節(jié)。
登錄
頁面剛加載時(shí)只顯示登錄界面,只有當(dāng)輸入昵稱提交后且收到服務(wù)端通知昵稱有效方可跳轉(zhuǎn)到聊天室。我們將ng-show指令添加到登錄界面和聊天室各自的dom節(jié)點(diǎn)上,來幫助我們顯示或隱藏元素。用'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", {...}); } //收到登錄結(jié)果 socket.on('userAddingResult', function(data) { if (data.result) { $scope.hasLogined = true; } else { //昵稱被占用 $scope.hasLogined = false; } });
這里監(jiān)聽了socket連接上的'userAddingResult'事件,接收服務(wù)端的通知,確認(rèn)是否登錄成功。
socket連接監(jiān)聽
成功登錄以后,我們還監(jiān)聽socket連接上的其他事件:
//接收到歡迎新用戶消息,顯示系統(tǒng)歡迎辭,刷新在線列表
socket.on('userAdded', function(data) {});
//接收到所有用戶信息,初始化在線列表
socket.on('allUser', function(data) {});
//接收到用戶退出消息,刷新在線列表
socket.on('userRemoved', function(data) {});
//接收到新消息,添加到聊天記錄
socket.on('messageAdded', function(data) {});
接收到事件以后,做相應(yīng)的刷新動(dòng)作,這里的socket是socket.io經(jīng)過包裝的服務(wù),內(nèi)部僅包裝了我們需要用到的兩個(gè)函數(shù)on和emit。我們?cè)谑录O(jiān)聽里對(duì)model做的修改,都會(huì)在AngularJS內(nèi)部得到通知和處理,UI才會(huì)得到及時(shí)刷新。
監(jiān)聽內(nèi)做的事情太具體和瑣碎了,這里就不列出了,接下來介紹一下message指令。
message 指令
最后分享一下我在寫message指令時(shí)遇到的問題。首先看一下其代碼:
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">系統(tǒng){{info.text}}來啦,大家不要放過他~</div> <!-- 退出消息 --> <div class="system-notification" ng-switch-when="bye">系統(tǒng):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指令監(jiān)聽info.type變量的值,根據(jù)其值的不同顯示不同內(nèi)容。比如,當(dāng)info.type值為"welcome"時(shí),創(chuàng)建第一個(gè)dom節(jié)點(diǎn),刪除下方另外兩個(gè)div。
另外,普通消息下,為了在UI上區(qū)分自己發(fā)出去的和收到的消息,需要給他們應(yīng)用不同的樣式,這里用ng-class指令實(shí)現(xiàn)。
ng-class="{others:self!==info.from,self:self===info.from}"
當(dāng)'self===info.from'返回true時(shí),應(yīng)用'self'類,否則,應(yīng)用'others'類。
在此指令中,我們創(chuàng)建了獨(dú)立作用域,并綁定了三個(gè)屬性,綁定完后還必須在父作用域的HTML標(biāo)簽上添加相應(yīng)屬性。
scope:{ info:"=", self:"=", scrolltothis:"&" } <message self="nickname" scrolltothis="scrollToBottom()" info="message" ng-repeat="message in messages"></message>
在link函數(shù)中,執(zhí)行一個(gè)動(dòng)作:每當(dāng)一個(gè)message被加到頁面上時(shí),將聊天記錄滾動(dòng)到最下方,一開始我是這樣寫的:
link:function(scope, elem, attrs){ scope.scrolltothis();}
結(jié)果發(fā)生了一個(gè)很奇怪的現(xiàn)象,總是滾動(dòng)到上一條位置,而不是最新這條。調(diào)試之后發(fā)現(xiàn)是因?yàn)?scrolltothis'函數(shù)執(zhí)行的時(shí)候,DOM還沒渲染,所以在函數(shù)內(nèi)部獲取scrollHeight的時(shí)候獲得的總是添加DOM節(jié)點(diǎn)之前的狀態(tài)。這時(shí)候,可以把代碼放到$timeout里延遲0秒執(zhí)行,延遲0秒并不意味著會(huì)立即執(zhí)行,因?yàn)閖s的單線程特性,代碼實(shí)際會(huì)等到dom渲染完再執(zhí)行。
$timeout(scope.scrolltothis);
完整代碼可以戳我的GitHub→ChatRoom-AngularJS,DEMO可以戳→chat room
有任何不妥之處或錯(cuò)誤歡迎各位指出,不勝感激~
- js實(shí)現(xiàn)簡易聊天對(duì)話框
- javascript和jQuery實(shí)現(xiàn)網(wǎng)頁實(shí)時(shí)聊天的ajax長輪詢
- nodejs實(shí)現(xiàn)的一個(gè)簡單聊天室功能分享
- 使用Angular和Nodejs、socket.io搭建聊天室及多人聊天室
- nw.js實(shí)現(xiàn)類似微信的聊天軟件
- Vue.js仿微信聊天窗口展示組件功能
- JavaScript/jQuery、HTML、CSS 構(gòu)建 Web IM 遠(yuǎn)程及時(shí)聊天通信程序
- js編寫簡單的聊天室功能
- nodejs基于WS模塊實(shí)現(xiàn)WebSocket聊天功能的方法
- JavaScript實(shí)現(xiàn)簡易QQ聊天界面
相關(guān)文章
AngularJS中的路由使用及實(shí)現(xiàn)代碼
本篇文章主要介紹了AngularJS中的路由使用及實(shí)現(xiàn)代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-10-10Angular?結(jié)合?dygraphs?實(shí)現(xiàn)?annotation功能
這篇文章主要介紹了Angular?結(jié)合?dygraphs?實(shí)現(xiàn)?annotation,本文,我們直接結(jié)合 Angular 來演示,如何通過 dygraphs 實(shí)現(xiàn)折線圖上的 annotation 的功能,需要的朋友可以參考下2022-08-08Angular.Js中ng-include指令的使用與實(shí)現(xiàn)
ng-include 指令用于包含外部的 HTML 文件。包含的內(nèi)容將作為指定元素的子節(jié)點(diǎn)。下面這篇文章主要給大家介紹了Angular.Js中ng-include指令的使用與實(shí)現(xiàn)的相關(guān)資料,文中介紹的非常詳細(xì),需要的朋友們下面來一起看看吧。2017-05-05AngularJS基于ngInfiniteScroll實(shí)現(xiàn)下拉滾動(dòng)加載的方法
這篇文章主要介紹了AngularJS基于ngInfiniteScroll實(shí)現(xiàn)下拉滾動(dòng)加載的方法,結(jié)合實(shí)例形式分析AngularJS下拉滾動(dòng)插件ngInfiniteScroll的下載、功能、屬性及相關(guān)使用方法,需要的朋友可以參考下2016-12-12AngularJs html compiler詳解及示例代碼
本文主要介紹AngularJs html compiler的知識(shí)講解,這里整理了相關(guān)資料及相關(guān)示例代碼,有興趣的小伙伴可以參考下2016-09-09Angular4學(xué)習(xí)筆記之實(shí)現(xiàn)綁定和分包
本篇文章主要介紹了Angular4學(xué)習(xí)筆記之實(shí)現(xiàn)綁定和分包,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-08-08詳解AngularJS controller調(diào)用factory
本篇文章主要介紹了詳解AngularJS controller調(diào)用factory,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-05-05