websocket + node.js聊天系統

轉:http://www.cnblogs.com/Wayou/p/hichat_built_with_nodejs_socket.htmljavascript

 

前端一直是一塊充滿驚喜的土地,不只是那些富有創造性的頁面,還有那些驚讚的效果及不斷推出的新技術。像node.js這樣的後端開拓者直接將前端人員的能力擴大到了後端。瞬間就有了一統天下的感受,來往穿梭於先後端之間代碼敲得飛起,今後由前端晉升爲'先後端'。css

圖片來自G+html

本文將使用Node.js加web socket協議打造一個網頁即時聊天程序,取名爲HiChat,中文翻過來就是'嗨聊',聽中文名有點像是專爲寂寞單身男女打造的~前端

其中將會使用到express和socket.io兩個包模塊,下面會有介紹。java

源碼&演示

在線演示 (heroku服務器網速略慢且免費套餐是小水管,建議下載代碼本地運行)node

源碼可訪問項目的GitHub頁面下載git

本地運行方法:程序員

  • 命令行運行npm install
  • 模塊下載成功後,運行node server啓動服務器
  • 打開瀏覽器訪問localhost

下圖爲效果預覽:github

 

準備工做

本文示例環境爲Windows,Linux也就Node的安裝與命令行稍有區別,程序實現部分基本與平臺無關。web

Node相關

  • 你須要在本機安裝Node.js(廢話)
  • 多少須要一點Node.js的基礎知識,若是還不曾瞭解過Node.js,這裏有一篇不錯的入門教程

而後咱們就能夠開始建立一個簡單的HTTP服務器啦。

相似下面很是簡單的代碼,它建立了一個HTTP服務器並監聽系統的80端口。

//node server example

//引入http模塊
var http = require('http'),
    //建立一個服務器
    server = http.createServer(function(req, res) {
        res.writeHead(200, {
            'Content-Type': 'text/plain'
        });
        res.write('hello world!');
        res.end();
    });
//監聽80端口
server.listen(80);
console.log('server started');

將其保存爲一個js文件好比server.js,而後從命令行運行node server或者node server.js,服務器即可啓動了,此刻咱們能夠在瀏覽器地址欄輸入localhost進行訪問,也能夠輸入本機IP127.0.0.1,都不用加端口,由於咱們服務器監聽的是默認的80端口。固然,若是你機子上面80端口被其餘程序佔用了,能夠選擇其餘端口好比8080,這樣訪問的時候須要顯示地加上端口號localhost:8080。

Express

首先經過npm進行安裝

  • 在咱們的項目文件夾下打開命令行(tip: 按住Shift同時右擊,能夠在右鍵菜單中找到'今後處打開命令行'選項)
  • 在命令行中輸入 npm install express 回車進行安裝
  • 而後在server.js中經過require('express')將其引入到項目中進行使用

express是node.js中管理路由響應請求的模塊,根據請求的URL返回相應的HTML頁面。這裏咱們使用一個事先寫好的靜態頁面返回給客戶端,只需使用express指定要返回的頁面的路徑便可。若是不用這個包,咱們須要將HTML代碼與後臺JavaScript代碼寫在一塊兒進行請求的響應,不太方便。

//返回一個簡單的HTML內容

server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-Type': 'text/html' //將返回類型由text/plain改成text/html
    });
    res.write('<h1>hello world!</h1>'); //返回HTML標籤
    res.end();
});

 在存放上一步建立的server.js文件的地方,咱們新建一個文件夾名字爲www用來存放咱們的網頁文件,包括圖片以及前端的js文件等。假設已經在www文件夾下寫好了一個index.html文件(將在下一步介紹,這一步你能夠放一個空的HTML文件),則能夠經過如下方式使用express將該頁面返回到瀏覽器。能夠看到較最開始,咱們的服務器代碼簡潔了很多。

//使用express模塊返回靜態頁面

var express = require('express'), //引入express模塊
    app = express(),
    server = require('http').createServer(app);
app.use('/', express.static(__dirname + '/www')); //指定靜態HTML文件的位置
server.listen(80);

 

其中有四個按鈕,分別是設置字體顏色,發送表情,發送圖片和清除記錄,將會在下面介紹其實現

 

socket.io

Node.js中使用socket的一個包。使用它能夠很方便地創建服務器到客戶端的sockets鏈接,發送事件與接收特定事件。

一樣經過npm進行安裝 npm install socket.io 。安裝後在node_modules文件夾下新生成了一個socket.io文件夾,其中咱們能夠找到一個socket.io.js文件。將它引入到HTML頁面,這樣咱們就能夠在前端使用socket.io與服務器進行通訊了。

<script src="/socket.io/socket.io.js"></script>

同時服務器端的server.js裏跟使用express同樣,也要經過require('socket.io')將其引入到項目中,這樣就能夠在服務器端使用socket.io了。

