本章內容javascript
1:認識各類Node組件css
2:一個用Node作的實時程序html
3:服務器跟客戶端交互java
完成本章你將:node
遊覽這個程序,瞭解他是如何工做的;jquery
審查技術需求,並完成程序的初始設置;web
提供程序所需的HTML、CSS和客戶端JavaScript;express
用Socket.IO處理跟聊天相關的消息;npm
用客戶端JavaScript作程序的UI;設計模式
2.1程序概覽
本章會構建一個在線聊天程序,用戶能夠在一個簡單的表達中輸入消息,相互聊天,消息輸入後會發送給同一個聊天室內的其餘全部用戶。
進入聊天室之後,程序會自動給用戶分配一個暱稱,但他們能夠用聊天命令修改本身的暱稱,聊天命令以斜杆/開頭。
一樣,用戶也能夠輸入命令建立新的聊天室(或已加入的已有聊天室),在加入或建立聊天室時,新聊天室的名稱會出如今聊天程序頂端的水平條上,也會出如今聊天消息區域右側的可用房間列表中。
在用戶換到新房間之後,系統會確認這一變化。
2.2程序需求及初始設置
將要建立的聊天程序須要完成以下任務:
1:提供靜態文件(好比HTML、CSS和客戶端JavaScript);
2:在服務器上處理與聊天相關的消息;
3:在用戶的瀏覽器中處理與聊天相關的消息;
爲了提供靜態文件,須要使用Node內置的http模塊。但經過Httpt提供文件時,一般不能只是發送文件中的內容,還應該有所發送文件的類型。也就是說要用正確的MIME類型設置HTTP投的Content-Type。爲了查找這些MIME類型,你會用到第三方的模塊MIME。
MIME類型 MIME類型在維基百科上的文章HTTP://en.wikipedia.org/wiki/MIME中有詳細論述。
爲了處理與聊天相關的消息,須要用Ajax輪詢服務器。單位了讓這個程序能竟可能快地作出響應,咱們不會用傳統的Ajax發送消息。Ajax用HTTP做爲傳輸機制,而且HTTP原本就不是作實時通訊的。在用HTTP發送消息時,必須用一個新的TCP/IP鏈接。打開和關閉鏈接所須要的時間。此外,由於每次請求都要發送HTTP頭,因此傳輸的數據流也比較大。這個程序沒用依賴於HTTP的方案,而是採用了WebSocket,這是一個爲支持實時通訊而設計的輕量的雙向通訊協議。
在開始作程序的文件結構和依賴項設置這些症狀的初期工做以前,咱們先聊聊Node如何同時處理HTTP和websocket,這是選它作實時程序最好的理由之一。
2.2.1 提供HTTP和WebSocket服務
儘管這個程序不會用Ajax發送和接收聊天消息,但它仍要用HTTP發送用在用戶瀏覽器中的HTML、CSS和客戶端JavaScript。
HTTP鏈接是短鏈接,用在不須要實時請求相應的場合,WebSocket鏈接,用在須要實時請求響應的場合。
2.2.2建立程序的文件結構
開始前,咱們先建立一個項目目錄。主程序文件會直接放在這個目錄下。你須要添加一個lib子目錄,用來放一些服務器邏輯。還須要建立一個public的子目錄,用來放客戶端的文件。在public子目錄下,建立一個JavaScript子目錄和一個Stylesheets目錄。
A.2在Windows上的安裝方法
下載地址:https://nodejs.org/en/#download
在項目目錄下:
輸入下面這條命令建立express包:
局部安裝:npm install express
全局安裝:npm install -g express
寫一個server.js文件
//內置的http模塊提供了HTTP服務器和客戶端功能
var http = require('http');
//內置的fs模塊提供了與文件系統相關的功能
var fs = require('fs'); //內置的path模塊提供了與文件系統路徑相關的功能 var path = require('path'); //附加的mime模塊有根據文件擴展名得出mime類型的能力 var mime = require('mime'); //用來緩存文件內容的對象 var cache = {}; //發送文件數據及錯誤響應 function send404(response){ response.writeHead(404,{'Content-Type':'text/plain'}); response.write('Error 404 : resource not fount.Hi!'); response.end(); } //提供文件數據服務 function sendFile(response, filePath, fileContents){ response.writeHead( 200, {"content-type":mime.lookup(basename(filePath))} ); response.end(fileContents); } //提供靜態文件服務 function serveStatic(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); } }); } } //建立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; serveStatic(response, cache, absPath); }) //啓動http服務器 server.listen(3000, function(){ console.log("Server listening on port 3000.") })
啓動服務器命令:node server.js
服務器運行起來後再瀏覽器中訪問127.0.0.1:3000會激發404錯誤的輔助函數,頁面上會顯示 Error 404 : resource not fount.Hi! 消息,由於沒添加靜態文件。
Ctrl+c關閉正在運行的服務器.
接下來添加靜態文件。
2.3.2添加HTML和CSS文件
第一個加默認的HTML文件。在public目錄下建立index.html文件,並引入一個CSS文件,設置顯示程序內容的div元素,價值一些客戶端JavaScript文件。這些JavaScript文件提供了客戶端Socket.IO功能、jQuery,以及兩個該程序特有的文件,用來提供聊天的功能。
<!DOCTYPE html>
<html lang="en">
<head>
<title>
Chat
</title>
<link href="./stylesheets/style.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<div id="content">
<!--顯示當前聊天室的名稱-->
<div id="room"></div>
<!--顯示當前可用聊天室列表的div-->
<div id="room-list"></div>
<!--顯示聊天信息的div-->
<div id="messages"></div>
<form id="send-from">
<!--用來輸入聊天命令和消息的表單輸入元素-->
<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>
body { padding: 50; 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; }
2.4用Socket.IO處理與聊天相關的消息
咱們前面說過程序必需要作三件事,其中第一個提供靜態文件已經作了,如今來解決第二個,處理瀏覽器和服務器之間的通訊。現代瀏覽器能用WebSocket處理瀏覽器跟服務器二者之間的通訊。
Socket.IO爲Node及客戶端JavaScript提供了基於WebSocket以及其餘傳輸方式的封裝,它提供了一個抽象層。若是瀏覽器沒有實現WebSocket,Socket.IO會自動去自動一個備選方案,而外提供的API仍是同樣的。本節將會:
1:簡要介紹下Socket.IO,並肯定要在服務器端使用的Socket.IO功能;
2:添加代碼設置Socket.IO服務器;
3:添加代碼處理各類聊天程序的事件;
Socket.IO提供了開箱即用的虛擬通道,全部程序不用吧每條消息都向已鏈接的用戶廣播,而是隻向那些預約了某個通道的用戶廣播。用這個功能實現程序裏的聊天室功能很是簡單。
Socket.IO仍是事件發射器的好例子。時間發射器本質上是組織異步邏輯的一種很方便的設計模式。本章中會有一些事件發射器的代碼,但下一章纔會作更深刻的討論。
事件發射器是跟某種資源相關聯的,它能向這個資源發送消息,也能從這個資源接收消息。
咱們先開始作服務器上的功能,並確立處理鏈接的邏輯。而後會定義服務器所須要的功能。
2.4.1設置Socket.IO服務器
在lib下建立chat._server.js:
//聲明被提供使用的Socket.IO,並初始化部分定義聊天狀態的變量
var socketio = require('socket.io');
var io; var guestNumber = 1; var nickNames = {}; var namesUsed = {}; var currentRoom = {}; //加載一個定製的Node模塊,提供處理基於Socket.IO的服務端聊天功能的,暫未定義。 var chatServer = require('./lib/chat_server'); //啓動Socket.IO服務器,給她提供一個已經定義好的HTTP服務器,跟HTTP服務器共享同一個TCP/IP端口 chatServer.listen(server); exports.listen = function(server){ //啓動Socket.IO服務器容許它搭載在已有的HTTP服務器上 io = socketio.listen(server); io.set('log level',1); //定義每一個用戶鏈接處理的邏輯 io.sockets.on('connection',function(socket){ //在用戶鏈接上來時賦予一個訪問名 guestNumber = assignGuestName(socket, guestNumber, nickNames, namesUsed); //在用戶鏈接上來時把他放入聊天室Lobby裏 joinRoom(socket,'Lobby'); //處理用戶的消息,改名,以及聊天室的建立和變動 handleMessageBroadcasting(socket, nickNames); handleNameChangeAttempts(socket, nickNames, namesUsed); handleRoomJoining(socket); //用戶發出請求時,向其提供已經被佔用的聊天室的列表 socket.on('rooms',function(){ socket.emit('rooms', io.sockets.manager.rooms); }); //定義用戶斷開鏈接後的清楚邏輯 handleClientDisconnection(socket, nickNames, namesUsed); }); };
已經肯定了鏈接的處理邏輯,如今該添加用來處理程序需求的全部輔助函數了。
2.4.2 處理程序場景及事件
聊天程序須要處理下面這些場景和事件
1:分配暱稱
2:房間更換請求
3:暱稱更換請求
4:發送聊天消息
5:房間建立
6:用戶斷開鏈接
要實現這些功能得添加幾個輔助函數,以下文所述。
1.分配暱稱
要添加的第一個輔助函數是assignGuestName,用來處理新用戶的暱稱。當用戶第一次鏈接到聊天服務器上時,用戶會被放到一個叫作Lobby的聊天室中,並調用assignGuestName給他們分配一個暱稱,以即可以相互區分開來。
//分配用戶暱稱
function assignGuestName(socket, guestNumber, nickNames, namesUsed){
//生成新的暱稱
var name = 'Guest' + guestNumber; //把用戶的暱稱跟客戶端鏈接ID關聯上 nickNames[socket.id] = name; //讓用戶知道他們的暱稱 socket.emit('nameResult', { success:true, name:name }); //存放已被佔用的暱稱 namesUsed.push(name); //增長用來生產暱稱的計數器 return guestNumber + 1; }
程序分配的全部暱稱基本上都是在Guest後面加上一個數字,,有新用戶鏈接進來時這個數字就會增加。用戶暱稱存在變量nickNames中以便於引用,而且會跟一個內部socketID關聯。暱稱還會被添加到namesUsed中,這個變量中保存的是已經被佔用的暱稱。把下面清單中的代碼添加到lib/chat_server.js中實現這個功能。
2.進入聊天室相關的邏輯
要添加到chat_server.js中的第二個輔助行數是joinRoom。處理邏輯跟用戶加入聊天室有關。
//進入聊天室相關的邏輯
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); //若是不止一個用戶在這個房間裏,彙總下有哪些用戶 if(usersInRoom.length > 1){ var usersInRoomSummary = 'Userd currently in ' + room + ':'; for(var index in userdInRoom){ var userSocketId = usersInRoom[index].id; if(userSocketId != socket.id){ if(index > 0){ usersInRoomSummary += ', '; } usersInRoomSummary +=nickNames[userSocketId]; } } usersInRoomSummary += '.'; //將房間裏其餘用戶的彙總發送給這個用戶 socket.emit('message', {text:usersInRoomSummary}); } }
調用socket對象上的join方法就能夠將用戶加入Socket.IO房間。而後程序會吧相關的細節向這個用戶及同一房間中的其餘用戶發送。程序會讓用戶知道有哪些用戶在這個房間裏,還會讓其餘用戶知道這個用戶進來了。
3.處理暱稱變動請求
若是用戶都用程序分配的暱稱,很難記住誰是誰。所以聊天程序容許用戶發起改名請求。改名須要用戶的瀏覽器經過Socket.IO發送一個請求,並接收表示成功或失敗的響應。
如下定義了一個出來用戶改名請求的函數,加入到lib/chat_server.js中,用戶不能將你從改爲以Guest開頭,或改爲其餘已經被佔用的暱稱。
//改名請求的處理邏輯
function handleNameChangeAttempts(socket, nickNames, namesUsed){
//添加nameAttempt事件的監聽器
socket.on('nameAttempt', function(name){ //暱稱不能以Guest開頭 if(name.indexOf('Guest') == 0){ 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.' }) } } }); }
4.發送聊天消息
用戶暱稱沒問題了,如今須要加個函數處理用戶發過了的聊天消息。基本流程是:用戶發射一個事件,代表消息是從哪一個房間發出來的,已經消息的內容是什麼;而後服務器將這條消息,轉發給同一房間的全部用戶。
將下面的代碼加入到lib/chat_server.js中。Socket.IO的broadcast函數是用來轉發消息的。
//發送聊天消息
function handleMessageBroadcasting(socket){
socket.on('message',function(message){ socket.broadcast.to(message.room).emit('message', { text: nickNames[socket.id] + ': ' + message.text }); }); }
5.建立房間
若是尚未房間的話則建立一個房間,如下代碼加入到lib/chat_server.js中,實現更好房間的功能。注意leave方法的使用。
//建立房間
function handleRoomJoining(socket){
socket.on('join',function(room){ socket.leave(currentRoom[socket.id]); joinRoom(socket, room.newRoom); }); }
6.用戶斷開鏈接
當用戶離開聊天程序時,從NickNames和namesUsed中移除用戶的暱稱,將下面的代碼加入到lib/chat_server.js中。
//用戶斷開鏈接
function handleClientDisconnection(socket){
socket.on('disconnect',function(){ var nameIndex = namesUsed.indexOf(nickNames[socket.id]); delete namesUsed(nameIndex); delete nickNames[socket.id]; }); }
2.5在程序的用戶界面上使用客戶端JavaScript
在服務端分發瀏覽器發來的消息的Socket.IO邏輯已經加上了,如今該添加跟服務器通訊所須要的客戶端JavaScript了。客戶端JavaScript須要實現如下功能:
1:向服務端發送用戶的消息和暱稱/房間變動請求;
2:顯示其餘用戶消息,以及可用房間的列表;
2.5.1將消息和暱稱/房間變動去請求傳送給服務器
要添加的第一段客戶端JavaScript代碼是一個JavaScript原型對象,用來處理聊天命令,發送消息,請求變動房間或暱稱
在public/javascripts目錄下建立一個chat.js文件,把下面的代碼放進去。這段代碼至關於定義了一個JavaScript「類」,在初始化時可用傳入一個Socket.IO的 參數socket:
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; };
2.5.2 在用戶界面中顯示消息及可用房間
如今該添加使用jQuery跟用戶界面(基於瀏覽器)直接交互的邏輯了。要添加的第一個功能是顯示文本數據。
從安全角度來看,Web程序中有兩種文本數據。一種是受信的文本數據,由程序提供的文本組成,另外一種是可疑的文本數據,是由程序的用戶建立的文本,或從用戶建立的文本中提取出來的。咱們之因此認爲來自用戶的文本數據時可疑的,是由於惡意用戶可能會蓄意在提交的文本數據中包含<script>標籤,放入JavaScript邏輯。若是不經修改就把這些數據展現給其餘用戶,可能會發生使人厭惡的事情,好比將用戶轉到其餘頁面。這種劫持Web程序的方法稱做跨域腳本(XSS)攻擊。
這個聊天程序會用聊個負責函數顯示文本數據。一個函數用來顯示可疑的文本數據,另外一個函數顯示受信的文本數據。
函數divEscapedContentElement用來顯示可疑的文本。他會淨化文本,將特殊字符轉換成HTML實體,這樣瀏覽器就會按輸入的樣子顯示它們,而不會視圖按HTML標籤解釋它們。
函數divSystemContentElement用來顯示系統建立的受信內容,而不是其餘用戶建立的。在public/javascripts目錄下建立chat_ui.js文件,並把下面兩個輔助函數放進去:
//用來顯示可疑的文本
function divEscapedContentElement(message){
return $('<div></div>').text(message); }; //用來顯示系統建立的受信內容 function divSystemContentElement(message){ return $('<div></div>').html('<i>' + message + '</i>'); }
下面要加到chat_ui.js中的函數是用來處理用戶輸入的,若是用戶輸入的內容以斜杆/開頭,它會將其做爲聊天命令處理。若是不是,就做爲聊天消息發送給服務器並廣播給其餘用戶,並添加到用戶所在的聊天室的聊天文本中。
//處理原始的用戶輸入
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(''); }
輔助函數已經定義好了,還須要添加下面這個代碼清單中的邏輯,他要在用戶的瀏覽器加載完頁面後執行。這段代碼會對客戶端的Socket.IO時間處理進行初始化。
//客戶端程序初始化邏輯
var socket = io.connect();
$(document).ready(function(){ var chatApp = new chatApp(socket); //顯示改名嘗試的結果 socket.on('nameResult', function(result){ var message; if(result.success){ message = 'Youare 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(); }); }); //按期請求可用房間列表 setInterval(function(){ socket.emit('rooms'); },1000); $('#send-message').focus(); //提交表單可用發送聊天消息 $('#send-form').submit(function(){ processUserInput(chatApp, socket); return false; }); });
接下來讓咱們把程序作完,將下面代碼清單中的CSS樣式代碼添加到public/stylesheets/style.css文件中。
#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; }
結束語:最笨的辦法也不錯哦。