Node.js在线聊天室实战
项目完整代码
项目完整代码可以通过wget下载:
# 下载代码
wget https://labfile.oss.aliyuncs.com/courses/455/louChat.zip
# 解压缩代码
unzip louChat.zip
同样的我们需要把 app.js 中第 59 行的代码中的端口号 3000 改为 8080。然后通过以下命令即可运行项目:
$ cd louChat
$ npm install
$ node app.js
运行效果为:
多次运行,也就是在我们的 WebIDE 中多点几次工具中的 Web 服务,即可登录多人,运行效果图前已经登录了用户名分别为 1 2 3 的用户。大概知道我们需要做什么东西后,下面教大家一步一步完成我们的项目。
二、框架搭建
本课程将通过使用 Node.js 与 socket.io 搭建服务程序,配合 Angular.js 能够动态声明内容,使用 bootstrap 框架的方式完成一个简单的聊天室应用。
选用 socket.io 是因为其封装了 WebSocket ,良好的兼容性,可以在不同的浏览器和移动设备上构建实时应用,非常方便和人性化。
通过从 npm init 指令开始,一步步实现所有功能。
1.1 package.json 配置文件
首先新建一个文件夹 louChats,作为项目的根目录,同时也是我们的项目名称,然后执行命令 - cd louChats && npm init
会出现许多配置项需要你输入,同时这一过程也是引导你创建一个 package.json 文件,完成配置项填写后,此文件就存在根目录 /louChats 下了。
2.1 服务端主程序文件 app.js
项目需要 Express 框架托管服务程序,还需要 socket.io 建立实时服务:
执行命令 npm install --save express 安装 Express;
执行命令 npm install --save socket.io 安装 socket.io。
在 /louChats 下新建 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 + "/client"));
app.get("/", function (req, res) {
res.sendfile("index.html");
});
var connectedSockets = {};
var allUsers = [{ nickname: "" }];
io.on("connection", function (socket) {
//有新用户进入聊天室
socket.on("addUser", function (data) {
// coding there ...
});
//有用户发送新消息
socket.on("addMessage", function (data) {
// coding there ...
});
//有用户退出聊天室
socket.on("disconnect", function () {
// coding there ...
});
});
http.listen(8080, function () {
console.log("app is running at port 8080 !");
});
- 服务端
2.1 服务端主程序文件 app.js
项目需要 Express 框架托管服务程序,还需要 socket.io 建立实时服务:
执行命令 npm install --save express 安装 Express;
执行命令 npm install --save socket.io 安装 socket.io。
在 /louChats 下新建 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 + “/client”));
app.get("/", function (req, res) {
res.sendfile(“index.html”);
});
var connectedSockets = {};
var allUsers = [{ nickname: “” }];
io.on(“connection”, function (socket) {
//有新用户进入聊天室
socket.on(“addUser”, function (data) {
// coding there …
});
//有用户发送新消息
socket.on(“addMessage”, function (data) {
// coding there …
});
//有用户退出聊天室
socket.on(“disconnect”, function () {
// coding there …
});
});
http.listen(8080, function () {
console.log(“app is running at port 8080 !”);
});
copy
在上面的代码中设置了三个事件监听代码:
- addUser
- addMessage
- disconnect
客户端
创建目录:
louChats
|--client
| |--assets
| | |--js
| | | `--client.js
| | |--css
| | `--index.css
| |--index.html
| |--message.html
| `--user.html
`--app.js
/louChats/client 目录下是静态资源文件和客户端页面文件,其中的 client.js 就是客户端的负责监听服务端请求的文件。
3.1 使用 bootstrap 和 jquery
使用 bootstrap 快速实现布局良好的界面,首先执行命令 - sudo npm install -g bower 安装 bower,
在 /louChats/client/assets 目录下执行命令 - bower install bootstrap#3.3.5即可。
3.2 使用 Angular.js
在 /louChats/client/assets 目录下执行命令 - bower install angular即可。
编辑 /louChats/client/index.html 文件:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>mychat</title>
<link
href="/assets/bower_components/bootstrap/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<link href="/assets/css/index.css" rel="stylesheet" />
<script src="/assets/bower_components/jquery/dist/jquery.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="/assets/bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script src="/assets/bower_components/angular/angular.min.js"></script>
<script src="./assets/js/client.js"></script>
</head>
<body ng-app="chatRoom" ng-controller="chatCtrl">
<div class="chat-room-wrapper" ng-show="hasLogined">
<div class="online panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">
Online<span class="user-number">{{users.length-1}}</span>
</h3>
</div>
<div class="user-wrapper panel-body">
<user
iscurrentreceiver="receiver===user.nickname"
info="user"
ng-click="setReceiver(user.nickname)"
ng-repeat="user in users"
></user>
</div>
</div>
<div class="chat-room panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">{{receiver?receiver:"Group-chat"}}</h3>
</div>
<div class="message-wrapper panel-body">
<message
self="nickname"
scrolltothis="scrollToBottom()"
info="message"
ng-repeat="message in messages"
></message>
</div>
<div class="panel-footer">
<form
class="post-form form-inline"
novalidate
name="postform"
ng-submit="postMessage()"
>
<input
type="text"
class="form-control"
ng-model="words"
placeholder="say something~~"
required
/>
<button
type="submit"
class="btn btn-success"
三、登录
/louChats/app.js 文件:
//有新用户进入聊天室
socket.on("addUser", function (data) {
if (connectedSockets[data.nickname]) {
//昵称已被占用
socket.emit("userAddingResult", { result: false });
} else {
socket.emit("userAddingResult", { result: true });
socket.nickname = data.nickname;
//保存每个socket实例,发私信需要用
connectedSockets[socket.nickname] = socket;
allUsers.push(data);
//广播欢迎新用户,除新用户外都可看到
socket.broadcast.emit("userAdded", data);
//将所有在线用户发给新用户
socket.emit("allUser", allUsers);
}
});
注:不同 socket.emit() 的对比:
- socket.emit():信息传输对象为当前 socket 对应的 client,可看做一对一;
- socket.broadcast.emit():信息传输对象为所有的 client,排除当前 socket 对应的 client;
- sockets.emit():信息传输对象为所有的 client。
修改 …/assets/js/client.js 文件:
// 登录进入聊天室
$scope.login = function () {
socket.emit("addUser", { nickname: $scope.nickname });
};
// 自动滚到最新信息的位置
$scope.scrollToBottom = function () {
messageWrapper.scrollTop(messageWrapper[0].scrollHeight);
};
// here coding ...
//收到登录结果
socket.on("userAddingResult", function (data) {
if (data.result) {
$scope.userExisted = false;
$scope.hasLogined = true;
} else {
//昵称被占用
$scope.userExisted = true;
}
});
//接收到欢迎新用户消息
socket.on("userAdded", function (data) {
if (!$scope.hasLogined) return;
$scope.publicMessages.push({ text: data.nickname, type: "welcome" });
$scope.users.push(data);
});
//接收到在线用户消息
socket.on("allUser", function (data) {
if (!$scope.hasLogined) return;
$scope.users = data;
});
完成之后的登录效果为:
样式有所不同,是因为我们还没有写 index.css 的内容。
四、显示在线人数
- 服务端
当用户登录成功后,服务端发送 addUser 的指令,指令中包含了所有用户信息的数组 - 客户端
…/assets/js/client.js 文件最后添加如下代码:
app.directive("user", [
"$timeout",
function ($timeout) {
return {
restrict: "E",
templateUrl: "user.html",
scope: {
info: "=",
iscurrentreceiver: "=",
setreceiver: "&",
},
};
},
]);
这是声明一个指令,将 user.html 页面嵌入 index.html
五、发送消息
- 服务端
//有用户发送新消息
socket.on("addMessage", function (data) {
if (data.to) {
//发给特定用户
connectedSockets[data.to].emit("messageAdded", data);
} else {
//群发-广播消息,除原发送者外都可看到
socket.broadcast.emit("messageAdded", data);
}
});
这里就可以运用之前说明的 socket.emit 和 socket.broadcast.emit 的不同来说实现不同的发送信息方式
2. 客户端
…/assets/js/client.js 文件:
$scope.postMessage = function () {
var msg = {
text: $scope.words,
type: "normal",
from: $scope.nickname,
to: $scope.receiver,
};
var rec = $scope.receiver;
if (rec) {
if (!$scope.privateMessages[rec]) {
$scope.privateMessages[rec] = [];
}
$scope.privateMessages[rec].push(msg);
} else {
$scope.publicMessages.push(msg);
}
$scope.words = "";
if (rec !== $scope.nickname) {
socket.emit("addMessage", msg);
}
};
$scope.setReceiver = function (receiver) {
$scope.receiver = receiver;
if (receiver) {
//私信用户
if (!$scope.privateMessages[receiver]) {
$scope.privateMessages[receiver] = [];
}
$scope.messages = $scope.privateMessages[receiver];
} else {
//广播
$scope.messages = $scope.publicMessages;
}
var user = userService.get($scope.users, receiver);
if (user) {
user.hasNewMessage = false;
}
};
// here coding ...
// 接收到新消息
socket.on("messageAdded", function (data) {
if (!$scope.hasLogined) return;
if (data.to) {
//私信
if (!$scope.privateMessages[data.from]) {
$scope.privateMessages[data.from] = [];
}
$scope.privateMessages[data.from].push(data);
} else {
//群发
$scope.publicMessages.push(data);
}
var fromUser = userService.get($scope.users, data.from);
var toUser = userService.get($scope.users, data.to);
if ($scope.receiver !== data.to) {
//与来信方不是正在聊天当中才提示新消息
if (fromUser && toUser.nickname) {
fromUser.hasNewMessage = true; //私信
} else {
toUser.hasNewMessage = true; //群发
}
}
});
// 新声明一个指令用于调用页面
app
.directive("message", [
"$timeout",
function ($timeout) {
return {
restrict: "E",
templateUrl: "message.html",
scope: {
info: "=",
self: "=",
scrolltothis: "&",
},
link: function (scope, elem, attrs) {
scope.time = new Date();
$timeout(scope.scrolltothis);
},
};
},
])
.directive("user", [
"$timeout",
function ($timeout) {
return {
restrict: "E",
templateUrl: "user.html",
scope: {
info: "=",
iscurrentreceiver: "=",
setreceiver: "&",
},
};
},
]);
服务端对发送消息注入了两个方法 postMessge 和 setReceiver,对比查看 index.html 中对两个方法的使用,前者是发送消息,后者是指定发送消息的对象。
新增一个 message 指令,将 message.html 页面嵌入到 index.html 中。
六、退出
- 服务端
/louChats/app.js 文件:
//有用户退出聊天室
socket.on("disconnect", function () {
//广播有用户退出
socket.broadcast.emit("userRemoved", {
nickname: socket.nickname,
});
for (var i = 0; i < allUsers.length; i++) {
if (allUsers[i].nickname == socket.nickname) {
allUsers.splice(i, 1);
}
}
//删除对应的socket实例
delete connectedSockets[socket.nickname];
});
2. 客户端
…/assets/js/client.js 文件:
//接收到用户退出消息
socket.on("userRemoved", function (data) {
if (!$scope.hasLogined) return;
$scope.publicMessages.push({ text: data.nickname, type: "bye" });
for (var i = 0; i < $scope.users.length; i++) {
if ($scope.users[i].nickname == data.nickname) {
$scope.users.splice(i, 1);
return;
}
}
});
完成后聊天界面展示:
最后 client.js 的代码为:
var app = angular.module("chatRoom", []);
app.factory("socket", function ($rootScope) {
var socket = io(); //默认连接部署网站的服务器
return {
on: function (eventName, callback) {
socket.on(eventName, function () {
var args = arguments;
$rootScope.$apply(function () {
//手动执行脏检查
callback.apply(socket, args);
});
});
},
emit: function (eventName, data, callback) {
socket.emit(eventName, data, function () {
var args = arguments;
$rootScope.$apply(function () {
if (callback) {
callback.apply(socket, args);
}
});
});
},
};
});
app.factory("userService", function ($rootScope) {
return {
get: function (users, nickname) {
if (users instanceof Array) {
for (var i = 0; i < users.length; i++) {
if (users[i].nickname === nickname) {
return users[i];
}
}
} else {
return null;
}
},
};
});
app.controller("chatCtrl", [
"$scope",
"socket",
"userService",
function ($scope, socket, userService) {
var messageWrapper = $(".message-wrapper");
$scope.hasLogined = false;
$scope.receiver = ""; //默认是群聊
$scope.publicMessages = []; //群聊消息
$scope.privateMessages = {}; //私信消息
$scope.messages = $scope.publicMessages; //默认显示群聊
$scope.users = []; //
$scope.login = function () {
//登录进入聊天室
socket.emit("addUser", {
nickname: $scope.nickname,
});
};
$scope.scrollToBottom = function () {
messageWrapper.scrollTop(messageWrapper[0].scrollHeight);
};
$scope.postMessage = function () {
var msg = {
text: $scope.words,
type: "normal",
from: $scope.nickname,
to: $scope.receiver,
};
var rec = $scope.receiver;
if (rec) {
//私信
if (!$scope.privateMessages[rec]) {
$scope.privateMessages[rec] = [];
}
$scope.privateMessages[rec].push(msg);
} else {
//群聊
$scope.publicMessages.push(msg);
}
$scope.words = "";
if (rec !== $scope.nickname) {
//排除给自己发的情况
socket.emit("addMessage", msg);
}
};
$scope.setReceiver = function (receiver) {
$scope.receiver = receiver;
if (receiver) {
//私信用户
if (!$scope.privateMessages[receiver]) {
$scope.privateMessages[receiver] = [];
}
$scope.messages = $scope.privateMessages[receiver];
} else {
//广播
$scope.messages = $scope.publicMessages;
}
var user = userService.get($scope.users, receiver);
if (user) {
user.hasNewMessage = false;
}
};
//收到登录结果
socket.on("userAddingResult", function (data) {
if (data.result) {
$scope.userExisted = false;
$scope.hasLogined = true;
} else {
//昵称被占用
$scope.userExisted = true;
}
});
//接收到欢迎新用户消息
socket.on("userAdded", function (data) {
if (!$scope.hasLogined) return;
$scope.publicMessages.push({
text: data.nickname,
type: "welcome",
});
$scope.users.push(data);
});
//接收到在线用户消息
socket.on("allUser", function (data) {
if (!$scope.hasLogined) return;
$scope.users = data;
});
//接收到用户退出消息
socket.on("userRemoved", function (data) {
if (!$scope.hasLogined) return;
$scope.publicMessages.push({
text: data.nickname,
type: "bye",
});
for (var i = 0; i < $scope.users.length; i++) {
if ($scope.users[i].nickname == data.nickname) {
$scope.users.splice(i, 1);
return;
}
}
});
//接收到新消息
socket.on("messageAdded", function (data) {
if (!$scope.hasLogined) return;
if (data.to) {
//私信
if (!$scope.privateMessages[data.from]) {
$scope.privateMessages[data.from] = [];
}
$scope.privateMessages[data.from].push(data);
} else {
//群发
$scope.publicMessages.push(data);
}
var fromUser = userService.get($scope.users, data.from);
var toUser = userService.get($scope.users, data.to);
if ($scope.receiver !== data.to) {
//与来信方不是正在聊天当中才提示新消息
if (fromUser && toUser.nickname) {
fromUser.hasNewMessage = true; //私信
} else {
toUser.hasNewMessage = true; //群发
}
}
});
},
]);
app
.directive("message", [
"$timeout",
function ($timeout) {
return {
restrict: "E",
templateUrl: "message.html",
scope: {
info: "=",
self: "=",
scrolltothis: "&",
},
link: function (scope, elem, attrs) {
scope.time = new Date();
$timeout(scope.scrolltothis);
},
};
},
])
.directive("user", [
"$timeout",
function ($timeout) {
return {
restrict: "E",
templateUrl: "user.html",
scope: {
info: "=",
iscurrentreceiver: "=",
setreceiver: "&",
},
};
},
]);
到此项目的核心代码就完成了,最后给出样式文件 index.css 代码:
* {
padding: 0;
margin: 0;
}
html,
body {
width: 100%;
height: 100%;
}
.userform-wrapper,
.chat-room-wrapper {
width: 100%;
height: 100%;
}
.userform-wrapper {
background-color: #ccc;
}
.userform-wrapper form.login {
text-align: center;
position: absolute;
top: 40%;
width: 100%;
}
.chat-room-wrapper {
min-width: 800px;
max-width: 1000px;
overflow: hidden;
margin: 0 auto;
text-align: center;
}
.chat-room-wrapper .chat-room .panel-body {
position: absolute;
top: 38px;
bottom: 55px;
overflow: auto;
width: 100%;
background-color: RGB(238, 238, 238);
}
.chat-room-wrapper .panel-footer {
position: absolute;
bottom: 0;
width: 100%;
}
.chat-room-wrapper .panel-footer input[type="text"] {
width: 80%;
}
.chat-room-wrapper .chat-room {
position: relative;
float: right;
height: 100%;
width: 75%;
}
.chat-room-wrapper .online {
position: relative;
width: 24%;
float: left;
height: 100%;
}
.chat-room-wrapper .online .user-number {
margin-left: 10px;
width: 20px;
height: 20px;
background: lightgray;
display: inline-block;
border-radius: 50%;
color: crimson;
vertical-align: text-top;
line-height: 20px;
font-size: 13px;
}
.chat-room-wrapper .online .user-wrapper {
background: RGB(238, 238, 238);
overflow-y: scroll;
overflow-x: hidden;
position: absolute;
width: 100%;
top: 38px;
bottom: 0;
padding: 0;
}
.chat-room-wrapper .online .user-wrapper .user {
text-align: left;
border-bottom: solid 1px #ccc;
padding: 10px 10px;
cursor: pointer;
font-size: 18px;
}
.chat-room-wrapper .online .user-wrapper .user:hover {
background-color: RGB(245, 250, 255);
}
.chat-room-wrapper .online .user-wrapper .user .avatar {
vertical-align: middle;
}
.chat-room-wrapper .online .user-wrapper .user .nickname {
margin-left: 20px;
vertical-align: middle;
color: rgba(0, 0, 0, 0.7);
}
.chat-room-wrapper .online .user-wrapper .user .unread {
vertical-align: middle;
margin-right: 5px;
color: red;
}
.chat-room-wrapper .system-notification {
background-color: lightsalmon;
display: inline-block;
padding: 2px 8px;
border-radius: 5px;
color: #fff;
margin-bottom: 10px;
}
.chat-room-wrapper .normal-message {
text-align: right;
margin: 0 10px 20px 20px;
}
.chat-room-wrapper .normal-message:after {
content: "";
display: block;
clear: both;
}
.chat-room-wrapper .normal-message.others {
text-align: left;
}
.chat-room-wrapper .normal-message .name-wrapper {
color: #aaa;
}
.chat-room-wrapper .normal-message .content {
display: inline-block;
color: #000;
border-radius: 3px;
position: relative;
margin-top: 5px;
max-width: 60%;
min-height: 40px;
text-align: left;
}
.chat-room-wrapper .normal-message.others .content-wrapper {
position: relative;
}
.chat-room-wrapper .avatar {
display: inline-block;
width: 40px;
height: 40px;
margin-top: 5px;
margin-left: 10px;
vertical-align: top;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.chat-room-wrapper .others .avatar {
margin-left: 0;
margin-right: 10px;
}
.yes {
color: yellow;
}
.no {
color: purple;
}
这样整个项目的介绍就完结了,项目中运用到了许多 Angular.js 的特性,这给项目带来了很好的体验,同时也给不熟悉 Angular.js 的同学带来了一定的学习压力。