使用socket.io,其先後端句法是一致的,即經過socket.emit()來激發一個事件,經過socket.on()來偵聽和處理對應事件。這兩個事件經過傳遞的參數進行通訊。具體工做模式能夠看下面這個示例。

好比咱們在index.html裏面有以下JavaScript代碼(假設你已經在頁面放了一個ID爲sendBtn的按鈕):

<script type="text/javascript">
	var socket=io.connect(),//與服務器進行鏈接
		button=document.getElementById('sendBtn');
	button.onclick=function(){
		socket.emit('foo', 'hello');//發送一個名爲foo的事件,而且傳遞一個字符串數據‘hello’
	}
</script>

 上述代碼首先創建與服務器的鏈接,而後獲得一個socket實例。以後若是頁面上面一個ID爲sendBtn的按鈕被點擊的話,咱們就經過這個socket實例發起一個名爲foo的事件,同時傳遞一個hello字符串信息到服務器。

與此同時,咱們須要在服務器端寫相應的代碼來處理這個foo事件並接收傳遞來的數據。

爲此,咱們在server.js中能夠這樣寫:

//服務器及頁面響應部分
var express = require('express'),
    app = express(),
    server = require('http').createServer(app),
    io = require('socket.io').listen(server); //引入socket.io模塊並綁定到服務器
app.use('/', express.static(__dirname + '/www'));
server.listen(80);

//socket部分
io.on('connection', function(socket) {
    //接收並處理客戶端發送的foo事件
    socket.on('foo', function(data) {
        //將消息輸出到控制檯
        console.log(data);
    })
});

 如今Ctrl+C關閉以前啓動的服務器,再次輸入node server啓動服務器運行新代碼查看效果,一切正常的話你會在點擊了頁面的按扭後,在命令行窗口裏看到輸出的'hello'字符串。

一如以前所說,socket.io在先後端的句法是一致的,因此相反地,從服務器發送事件到客戶端,在客戶端接收並處理消息也是顯而易見的事件了。這裏只是簡單介紹,具體下面會經過發送聊天消息進一步介紹。

基本頁面

有了上面一些基礎的瞭解,下面能夠進入聊天程序功能的開發了。

首先咱們構建主頁面。由於是比較大衆化的應用了,界面不用多想,腦海中已經有大體的雛形,它有一個呈現消息的主窗體,還有一個輸入消息的文本框,同時須要一個發送消息的按鈕,這三個是必備的。

另外就是,這裏還準備實現如下四個功能,因此界面上還有設置字體顏色,發送表情,發送圖片和清除記錄四個按鈕。

最後的頁面也就是先前截圖展現的那們,而代碼以下:

www/index.html

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
        <meta name="author" content="Wayou">
        <meta name="description" content="hichat | a simple chat application built with node.js and websocket">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>hichat</title>
        <link rel="stylesheet" href="styles/main.css">
        <link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
        <link rel="icon" href="favicon.ico" type="image/x-icon">
    </head>
    <body>
        <div class="wrapper">
            <div class="banner">
                <h1>HiChat :)</h1>
                <span id="status"></span>
            </div>
            <div id="historyMsg">
            </div>
            <div class="controls" >
                <div class="items">
                    <input id="colorStyle" type="color" placeHolder='#000' title="font color" />
                    <input id="emoji" type="button" value="emoji" title="emoji" />
                    <label for="sendImage" class="imageLable">
                        <input type="button" value="image"  />
                        <input id="sendImage" type="file" value="image"/>
                    </label>
                    <input id="clearBtn" type="button" value="clear" title="clear screen" />
                </div>
                <textarea id="messageInput" placeHolder="enter to send"></textarea>
                <input id="sendBtn" type="button" value="SEND">
                <div id="emojiWrapper">
                </div>
            </div>
        </div>
        <div id="loginWrapper">
            <p id="info">connecting to server...</p>
            <div id="nickWrapper">
                <input type="text" placeHolder="nickname" id="nicknameInput" />
                <input type="button" value="OK" id="loginBtn" />
            </div>
        </div>
        <script src="/socket.io/socket.io.js"></script>
        <script src="scripts/hichat.js"></script>
    </body>
</html>

 

樣式文件 www/styles/main.css

html, body {
    margin: 0;
    background-color: #efefef;
    font-family: sans-serif;
}
.wrapper {
    width: 500px;
    height: 640px;
    padding: 5px;
    margin: 0 auto;
    background-color: #ddd;
}
#loginWrapper {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background-color: rgba(5, 5, 5, .6);
    text-align: center;
    color: #fff;
    display: block;
    padding-top: 200px;
}
#nickWrapper {
    display: none;
}
.banner {
    height: 80px;
    width: 100%;
}
.banner p {
    float: left;
    display: inline-block;
}
.controls {
    height: 100px;
    margin: 5px 0px;
    position: relative;
}
#historyMsg {
    height: 400px;
    background-color: #fff;
    overflow: auto;
    padding: 2px;
}
#historyMsg img {
    max-width: 99%;
}
.timespan {
    color: #ddd;
}
.items {
    height: 30px;
}
#colorStyle {
    width: 50px;
    border: none;
    padding: 0;
}
/*custom the file input*/

