全部文章搬運自個人我的主頁:sheilasun.mecss
不得不說,上手AngularJS比我想象得難多了,把官網提供的PhoneCat例子看完,又跑到慕課網把大漠窮秋的AngularJS實戰系列看了一遍,對於基本的使用依然有不少說不清道不明的疑惑,因而決定經過作一個在線聊天室幫助理解。DEMO能夠戳→chat room,代碼能夠戳→ChatRoom-AngularJS。html
清晰圖能夠戳 http://sheilasun.sinaapp.com/public/images/chatroom.gifnode
着手開發以前,首先明確一下須要實現的功能:jquery
由於本身是個審美渣,因此全靠bootstrap了,另外還模仿了下微信聊天記錄裏的氣泡設計。
界面分左右兩個板塊,分別用於顯示在線列表和聊天內容。
在左側的在線列表中,點擊不一樣項能夠切換右側板塊的聊天對象。
右側顯示與當前聊天對象的對話記錄,不過僅顯示最近的30條。每一條聊天記錄內容包括髮送人的暱稱及頭像、發送時間、消息內容。關於頭像,這裏作簡單處理,用填充了隨機色的方塊代替。另外,本身發出去的消息與收到的消息樣式天然要作不一樣設計,全部效果能夠看下圖。git
清晰圖能夠戳 http://sheilasun.sinaapp.com/public/images/chatroomsc.pngangularjs
服務端咱們用Node.js以及混入express、socket.io來開發,在程序根目錄打開終端,執行:github
npm init
根據提示,生成一個package.json文件。打開並配置依賴項:express
"dependencies": { "express": "^4.13.3", "socket.io": "^1.3.6" }
以後執行 npm install 安裝依賴模塊。npm
接下來,咱們在根目錄下新建app.js,在其中寫Server端代碼。再新建public文件夾,存放client端代碼。json
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 -->
$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.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指令時遇到的問題。首先看一下其代碼:
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>
關於Isolated Scope的知識,能夠查看這兩篇博文AngularJS 做用域與數據綁定機制,Understanding AngularJS Isolated Scope。
在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
有任何不妥之處或錯誤歡迎各位指出,不勝感激~