『转』★嗨聊


前端一直是一块充满惊喜的土地,不只是那些富有创造性的页面,还有那些惊赞的效果及不断推出的新技术。像node.js这样的后端开拓者直接将前端人员的能力扩大到了后端。瞬间就有了一统天下的感受,来往穿梭于先后端之间代码敲得飞起,今后由前端晋升为'先后端'。javascript

图片来自G+php

本文将使用Node.js加web socket协议打造一个网页即时聊天程序,取名为HiChat,中文翻过来就是'嗨聊',听中文名有点像是专为寂寞单身男女打造的~css

其中将会使用到express和socket.io两个包模块,下面会有介绍。html

源码&演示
前端

在线演示
 (heroku服务器网速略慢且免费套餐是小水管,建议下载代码本地运行)java

源码可访问项目的GitHub页面下载node

本地运行方法:git

  • 命令行运行npm install程序员

  • 模块下载成功后,运行node server启动服务器github

  • 打开浏览器访问localhost

下图为效果预览:

准备工做

本文示例环境为Windows,Linux也就Node的安装与命令行稍有区别,程序实现部分基本与平台无关。

Node相关

  • 你须要在本机安装Node.js(废话)

  • 多少须要一点Node.js的基础知识,若是还不曾了解过Node.js,这里有一篇不错的入门教程

而后咱们就能够开始建立一个简单的HTTP服务器啦。

相似下面很是简单的代码,它建立了一个HTTP服务器并监听系统的80端口。

//引入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代码写在一块儿进行请求的响 应,不太方便。

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将该页面返回到浏览器。能够看到较最开始,咱们的服务器代码简洁了很多。

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在先后端的句法是一致的,因此相反地,从服务器发送事件到客户端,在客户端接收并处理消息也是显而易见的事件了。这里只是简单介绍,具体下面会经过发送聊天消息进一步介绍。

基本页面

有了上面一些基础的了解,下面能够进入聊天程序功能的开发了。

首先咱们构建主页面。由于是比较大众化的应用了,界面不用多想,脑海中已经有大体的雏形,它有一个呈现消息的主窗体,还有一个输入消息的文本框,同时须要一个发送消息的按钮,这三个是必备的。

另外就是,这里还准备实现如下四个功能,因此界面上还有设置字体颜色,发送表情,发送图片和清除记录四个按钮。

最后的页面也就是先前截图展现的那们,而代码以下:

<!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事件更改以下:

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的回调函数中。

//断开链接的事件
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方法中。

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类当中。

//向原型添加业务方法
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方法中。

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方法中。

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回调函数中。

//接收用户发来的图片
 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类中。

_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:xx],中括号括起来而后'emoji'加个 冒号,后面跟一个数字,这个数字表示某个gif图片的编号。程序中,若是咱们点击表情按扭,而后呈现全部可用的表情图片,当用户选择一个表情后,生成对应 的代码插入到当前待发送的文字消息中。发出去后,每一个人接收到的也是代码形式的消息,只是在将消息显示到页面前,咱们将表情代码提取出来,获取图片编号, 而后用相应的图片替换。

首先得将全部可用的表情图片显示到一个小窗口,这个窗口会在点击了表情按钮后显示以下图,在HTML代码中已经添加好了这个窗口了,下面只需实现代码部分。

咱们使用兔斯基做为咱们聊天程序的表情包。能够看到,有不少张gif图,若是手动编写的话,要花一些功夫,不断地写<img src='xx.gif'>,因此考虑将这个工做交给代码来自动完成,写一个方法来初始化全部表情。

为此将如下代码添加到HiChat类中,并在init方法中调用这个方法。

_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方法中。

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方法中。

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:作程序员以前有两个想法,一是写个播放器,一是写个聊天程序,如今圆满了。

REFERENCE

  1. HOW TO SEND IMAGES THROUGH WEB SOCKETS WITH NODE.JS AND SOCKET.IO

  2. Simple Chat - Node.js + WebSockets

  3. Hosting compatible with Node

  4. What is a good tool to export a directory structure

  5. heroku

  6. codeship

相关文章
相关标签/搜索