.imageLable {
    position: relative;
}
#sendImage {
    position: absolute;
    width: 52px;
    left: 0;
    opacity: 0;
    overflow: hidden;
}
/*end custom file input*/

#messageInput {
    width: 440px;
    max-width: 440px;
    height: 90px;
    max-height: 90px;
}
#sendBtn {
    width: 50px;
    height: 96px;
    float: right;
}
#emojiWrapper {
    display: none;
    width: 500px;
    bottom: 105px;
    position: absolute;
    background-color: #aaa;
    box-shadow: 0 0 10px #555;
}
#emojiWrapper img {
    margin: 2px;
    padding: 2px;
    width: 25px;
    height: 25px;
}
#emojiWrapper img:hover {
    background-color: blue;
}
.emoji{
    display: inline;
}
footer {
    text-align: center;
}

爲了讓項目有一個良好的目錄結構便於管理,這裏在www文件夾下又新建了一個styles文件夾存放樣式文件main.css,而後新建一個scripts文件夾存放前端須要使用的js文件好比hichat.js(咱們前端全部的js代碼會放在這個文件中),而咱們的服務器js文件server.js位置不變仍是放在最外層。

同時再新建一個content文件夾用於存放其餘資源好比圖片等,其中content文件夾裏再建一個emoji文件夾用於存入表情gif圖,後面會用到。最後咱們項目的目錄結構應該是這樣的了:

├─node_modules
└─www
    ├─content
    │  └─emoji
    ├─scripts
    └─styles

 此刻打開頁面你看到的是一個淡黑色的遮罩層,而接下來咱們要實現的是用戶暱稱的輸入與服務器登入。這個遮罩層用於顯示鏈接到服務器的狀態信息,而當鏈接完成以後,會出現一個輸入框用於暱稱輸入。

上面HTML代碼裏已經看到,咱們將www/scripts/hichat.js文件已經引入到頁面了,下面開始寫一些基本的前端js開始實現鏈接功能。

定義一個全局變量用於咱們整個程序的開發HiChat,同時使用window.onload在頁面準備好以後實例化HiChat,調用其init方法運行咱們的程序。

www/scripts/Hichat.js

window.onload = function() {
    //實例並初始化咱們的hichat程序
    var hichat = new HiChat();
    hichat.init();
};

//定義咱們的hichat類
var HiChat = function() {
    this.socket = null;
};

//向原型添加業務方法
HiChat.prototype = {
    init: function() {//此方法初始化程序
        var that = this;
        //創建到服務器的socket鏈接
        this.socket = io.connect();
        //監聽socket的connect事件,此事件表示鏈接已經創建
        this.socket.on('connect', function() {
            //鏈接到服務器後,顯示暱稱輸入框
            document.getElementById('info').textContent = 'get yourself a nickname :)';
            document.getElementById('nickWrapper').style.display = 'block';
            document.getElementById('nicknameInput').focus();
        });
    }
};

 上面的代碼定義了整個程序須要使用的類HiChat,以後咱們處理消息顯示消息等全部業務邏輯均寫在這個類裏面。

首先定義了一個程序的初始化方法,這裏面初始化socket,監聽鏈接事件,一旦鏈接到服務器,便顯示暱稱輸入框。當用戶輸入暱稱後,即可以在服務器後臺接收到而後進行下一步的處理了。

設置暱稱

咱們要求鏈接的用戶須要首先設置一個暱稱,且這個暱稱還要惟一,也就是不能與別人同名。一是方便用戶區分,二是爲了統計在線人數,同時也方便維護一個保存全部用戶暱稱的數組。

爲此在後臺server.js中,咱們建立一個名叫users的全局數組變量,當一個用戶設置好暱稱發送到服務器的時候,將暱稱壓入users數組。同時注意,若是用戶斷線離開了,也要相應地從users數組中移除以保證數據的正確性。

在前臺,輸入暱稱點擊OK提交後,咱們須要發起一個設置暱稱的事件以便服務器偵聽到。將如下代碼添加到以前的init方法中。

www/scripts/hichat.js

//暱稱設置的肯定按鈕
document.getElementById('loginBtn').addEventListener('click', function() {
    var nickName = document.getElementById('nicknameInput').value;
    //檢查暱稱輸入框是否爲空
    if (nickName.trim().length != 0) {
        //不爲空,則發起一個login事件並將輸入的暱稱發送到服務器
        that.socket.emit('login', nickName);
    } else {
        //不然輸入框得到焦點
        document.getElementById('nicknameInput').focus();
    };
}, false);

 server.js

