1 項目名稱
Web聊天室(《這是NodeJs實戰》第二章的一個案例,把整個開發過程記錄下來)javascript
2 項目描述
該項目是一個簡單的在線聊天程序。打開聊天頁面,程序自動給用戶分配一個暱稱,進入默認的Lobby聊天室。用戶能夠發送消息,也可使用聊天命令(聊天命令以/開頭)修改本身的暱稱或者加入已有的聊天室(聊天室不存在時,建立新的聊天室)。在加入或建立聊天室時,新聊天室的名稱會出如今聊天程序頂端的水平條上,也會出如今聊天消息區域右側的可用房間列表中。在用戶換到新房間後,系統會顯示信息以確認這一變化。css
3 系統設計
該項目使用Node實現,由於Node用一個端口就能夠輕鬆地提供HTTP和WebSocket兩種服務。使用HTTP處理靜態文件的同時使用WebSocket實現實時數據(聊天消息)。程序的實現能夠劃分如下幾個功能模塊:html
- 提供靜態文件(好比HTML、CSS和客戶端JavaScript)
- 在服務器上處理與聊天相關的消息
- 在用戶的瀏覽器中處理與聊天相關的消息
爲了提供靜態文件,須要使用Node內置的http模塊。但經過HTTP提供文件時,一般不能只是發送文件中的內容,還應該有所發送文件的類型。也就是說要用正確的MIME類型設置HTTP 頭的Content-Type。爲了查找這些MIME類型,會用到第三方的模塊mime。java
爲了處理與聊天相關的消息,須要用Ajax輪詢服務器。爲了讓這個程序能儘量快的做出響應,咱們不會用傳統的Ajax發送消息。採用WebSocket,這是一個爲支持實時通信而設計的輕量的雙向通訊協議。由於在大多數狀況下,只有兼容HTML5的瀏覽器才支持WebSocket,因此這個程序會使用流行的Socket.IO庫,他給不能使用WebSocket的瀏覽器提供了一些後備措施。node
4 系統實現
使用WebStorm開發該項目。WebStorm被稱爲「最強大的HTML5編輯器」、「最智能的JavaScript IDE」。jquery
4.1 建立程序的文件結構
使用WebStorm,選擇一個目錄,建立一個新的空項目。設計項目結構以下所示:npm
4.2 指明依賴項
程序的依賴項是在package.json文件中指明的。這個文件老是被放在程序的根目錄下。 package.json文件用於描述你的應用程序,它包含一些JSON表達式。在package.json文件中能夠定義不少事情,但最重要的是程序的名稱、版本號、對程序的描述,以及程序的依賴項。 代碼清單1中是一個包描述文件,描述了項目的功能和依賴項。將這個文件保存到項目的根目錄中,命名爲package.json。json
{ "name": "chatrooms", "version": "0.0.1", "description":"Minimalist multiroom chat server", "dependencies":{ "socket.io":"~0.9.6", "mime":"~1.2.11" } }數組 |
4.3 安裝依賴項
切換到DOS窗口,在項目的根目錄下輸入如下這條命令瀏覽器
npm install
若是按照失敗,切換到國內的npm鏡像,而後再安裝。鏡像使用方法(三種辦法任意一種都能解決問題,建議使用第三種,將配置寫死,下次用的時候配置還在):
1.經過config命令
npm config set registry https://registry.npm.taobao.org
npm info underscore (若是上面配置正確,這個命令會有字符串response)
2.命令行指定
npm --registry https://registry.npm.taobao.org info underscore
3.編輯 ~/.npmrc 加入下面內容
registry = https://registry.npm.taobao.org
安裝成功後,在根目錄下建立的node_modules目錄,這個目錄中放的就是程序的依賴項。
4.4提供HTML、CSS和客戶端 JavaScript的服務
程序的邏輯是由一些文件實現的,有些運行在服務器上,有些運行在客戶端。 在客戶端運行的JavaScript須要做爲靜態資源發給瀏覽器,而不是在Node上執行。
服務器端的文件:
server.js
lib/chat_server.js
發送給客戶端的文件:
public/index.html
public/stylesheets/style.css
public/javascripts/chat.js
public/javascripts/chat_ui.js
4.4.1 在server.js中提供靜態文件服務器
/**
* Created by Administrator on 2016-05-05.
*/
var http = require('http');
var fs = require('fs');
var path = require('path');
var mime = require('mime');
var cache={};//緩存文件內容的對象
/* 請求的文件不存在時,發送404錯誤*, /
function send404(response){
response.writeHead(404,{'Content-Type':'text/plain'});
response.write('Error 404:resource not found.');
response.end();
}
/* 發送數據文件*/
function sendFile(response,filePath,fileContents){
response.writeHead(200,{"Content-type":mime.lookup(path.basename(filePath))});
response.end(fileContents);
}
/*提供靜態文件服務*/
function serverStatic(response,cache,absPath){
if(cache[absPath]){
sendFile(response, absPath,cache[absPath]);//從內存中返回數據
}else{
fs.exists(absPath,function(exists){//檢查文件是否存在
if(exists){
fs.readFile(absPath,function(err,data){
if(err){
send404(response);
}else{
cache[absPath] = data;
sendFile(response,absPath,data);
}
});
}else{
send404(response);
}
});
}
}
/* 1 建立HTTP服務器*,從該句代碼開始閱讀*/
var server= http.createServer(function(request,response){
var filePath = false;
if(request.url == '/'){
filePath = 'public/index.html';
}else{
filePath = 'public' + request.url;
}
var absPath='./' + filePath;
serverStatic(response,cache,absPath);
});
server.listen(3000,function(){
console.log("server listening on port 3000.");
}); /* 2 加載chat_server,建立聊天服務器,chat_server 模塊隨後實現*/
var chatServer = require('./lib/chat_server');
chatServer.listen(server); |
4.4.2添加HTML和CSS文件
Index.html文件內容:
<!doctype html>
<html lang='en'>
<head>
<title>Chat</title>
<link rel='stylesheet' href='/stylesheets/style.css'></link>
</head>
<body>
<div id='content'>
<div id='room'></div>
<div id='room-list'></div>
<div id='messages'></div>
<form id='send-form'>
<input id='send-message' />
<input id='send-button' type='submit' value='Send'/>
<div id='help'>
Chat commands:
<ul>
<li>Change nickname: <code>/nick [username]</code></li>
<li>Join/create room: <code>/join [room name]</code></li>
</ul>
</div>
</form>
</div>
<script src='/socket.io/socket.io.js' type='text/javascript'></script>
<script src='http://code.jquery.com/jquery-1.8.0.min.js' type='text/javascript'></script>
<script src='/javascripts/chat.js' type='text/javascript'></script>
<script src='/javascripts/chat_ui.js' type='text/javascript'></script>
</body>
</html> |
style.css文件內容
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}
#content {
width: 800px;
margin-left: auto;
margin-right: auto;
}
#room {
background-color: #ddd;
margin-bottom: 1em;
}
#messages {
width: 690px;
height: 300px;
overflow: auto;
background-color: #eee;
margin-bottom: 1em;
margin-right: 10px;
}
#room-list {
float: right;
width: 100px;
height: 300px;
overflow: auto;
}
#room-list div {
border-bottom: 1px solid #eee;
}
#room-list div:hover {
background-color: #ddd;
}
#send-message {
width: 700px;
margin-bottom: 1em;
margin-right: 1em;
}
#help {
font: 10px "Lucida Grande", Helvetica, Arial, sans-serif;
} |
4.5用Socket.IO處理與聊天相關的功能
4.5.1 chat_server.js實現服務器端功能
var socketio = require('socket.io');
var io;
var guestNumber = 1;
var nickNames = {};
var namesUsed = [];
var currentRoom = {};
exports.listen = function(server){ //啓動socket.io服務器,容許它搭載在已有的http服務器上
io = socketio.listen(server);
io.set('log level',1); //定義每一個用戶鏈接的處理邏輯
io.sockets.on('connection',function(socket){
// 1 客戶端鏈接後,分配用戶暱稱
guestNumber=assignGuestName(socket,guestNumber,nickNames,namesUsed);
// 2 加入Lobby聊天室
joinRoom(socket,'Lobby');
// 3 處理廣播消息
handleMessageBroadcasting(socket,nickNames);
// 4處理修改暱稱命令
handleNameChangeAttempts(socket,nickNames,namesUsed);
// 5 處理切換/建立聊天室命令
handleRoomJoining(socket);
// 6 當收到客戶端請求後,發送給客戶端房間列表
socket.on('rooms',function(){
socket.emit('rooms',io.sockets.manager.rooms);
});
// 7 處理客戶端斷開鏈接
handleClientDisconnection(socket,nickNames,namesUsed);
});
};
/* 分配用戶暱稱*/
function assignGuestName(socket,guestNumber,nickNames,namesUsed){
var name='Guest'+guestNumber; //將暱稱保存在暱稱集合nickNames中
nickNames[socket.id] = name; //發送給客戶端知悉其暱稱
socket.emit('nameResult',{
success:true,
name:name
}); //將暱稱保存在另外一個已使用的暱稱數組中
namesUsed.push(name);
return guestNumber + 1;
}
/*加入房間*/
function joinRoom(socket, room) { //用戶進入房間
socket.join(room); //記錄用戶的當前房間
currentRoom[socket.id] = room; //讓用戶知悉他進入了新的房間
socket.emit('joinResult', {room: room});
//讓該房間裏的其餘用戶知悉有新用戶進入了房間 socket.broadcast.to(room).emit('message', {
text: nickNames[socket.id] + ' has joined ' + room + '.'
});
//獲取該房間裏全部的用戶
var usersInRoom = io.sockets.clients(room); //若是用戶數量大於1
if (usersInRoom.length > 1) {
var usersInRoomSummary = 'Users currently in ' + room + ': ';
for (var index in usersInRoom) {
var userSocketId = usersInRoom[index].id; //判斷非當前用戶
if (userSocketId != socket.id) {
if (index > 0) {
usersInRoomSummary += ', ';
}
usersInRoomSummary += nickNames[userSocketId];
}
}
usersInRoomSummary += '.';
//彙總該房間裏的其它成員名稱發送給給該用戶
socket.emit('message', {text: usersInRoomSummary});
}
}
/* 處理改名請求*/
function handleNameChangeAttempts(socket, nickNames, namesUsed) {
socket.on('nameAttempt', function(name) {
if (name.indexOf('Guest') == 0) {//不能以Guest開頭
socket.emit('nameResult', {
success: false,
message: 'Names cannot begin with "Guest".'
});
} else {//註冊用戶名稱
if (namesUsed.indexOf(name) == -1) {
var previousName = nickNames[socket.id];
var previousNameIndex = namesUsed.indexOf(previousName);
namesUsed.push(name);
nickNames[socket.id] = name;
delete namesUsed[previousNameIndex]; //當前用戶會收到改名信息
socket.emit('nameResult', {
success: true,
name: name
}); //房間中其餘用戶知悉當前用戶已改名
socket.broadcast.to(currentRoom[socket.id]).emit('message', {
text: previousName + ' is now known as ' + name + '.'
});
} else {//該暱稱已經存在
socket.emit('nameResult', {
success: false,
message: 'That name is already in use.'
});
}
}
});
}
/*發送聊天消息,即Node服務器收到客戶端消息,轉發給該房間的其它用戶*/
function handleMessageBroadcasting(socket) {
socket.on('message', function (message) {
socket.broadcast.to(message.room).emit('message', {
text: nickNames[socket.id] + ': ' + message.text
});
});
}
/* 切換聊天室,即離開當前房間,加入其餘房間,房間不存在則建立新的房間*/
function handleRoomJoining(socket) {
socket.on('join', function(room) {
socket.leave(currentRoom[socket.id]);
joinRoom(socket, room.newRoom);
});
}
/*處理客戶端斷開鏈接*/
function handleClientDisconnection(socket) {
socket.on('disconnect', function() {
var nameIndex = namesUsed.indexOf(nickNames[socket.id]);
delete namesUsed[nameIndex];
delete nickNames[socket.id];
});
} |
4.5.2 chat.js以及chat_ui.js實現客戶端功能
客戶端JavaScript須要實現如下功能:向服務器發送用戶的消息和暱稱/房間變動請求; 顯示其餘用戶的消息,以及可用房間的列表。Chat.js中定義一個原型對象,用於處理聊天消息和命令,該原型對象中的函數在chat_ui.js中調用。
chat.js文件內容:
/**
* Created by Administrator on 2016-05-05.
*/
/*JavaScript原型對象,處理髮送聊天消息、變動房間、處理聊天命令*/
var Chat = function(socket) {
this.socket = socket;
};
Chat.prototype.sendMessage = function(room, text) {
var message = {
room: room,
text: text
};
this.socket.emit('message', message);
};
Chat.prototype.changeRoom = function(room) {
this.socket.emit('join', {
newRoom: room
});
};
Chat.prototype.processCommand = function(command) {
var words = command.split(' ');
var command = words[0]
.substring(1, words[0].length)
.toLowerCase();
var message = false;
switch(command) {
case 'join':
words.shift();
var room = words.join(' ');
this.changeRoom(room);//變動房間
break;
case 'nick':
words.shift();
var name = words.join(' ');
this.socket.emit('nameAttempt', name);//修改暱稱
break;
default:
message = 'Unrecognized command.';
break;
};
return message;
}; |
chat_ui.js文件內容:
/**
* Created by Administrator on 2016-05-05.
*/ /*用來顯示可疑的文本。它會淨化文本,將特殊字符轉換 成HTML實體*/
function divEscapedContentElement(message) {
return $('<div></div>').text(message);
}
/*顯示系統建立的受信內容*/
function divSystemContentElement(message) {
return $('<div></div>').html('<i>' + message + '</i>');
}
/*顯示用戶輸入的信息*/
function processUserInput(chatApp, socket) {
var message = $('#send-message').val();
var systemMessage;
if (message.charAt(0) == '/') {//顯示系統受信內容
systemMessage = chatApp.processCommand(message);
if (systemMessage) {
$('#messages').append(divSystemContentElement(systemMessage));
}
} else {//顯示用戶輸入內容
chatApp.sendMessage($('#room').text(), message);
$('#messages').append(divEscapedContentElement(message));
$('#messages').scrollTop($('#messages').prop('scrollHeight'));
}
//清空輸入框
$('#send-message').val('');
}
/*客戶端程序初始化邏輯*/
var socket = io.connect();
$(document).ready(function() {
var chatApp = new Chat(socket);
//顯示用戶改名結果
socket.on('nameResult', function(result) {
var message;
if (result.success) {
message = 'You are now known as ' + result.name + '.';
} else {
message = result.message;
}
$('#messages').append(divSystemContentElement(message));
});
//顯示用戶切換聊天室結果
socket.on('joinResult', function(result) {
$('#room').text(result.room);
$('#messages').append(divSystemContentElement('Room changed.'));
});
//顯示聊天消息
socket.on('message', function (message) {
var newElement = $('<div></div>').text(message.text);
$('#messages').append(newElement);
});
//顯示房間列表
socket.on('rooms', function(rooms) {
$('#room-list').empty();
for(var room in rooms) {
room = room.substring(1, room.length);
if (room != '') {
$('#room-list').append(divEscapedContentElement(room));
}
}
$('#room-list div').click(function() {
chatApp.processCommand('/join ' + $(this).text());
$('#send-message').focus();
});
});
//每間隔1秒,向服務器從新請求房間列表
setInterval(function() {
socket.emit('rooms');
}, 1000);
$('#send-message').focus();
//提交表單,發送聊天消息
$('#send-form').submit(function() {
processUserInput(chatApp, socket);
return false;
});
}); |
4.6 服務器與客戶端的事件分析
服務器與客戶端的交互主要是經過相互發送事件-處理事件完成的,如下是在整個流程中發生的事件:
4.6.1服務器事件流程
'connection'事件//接收客戶端鏈接
{
// 1 assignGuestName()函數中的事件
socket.emit('nameResult',{ //分配默認暱稱
success:true,
name:name
});
// 2 joinRoom()函數中的事件
socket.emit('joinResult', {room: room});//加入房間
socket.broadcast.to(room).emit('message', {//廣播消息
text: nickNames[socket.id] + ' has joined ' + room + '.'
});
socket.emit('message', {text: usersInRoomSummary});//發送給每一個用戶包含該房間的其餘用戶的列表
// 3 handleMessageBroadcasting()函數中的事件
socket.on('message', function (message) {//收到客戶端消息後,發射給同房間的其餘用戶
socket.broadcast.to(message.room).emit('message', {
text: nickNames[socket.id] + ': ' + message.text
});
});
// 4 handleNameChangeAttempts()函數中的事件
socket.emit('nameResult', {//改名
success: true,
name: name
});
socket.broadcast.to(currentRoom[socket.id]).emit('message', {//房間中其餘用戶知悉當前用戶已改名
text: previousName + ' is now known as ' + name + '.'
});
// 5 handleRoomJoining()函數中的事件
socket.on('join', function(room) {//切換聊天室
socket.leave(currentRoom[socket.id]);
joinRoom(socket, room.newRoom);
});
// 6 當收到客戶端請求後,發送給客戶端房間列表
socket.on('rooms',function(){
socket.emit('rooms',io.sockets.manager.rooms);
});
// 7 handleClientDisconnection()函數中的事件
socket.on('disconnect', function() {
var nameIndex = namesUsed.indexOf(nickNames[socket.id]);
delete namesUsed[nameIndex];
delete nickNames[socket.id];
});
}
4.6.2客戶端事件流程
// 1 鏈接服務器
var socket = io.connect();
// 2 鏈接服務器後,處理服務器發送過來的事件
socket.on('nameResult', function(result) {...});//顯示暱稱
socket.on('joinResult', function(result) {...});//顯示加入房間
socket.on('message', function (message) {...});//顯示該房間的其餘用戶
socket.on('rooms', function(rooms) {...});//顯示房間列表
setInterval(function() {
socket.emit('rooms');//按期向服務器請求房間列表
}, 1000);
// 3 點擊房間列表
$('#room-list div').click(function() {
this.socket.emit('join', { newRoom: room});//切換聊天室
});
// 4 點擊提交按鈕調用processUserInput()函數中觸發的客戶端事件
this.socket.emit('message', message);//發送消息
this.socket.emit('nameAttempt', name);//改名
this.socket.emit('join', { newRoom: room});//建立聊天室