Node.js在线聊天室实战

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 !");
});
  1. 服务端
    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 的内容。

四、显示在线人数

  1. 服务端
    当用户登录成功后,服务端发送 addUser 的指令,指令中包含了所有用户信息的数组
  2. 客户端
    …/assets/js/client.js 文件最后添加如下代码:
app.directive("user", [
  "$timeout",
  function ($timeout) {
    return {
      restrict: "E",
      templateUrl: "user.html",
      scope: {
        info: "=",
        iscurrentreceiver: "=",
        setreceiver: "&",
      },
    };
  },
]);

这是声明一个指令,将 user.html 页面嵌入 index.html

五、发送消息

  1. 服务端
//有用户发送新消息
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 中。

六、退出

  1. 服务端
    /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 的同学带来了一定的学习压力。

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值