//服務器及頁面部分
var express = require('express'),
    app = express(),
    server = require('http').createServer(app),
    io = require('socket.io').listen(server),
    users=[];//保存全部在線用戶的暱稱
app.use('/', express.static(__dirname + '/www'));
server.listen(80);
//socket部分
io.on('connection', function(socket) {
    //暱稱設置
    socket.on('login', function(nickname) {
        if (users.indexOf(nickname) > -1) {
            socket.emit('nickExisted');
        } else {
            socket.userIndex = users.length;
            socket.nickname = nickname;
            users.push(nickname);
            socket.emit('loginSuccess');
            io.sockets.emit('system', nickname); //向全部鏈接到服務器的客戶端發送當前登錄用戶的暱稱 
        };
    });
});

 

須要解釋一下的是,在connection事件的回調函數中,socket表示的是當前鏈接到服務器的那個客戶端。因此代碼socket.emit('foo')則只有本身收穫得這個事件,而socket.broadcast.emit('foo')則表示向除本身外的全部人發送該事件,另外,上面代碼中,io表示服務器整個socket鏈接,因此代碼io.sockets.emit('foo')表示全部人均可以收到該事件。

上面代碼先判斷接收到的暱稱是否已經存在在users中,若是存在,則向本身發送一個nickExisted事件,在前端接收到這個事件後咱們顯示一條信息通知用戶。

將下面代碼添加到hichat.js的inti方法中。

www/scripts/hichat.js

this.socket.on('nickExisted', function() {
     document.getElementById('info').textContent = '!nickname is taken, choose another pls'; //顯示暱稱被佔用的提示
 });

若是暱稱沒有被其餘用戶佔用,則將這個暱稱壓入users數組,同時將其做爲一個屬性存到當前socket變量中,而且將這個用戶在數組中的索引(由於是數組最後一個元素,因此索引就是數組的長度users.length)也做爲屬性保存到socket中,後面會用到。最後向本身發送一個loginSuccess事件,通知前端登錄成功,前端接收到這個成功消息後將灰色遮罩層移除顯示聊天界面。

將下面代碼添加到hichat.js的inti方法中。

www/scripts/hichat.js

this.socket.on('loginSuccess', function() {
     document.title = 'hichat | ' + document.getElementById('nicknameInput').value;
     document.getElementById('loginWrapper').style.display = 'none';//隱藏遮罩層顯聊天界面
     document.getElementById('messageInput').focus();//讓消息輸入框得到焦點
 });

在線統計

這裏實現顯示在線用戶數及在聊天主界面中以系統身份顯示用戶鏈接離開等信息。

上面server.js中除了loginSuccess事件,後面還有一句代碼,經過io.sockets.emit 向全部用戶發送了一個system事件,傳遞了剛登入用戶的暱稱,全部人接收到這個事件後,會在聊天窗口顯示一條系統消息'某某加入了聊天室'。同時考慮到在前端咱們沒法得知用戶是進入仍是離開,因此在這個system事件裏咱們多傳遞一個數據來代表用戶是進入仍是離開。

將server.js中login事件更改以下:

server.js

socket.on('login', function(nickname) {
     if (users.indexOf(nickname) > -1) {
         socket.emit('nickExisted');
     } else {
         socket.userIndex = users.length;
         socket.nickname = nickname;
         users.push(nickname);
         socket.emit('loginSuccess');
         io.sockets.emit('system', nickname, users.length, 'login');
     };
 });

 較以前,多傳遞了一個login字符串。

同時再添加一個用戶離開的事件,這個可能經過socket.io自帶的disconnect事件完成,當一個用戶斷開鏈接,disconnect事件就會觸發。在這個事件中,作兩件事情,一是將用戶從users數組中刪除,一是發送一個system事件通知全部人'某某離開了聊天室'。

將如下代碼添加到server.js中connection的回調函數中。

server.js

//斷開鏈接的事件
socket.on('disconnect', function() {
    //將斷開鏈接的用戶從users中刪除
    users.splice(socket.userIndex, 1);
    //通知除本身之外的全部人
    socket.broadcast.emit('system', socket.nickname, users.length, 'logout');
});

 上面代碼經過JavaScript數組的splice方法將當前斷開鏈接的用戶從users數組中刪除,這裏咱們看到以前保存的用戶索引被使用了。同時發送和用戶鏈接時同樣的system事件通知全部人'某某離開了',爲了讓前端知道是離開事件,因此發送了一個'logout'字符串。

下面開始前端的實現,也就是接收system事件。

在hichat.js中,將如下代碼添加到init方法中。

www/scripts/hichat.js

this.socket.on('system', function(nickName, userCount, type) {
     //判斷用戶是鏈接仍是離開以顯示不一樣的信息
     var msg = nickName + (type == 'login' ? ' joined' : ' left');
     var p = document.createElement('p');
     p.textContent = msg;
     document.getElementById('historyMsg').appendChild(p);
     //將在線人數顯示到頁面頂部
     document.getElementById('status').textContent = userCount + (userCount > 1 ? ' users' : ' user') + ' online';
 });

 如今運行程序,打開多個瀏覽器標籤,而後登錄離開,你就能夠看到相應的系統提示消息了。

 

發送消息

用戶鏈接以及斷開咱們須要顯示系統消息,用戶還要頻繁的發送聊天消息,因此能夠考慮將消息顯示到頁面這個功能單獨寫一個函數方便咱們調用。爲此咱們向HiChat類中添加一個_displayNewMsg的方法,它接收要顯示的消息,消息來自誰,以及一個顏色共三個參數。由於咱們想系統消息區別於普通用戶的消息,因此增長一個顏色參數。同時這個參數也方便咱們以後實現讓用戶自定義文本顏色作準備。

將如下代碼添加到的個人HiChat類當中。

www/scripts/hichat.js

//向原型添加業務方法
HiChat.prototype = {
    init: function() { //此方法初始化程序
        //...
    },
    _displayNewMsg: function(user, msg, color) {
        var container = document.getElementById('historyMsg'),
            msgToDisplay = document.createElement('p'),
            date = new Date().toTimeString().substr(0, 8);
        msgToDisplay.style.color = color || '#000';
        msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span>' + msg;
        container.appendChild(msgToDisplay);
        container.scrollTop = container.scrollHeight;
    }
};

 在_displayNewMsg方法中,咱們還向消息添加了一個日期。咱們也判斷了該方法在調用時有沒有傳遞顏色參數,沒有傳遞顏色的話默認使用#000即黑色。

同時修改咱們在system事件中顯示系統消息的代碼,讓它調用這個_displayNewMsg方法。

www/scripts/hichat.js

this.socket.on('system', function(nickName, userCount, type) {
    var msg = nickName + (type == 'login' ? ' joined' : ' left');
    //指定系統消息顯示爲紅色
    that._displayNewMsg('system ', msg, 'red');
    document.getElementById('status').textContent = userCount + (userCount > 1 ? ' users' : ' user') + ' online';
});

 如今的效果以下:

有了這個顯示消息的方法後,下面就開始實現用戶之間的聊天功能了。

作法也很簡單,若是你掌握了上面所描述的emit發送事件,on接收事件,那麼用戶聊天消息的發送接收也就輕車熟路了。

首先爲頁面的發送按鈕寫一個click事件處理程序,咱們經過addEventListner來監聽這個click事件,當用戶點擊發送的時候,先檢查輸入框是否爲空,若是不爲空,則向服務器發送postMsg事件,將用戶輸入的聊天文本發送到服務器,由服務器接收並分發到除本身外的全部用戶。

將如下代碼添加到hichat.js的inti方法中。

www/scripts/hichat.js

document.getElementById('sendBtn').addEventListener('click', function() {
    var messageInput = document.getElementById('messageInput'),
        msg = messageInput.value;
    messageInput.value = '';
    messageInput.focus();
    if (msg.trim().length != 0) {
        that.socket.emit('postMsg', msg); //把消息發送到服務器
        that._displayNewMsg('me', msg); //把本身的消息顯示到本身的窗口中
    };
}, false);

 在server.js中添加代碼以接收postMsg事件。

server.js

io.on('connection', function(socket) {
    //其餘代碼。。。

    //接收新消息
    socket.on('postMsg', function(msg) {
        //將消息發送到除本身外的全部用戶
        socket.broadcast.emit('newMsg', socket.nickname, msg);
    });
});

 而後在客戶端接收服務器發送的newMsg事件,並將聊天消息顯示到頁面。

將如下代碼顯示添加到hichat.js的init方法中了。

this.socket.on('newMsg', function(user, msg) {
    that._displayNewMsg(user, msg);
});

運行程序,如今能夠發送聊天消息了。

發送圖片

上面已經實現了基本的聊天功能了,進一步,若是咱們還想讓用戶能夠發送圖片,那程序便更加完美了。

圖片不一樣於文字,但經過將圖片轉化爲字符串形式後,即可以像發送普通文本消息同樣發送圖片了,只是在顯示的時候將它還原爲圖片。

在這以前,咱們已經將圖片按鈕在頁面放好了,實際上是一個文件類型的input,下面只需在它身上作功夫即可。

用戶點擊圖片按鈕後,彈出文件選擇窗口供用戶選擇圖片。以後咱們能夠在JavaScript代碼中使用FileReader來將圖片讀取爲base64格式的字符串形式進行發送。而base64格式的圖片直接能夠指定爲圖片的src,這樣就能夠將圖片用img標籤顯示在頁面了。

爲此咱們監聽圖片按鈕的change事件,一但用戶選擇了圖片,便顯示到本身的屏幕上同時讀取爲文本發送到服務器。

將如下代碼添加到hichat.js的init方法中。

www/scripts/hichat.js

document.getElementById('sendImage').addEventListener('change', function() {
    //檢查是否有文件被選中
     if (this.files.length != 0) {
        //獲取文件並用FileReader進行讀取
         var file = this.files[0],
             reader = new FileReader();
         if (!reader) {
             that._displayNewMsg('system', '!your browser doesn\'t support fileReader', 'red');
             this.value = '';
             return;
         };
         reader.onload = function(e) {
            //讀取成功,顯示到頁面併發送到服務器
             this.value = '';
             that.socket.emit('img', e.target.result);
             that._displayImage('me', e.target.result);
         };
         reader.readAsDataURL(file);
     };
 }, false);

 上面圖片讀取成功後,調用_displayNImage方法將圖片顯示在本身的屏幕同時向服務器發送了一個img事件,在server.js中,咱們經過這個事件來接收並分發圖片到每一個用戶。同時也意味着咱們還要在前端寫相應的代碼來接收。

這個_displayNImage尚未實現,將會在下面介紹。

將如下代碼添加到server.js的socket回調函數中。

server.js

//接收用戶發來的圖片
 socket.on('img', function(imgData) {
    //經過一個newImg事件分發到除本身外的每一個用戶
     socket.broadcast.emit('newImg', socket.nickname, imgData);
 });

 同時向hichat.js的init方法添加如下代碼以接收顯示圖片。

 this.socket.on('newImg', function(user, img) {
     that._displayImage(user, img);
 });

有個問題就是若是圖片過大,會破壞整個窗口的佈局,或者會出現水平滾動條,因此咱們對圖片進行樣式上的設置讓它最多隻能以聊天窗口的99%寬度來顯示,這樣過大的圖片就會本身縮小了。

#historyMsg img {
    max-width: 99%;
}

但考慮到縮小後的圖片有可能失真,用戶看不清,咱們須要提供一個方法讓用戶能夠查看原尺寸大小的圖片,因此將圖片用一個連接進行包裹,當點擊圖片的時候咱們打開一個新的窗口頁面,並將圖片按原始大小呈現到這個新頁面中讓用戶查看。

因此最後咱們實現的_displayNImage方法應該是這樣的。

將如下代碼添加到hichat.js的HiChat類中。

www/scripts/hichat.js

_displayImage: function(user, imgData, color) {
    var container = document.getElementById('historyMsg'),
        msgToDisplay = document.createElement('p'),
        date = new Date().toTimeString().substr(0, 8);
    msgToDisplay.style.color = color || '#000';
    msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span> <br/>' + '<a href="' + imgData + '" target="_blank"><img src="' + imgData + '"/></a>';
    container.appendChild(msgToDisplay);
    container.scrollTop = container.scrollHeight;
}

 再次啓動服務器打開程序,咱們能夠發送圖片了。

發送表情

文字老是很難表達出說話時的面部表情的,因而表情就誕生了。

前面已經介紹過如何發送圖片了,嚴格來講,表情也是圖片,但它有特殊之處,由於表情能夠穿插在文字中一併發送,因此就不能像處理圖片那樣來處理表情了。

根據以往的經驗,其餘聊天程序是把表情轉爲符號,好比我想發笑臉,而且規定':)'這個符號代碼笑臉表情,而後數據傳輸過程當中其實轉輸的是一個冒號加右括號的組合,當每一個客戶端接收到消息後,從文字當中將這些表情符號提取出來,再用gif圖片替換,這樣呈現到頁面咱們就 看到了表情加文字的混排了。

你好,王尼瑪[emoji:23]------>你好,王尼瑪

上面形象地展現了咱們程序中表情的使用,能夠看出我規定了一種格式來表明表情,[emoji:xx],中括號括起來而後'emoji'加個冒號,後面跟一個數字,這個數字表示某個gif圖片的編號。程序中,若是咱們點擊表情按扭,而後呈現全部可用的表情圖片,當用戶選擇一個表情後,生成對應的代碼插入到當前待發送的文字消息中。發出去後,每一個人接收到的也是代碼形式的消息,只是在將消息顯示到頁面前,咱們將表情代碼提取出來,獲取圖片編號,而後用相應的圖片替換。

首先得將全部可用的表情圖片顯示到一個小窗口,這個窗口會在點擊了表情按鈕後顯示以下圖,在HTML代碼中已經添加好了這個窗口了,下面只需實現代碼部分。

咱們使用兔斯基做爲咱們聊天程序的表情包。能夠看到,有不少張gif圖,若是手動編寫的話,要花一些功夫,不斷地寫<img src='xx.gif'/>,因此考慮將這個工做交給代碼來自動完成,寫一個方法來初始化全部表情。

爲此將如下代碼添加到HiChat類中,並在init方法中調用這個方法。

www/scripts/hichat.js

_initialEmoji: function() {
    var emojiContainer = document.getElementById('emojiWrapper'),
        docFragment = document.createDocumentFragment();
    for (var i = 69; i > 0; i--) {
        var emojiItem = document.createElement('img');
        emojiItem.src = '../content/emoji/' + i + '.gif';
        emojiItem.title = i;
        docFragment.appendChild(emojiItem);
    };
    emojiContainer.appendChild(docFragment);
}

同時將如下代碼添加到hichat.js的init方法中。

www/scripts/hichat.js

this._initialEmoji();
 document.getElementById('emoji').addEventListener('click', function(e) {
     var emojiwrapper = document.getElementById('emojiWrapper');
     emojiwrapper.style.display = 'block';
     e.stopPropagation();
 }, false);
 document.body.addEventListener('click', function(e) {
     var emojiwrapper = document.getElementById('emojiWrapper');
     if (e.target != emojiwrapper) {
         emojiwrapper.style.display = 'none';
     };
 });

上面向頁面添加了兩個單擊事件,一是表情按鈕單擊顯示錶情窗口,二是點擊頁面其餘地方關閉表情窗口。

如今要作的就是,具體到某個表情被選中後,須要獲取被選中的表情,而後轉換爲相應的表情代碼插入到消息框中。

爲此咱們再寫一個這些圖片的click事件處理程序。將如下代碼添加到hichat.js的inti方法中。

www/scripts/hichat.js

document.getElementById('emojiWrapper').addEventListener('click', function(e) {
    //獲取被點擊的表情
    var target = e.target;
    if (target.nodeName.toLowerCase() == 'img') {
        var messageInput = document.getElementById('messageInput');
        messageInput.focus();
        messageInput.value = messageInput.value + '[emoji:' + target.title + ']';
    };
}, false);

 如今表情選中後,消息輸入框中能夠獲得相應的代碼了。

以後的發送也普通消息發送沒區別,由於以前已經實現了文本消息的發送了,因此這裏不用再實現什麼,只是須要更改一下以前咱們用來顯示消息的代碼,首先判斷消息文本中是否含有表情符號,若是有,則轉換爲圖片,最後再顯示到頁面。

爲此咱們寫一個方法接收文本消息爲參數,用正則搜索其中的表情符號,將其替換爲img標籤,最後返回處理好的文本消息。

將如下代碼添加到HiChat類中。

www/scripts/hichat.js

_showEmoji: function(msg) {
    var match, result = msg,
        reg = /\[emoji:\d+\]/g,
        emojiIndex,
        totalEmojiNum = document.getElementById('emojiWrapper').children.length;
    while (match = reg.exec(msg)) {
        emojiIndex = match[0].slice(7, -1);
        if (emojiIndex > totalEmojiNum) {
            result = result.replace(match[0], '[X]');
        } else {
            result = result.replace(match[0], '<img class="emoji" src="../content/emoji/' + emojiIndex + '.gif" />');
        };
    };
    return result;
}

 如今去修改以前咱們顯示消息的_displayNewMsg方法,讓它在顯示消息以前調用這個_showEmoji方法。

_displayNewMsg: function(user, msg, color) {
     var container = document.getElementById('historyMsg'),
         msgToDisplay = document.createElement('p'),
         date = new Date().toTimeString().substr(0, 8),
         //將消息中的表情轉換爲圖片
         msg = this._showEmoji(msg);
     msgToDisplay.style.color = color || '#000';
     msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span>' + msg;
     container.appendChild(msgToDisplay);
     container.scrollTop = container.scrollHeight;
 }

下面是實現後的效果:

主要功能已經完成得差很少了,爲了讓程序更加人性與美觀,能夠加入一個修改文字顏色的功能,以及鍵盤快捷鍵操做的支持,這也是通常聊天程序都有的功能,回車便可以發送消息。

文字顏色

萬幸,HTML5新增了一個專門用於顏色選取的input標籤,而且Chrome對它的支持很是之贊,直接彈出系統的顏色拾取窗口。

IE及FF中均是一個普通的文本框,不過不影響使用,只是用戶只能經過輸入具體的顏色值來進行顏色設置,沒有Chrome裏面那麼方便也直觀。

以前咱們的_displayNewMsg方法能夠接收一個color參數,如今要作的就是每次發送消息到服務器的時候,多加一個color參數就能夠了,同時,在顯示消息時調用_displayNewMsg的時候將這個color傳遞過去。

下面是修改hichat.js中消息發送按鈕代碼的示例:

document.getElementById('sendBtn').addEventListener('click', function() {
    var messageInput = document.getElementById('messageInput'),
        msg = messageInput.value,
        //獲取顏色值
        color = document.getElementById('colorStyle').value;
    messageInput.value = '';
    messageInput.focus();
    if (msg.trim().length != 0) {
        //顯示和發送時帶上顏色值參數
        that.socket.emit('postMsg', msg, color);
        that._displayNewMsg('me', msg, color);
    };
}, false);

 同時修改hichat.js中接收消息的代碼,讓它接收顏色值

this.socket.on('newMsg', function(user, msg, color) {
     that._displayNewMsg(user, msg, color);
 });

 這只是展現了發送按鈕的修改,改動很是小,只是每次消息發送時獲取一下顏色值,同時emit事件到服務器的時候也帶上這個顏色值,這樣前端在顯示時就能夠根據這個顏色值爲每一個不兩隻用戶顯示他們本身設置的顏色了。剩下的就是按相同的作法把發送圖片時也加上顏色,這裏省略。

最後效果:

按鍵操做

將如下代碼添加到hichat.js的inti方法中,這樣在輸入暱稱後,按回車鍵就能夠登錄,進入聊天界面後,回車鍵能夠發送消息。

document.getElementById('nicknameInput').addEventListener('keyup', function(e) {
      if (e.keyCode == 13) {
          var nickName = document.getElementById('nicknameInput').value;
          if (nickName.trim().length != 0) {
              that.socket.emit('login', nickName);
          };
      };
  }, false);
  document.getElementById('messageInput').addEventListener('keyup', function(e) {
      var messageInput = document.getElementById('messageInput'),
          msg = messageInput.value,
          color = document.getElementById('colorStyle').value;
      if (e.keyCode == 13 && msg.trim().length != 0) {
          messageInput.value = '';
          that.socket.emit('postMsg', msg, color);
          that._displayNewMsg('me', msg, color);
      };
  }, false);

 

部署上線

最後一步,固然就是將咱們的辛勤結晶部署到實際的站點。這應該是最激動人心也是如釋重負的一刻。但在這以前,讓咱們先添加一個node.js程序通用的package.json文件,該文件裏面能夠指定咱們的程序使用了哪些模塊,這樣別人在獲取到代碼後,只需經過npm install命令就能夠本身下載安裝程序中須要的模塊了,而不用咱們把模塊隨源碼一塊兒發佈。

添加package.json文件

將如下代碼保存爲package.json保存到跟server.js相同的位置。

{
    "name": "hichat",
    "description": "a realtime chat web application",
    "version": "0.4.0",
    "main": "server.js",
    "dependencies": {
        "express": "3.4.x",
        "socket.io": "0.9.x"
    },
    "engines": {
        "node": "0.10.x",
        "npm": "1.2.x"
    }
}

 

雲服務選擇與部署

首先咱們得選擇一個支持Node.js同時又支持web socket協議的雲服務器。由於只是用於測試,空間內存限制什麼的都無所謂,只要免費就行。Node.js在GitHub的Wiki頁面上列出了衆多支持Node.js環境的雲服務器,選來選去知足條件的只有heroku

若是你以前到heroku部署過相關Node程序的話,必定知道其麻煩之處,而且出錯了很是不容易調試。不過當我在寫這篇博客的時候,我發現了一個利器codeship,將它與你的github綁定以後,你每次提交了新的代碼它會自動部署到heroku上面。什麼都不用作!

代碼更新,環境設置,編譯部署,所有自動搞定,而且提供了詳細的log信息及各步驟的狀態信息。使用方法也是很簡單,註冊後按提示,兩三步搞定,鑑於本文已經夠長了,應該創紀錄了,這裏就很少說了。

已知問題

部署測試後,發現一些本地未出現的問題,主要有如下幾點:

  • 首次鏈接過慢,有時會失敗出現503錯誤,這個查了下heroku文檔,官方表示程序首次接入時受資源限制確實會很慢的,這就是用免費套餐註定被鄙視的結果,不過用於線上測試這點仍是可以忍受的
  • 發送表情時,Chrome會向服務器從新請求已經下載到客戶端的gif圖片,而IE和FF都無此問題,致使在Chrome裏表情會有延遲,進而出現聊天主信息窗口滾動也不及時的現象
  • 用戶未活動必定時間後會與服務器失連,socket自動斷開,不知道是socket.io內部機制仍是又是heroku搗鬼

總結展望

通過上面一番折騰,一個基本的聊天程序便打造完畢。能夠完善的地方還有許多,好比利用CSS3的動畫,徹底能夠製做出窗口抖動功能的。聽起來很不錯是吧。同時利用HTML5的Audio API,要實現相似微信的語音消息也不是不可能的,夠震撼吧。甚至還有Geolocaiton API咱們就能夠聯想到實現同城功能,利用Webcam能夠打造出視頻對聊,但這方面WebRTC已經作得很出色了。

PS:作程序員以前有兩個想法,一是寫個播放器,一是寫個聊天程序,如今圓滿了。

相關文章
相關標籤/搜索