├── .gitignore
├── .gitattributes
├── public
├── img
│ ├── bg.jpg
│ └── pl.jpg
├── css
│ └── index.css
├── js
│ └── index.js
└── less
│ └── index.less
├── config.json
├── package.json
├── LICENSE
├── README.md
├── index.html
├── backup
└── example.json
└── app.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | node_modules/
3 | backup/
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.css linguist-language=JavaScript
2 | *.html linguist-language=JavaScript
--------------------------------------------------------------------------------
/public/img/bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShanaMaid/websocket-express-webchat/HEAD/public/img/bg.jpg
--------------------------------------------------------------------------------
/public/img/pl.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShanaMaid/websocket-express-webchat/HEAD/public/img/pl.jpg
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "history_num":20,
3 | "sever_port":80,
4 | "backup":true,
5 | "backup_filename":"./backup/example.json"
6 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "websocket-express-webchat",
3 | "version": "1.0.1",
4 | "description": "基于websocket的一个简单的聊天室",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "angular": "^1.6.2",
13 | "animate.css": "^3.5.2",
14 | "async-lock": "^0.3.9",
15 | "bootstrap": "^3.3.7",
16 | "express": "^4.14.1",
17 | "less": "^2.7.2",
18 | "normalize.css": "^5.0.0",
19 | "socket.io": "^1.7.2"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Shana
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # websocket-express-webchat
2 | 基于websocket的一个简单的聊天室
3 | express+socket.io+animate.css+angular
4 |
5 | ## Warning!
6 | 请使用高版本nodejs,本项目包含部分ES6语法
7 |
8 | ## Demo
9 | 由于域名备案原因,不再提供在线版,请clone到本地使用
10 |
11 | ## 使用方法
12 | * Step 1 下载本项目
13 | ```
14 | https://github.com/ShanaMaid/websocket-express-webchat.git
15 | ```
16 |
17 | * Step 2 安装依赖
18 | ```
19 | npm install
20 | ```
21 |
22 | * Step 3 启动服务
23 | ```
24 | node app.js
25 | ```
26 |
27 | * Step 4 进入聊天室
28 | ```
29 | 访问 http://localhost/
30 | ```
31 |
32 | ## config.json
33 | 聊天室配置文件
34 | ```
35 | {
36 | "history_num":20, //服务器缓存的历史信息条数
37 | "sever_port":80, //服务器监听端口号
38 | "backup":true, //是否开启服务端信息备份
39 | "backup_filename":"./backup/example.json" //备份文件名字
40 | }
41 | ```
42 |
43 | ## 备份
44 | ```
45 | [
46 | {
47 | "name":"测试人员1",
48 | "time":"2017-2-13 23:32:17",
49 | "content":"一条简单的测试信息"
50 | },
51 | {
52 | "name":"测试人2",
53 | "time":"2017-2-13 23:33:42",
54 | "content":"那你很棒哦"
55 | },
56 | {
57 | "name":"测试人3",
58 | "time":"2017-2-13 23:33:54",
59 | "content":"肯定很棒哦"
60 | }
61 | ]
62 | ```
63 |
64 | ## 功能
65 | - [x] 进入房间通知
66 | - [x] 离开房间通知
67 | - [x] 消息接收与发送
68 | - [x] 在线列表
69 | - [x] 服务器端信息备份
70 |
71 | ## 版本更新记录
72 | * v1.0.3 聊天室在线人员显示错误,多人离线时会出现在线列表混乱。目前已修复
73 | * v1.0.2 聊天室历史加载记录,存在错误,已修复!
74 | * v1.0.1 服务器端信息备份
75 | * v1.0.0 即时聊天
76 |
77 | ## License
78 | see [MIT LICENSE](./LICENSE) for details
79 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 一个简单的Web聊天室
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
展开在线用户列表
13 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 当前在线人数:{{personlist.length}}
26 | 在线人列表:
27 |
{{x}}
28 |
收起
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/public/css/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | padding: 0;
3 | margin: 0;
4 | box-sizing: border-box;
5 | font-family: 'microsoft yahei';
6 | font-weight: bold;
7 | }
8 | #chat {
9 | margin: 0 auto;
10 | max-width: 600px;
11 | width: 100%;
12 | min-height: 200px ;
13 | height: 100vh;
14 | min-height: 500px;
15 | position: relative;
16 | }
17 | #message {
18 | width: 100%;
19 | height: 80%;
20 | overflow-y: scroll;
21 | box-sizing: border-box;
22 | background-image: url("/static/img/bg.jpg");
23 | background-size: cover;
24 | }
25 | #input {
26 | width: 100%;
27 | height: 15%;
28 | }
29 | #text {
30 | resize: none;
31 | width: 80%;
32 | height: 100%;
33 | float: left;
34 | }
35 | #send {
36 | height: 70%;
37 | width: 20%;
38 | text-align: center;
39 | float: right;
40 | }
41 | #item {
42 | padding: 0 20px;
43 | margin-bottom: 5px;
44 | word-wrap: normal;
45 | word-break: break-all;
46 | word-wrap: break-word;
47 | }
48 | #name {
49 | height: 30%;
50 | width: 20%;
51 | }
52 | #person_list {
53 | position: absolute;
54 | top: 0;
55 | left: 0;
56 | width: 100%;
57 | z-index: 11;
58 | background-color: blue;
59 | padding: 20px;
60 | display: none;
61 | background-image: url("/static/img/pl.jpg");
62 | background-size: cover;
63 | }
64 | #retract {
65 | text-align: center;
66 | }
67 | #spread {
68 | text-align: center;
69 | height: 5%;
70 | }
71 | /*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/
72 | ::-webkit-scrollbar {
73 | width: 15px;
74 | height: 16px;
75 | background-color: #F5F5F5;
76 | }
77 | /*定义滚动条轨道 内阴影+圆角*/
78 | ::-webkit-scrollbar-track {
79 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
80 | border-radius: 10px;
81 | background-color: #F5F5F5;
82 | }
83 | /*定义滑块 内阴影+圆角*/
84 | ::-webkit-scrollbar-thumb {
85 | border-radius: 10px;
86 | -webkit-box-shadow: inset 0 0 6px pink;
87 | background-color: pink;
88 | }
89 |
--------------------------------------------------------------------------------
/public/js/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const msg = document.getElementById('message');
3 | var app = angular.module('webchat', []);
4 |
5 | app.controller('myCtrl', function($scope) {
6 | $scope.data = []; //接收-消息队列
7 | $scope.name = '';
8 | $scope.content = '';
9 | $scope.personnum = 0;
10 | $scope.personlist = [];
11 | $scope.flag = false;
12 | const socket_url = 'http://localhost';
13 | var pl = angular.element(document.getElementById('person_list'));
14 |
15 | var socket = io(socket_url);
16 | socket.on('news', (data) => {
17 | ($scope.data).push(data);
18 | $scope.$apply();
19 | msg.scrollTop = msg.scrollHeight;
20 | });
21 |
22 | socket.on('history', (data) => {
23 | for(let x in data){
24 | ($scope.data).push(data[x]);
25 | }
26 | ($scope.data).push({content:'----------以上是历史消息-----------'});
27 | $scope.$apply();
28 | msg.scrollTop = msg.scrollHeight;
29 | });
30 |
31 | socket.on('updatePerson', (data) => {
32 | $scope.personlist = data;
33 | $scope.$apply();
34 | });
35 |
36 | $scope.sendMsg = (data = $scope.content)=>{
37 | var date = new Date();
38 | if (!$scope.flag){
39 | $scope.flag = true;
40 | socket.emit('setUserName', $scope.name);
41 | }
42 | if ($scope.content!='')
43 | socket.emit('sendMsg', data);
44 | $scope.content='';
45 | };
46 |
47 | $scope.enter = function(e){
48 | var keycode = window.event?e.keyCode:e.which;
49 | if(keycode==13){
50 | $scope.sendMsg();
51 | }
52 | };
53 |
54 | $scope.retract = function () {
55 | pl.removeClass('flipInX');
56 | pl.addClass('flipOutX');
57 | };
58 |
59 | $scope.spread = function () {
60 | pl.removeClass('flipOutX');
61 | pl.css({display:"block"});
62 | pl.addClass('flipInX');
63 | };
64 | });
65 |
--------------------------------------------------------------------------------
/backup/example.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name":"测试人员1",
4 | "time":"2017-2-13 23:32:17",
5 | "content":"一条简单的测试信息"
6 | },
7 | {
8 | "name":"测试人2",
9 | "time":"2017-2-13 23:33:42",
10 | "content":"那你很棒哦"
11 | },
12 | {
13 | "name":"测试人3",
14 | "time":"2017-2-13 23:33:54",
15 | "content":"肯定很棒哦"
16 | },
17 | {
18 | "name":"12",
19 | "time":"2017-2-15 17:41:39",
20 | "content":"12"
21 | },
22 | {
23 | "name":"温柔",
24 | "time":"2017-2-15 17:41:43",
25 | "content":"温柔为温柔"
26 | },
27 | {
28 | "name":"23423",
29 | "time":"2017-2-15 17:41:47",
30 | "content":"4234234"
31 | },
32 | {
33 | "name":"2345而额温柔",
34 | "time":"2017-2-15 17:41:53",
35 | "content":"二23日23sdf"
36 | },
37 | {
38 | "name":"阿什顿飞",
39 | "time":"2017-2-15 17:41:57",
40 | "content":"二请问人情味"
41 | },
42 | {
43 | "name":"让人23",
44 | "time":"2017-2-15 17:42:2",
45 | "content":"23日23 23 3"
46 | },
47 | {
48 | "name":"3241432",
49 | "time":"2017-2-15 18:58:0",
50 | "content":"234124214"
51 | },
52 | {
53 | "name":"234",
54 | "time":"2017-2-15 18:58:4",
55 | "content":"dfsdfs"
56 | },
57 | {
58 | "name":"12354",
59 | "time":"2017-2-15 18:58:7",
60 | "content":"21341234"
61 | },
62 | {
63 | "name":"2341234",
64 | "time":"2017-2-15 18:58:11",
65 | "content":"2341243"
66 | },
67 | {
68 | "name":"21341234",
69 | "time":"2017-2-15 18:58:15",
70 | "content":"123412341234"
71 | },
72 | {
73 | "name":"324234",
74 | "time":"2017-2-15 19:4:45",
75 | "content":"234234"
76 | },
77 | {
78 | "name":"2342134",
79 | "time":"2017-2-15 19:4:49",
80 | "content":"说的方式答复"
81 | },
82 | {
83 | "name":"21342",
84 | "time":"2017-2-15 19:4:53",
85 | "content":"2341234"
86 | },
87 | {
88 | "name":"时代发生的",
89 | "time":"2017-2-15 19:4:56",
90 | "content":"sdf收到"
91 | },
92 | {
93 | "name":"234234",
94 | "time":"2017-2-15 19:5:0",
95 | "content":"2342343"
96 | }
97 | ]
--------------------------------------------------------------------------------
/public/less/index.less:
--------------------------------------------------------------------------------
1 | *{
2 | padding: 0;
3 | margin: 0;
4 | box-sizing: border-box;
5 | font-family: 'microsoft yahei';
6 | font-weight: bold;
7 | }
8 |
9 | #chat{
10 | margin: 0 auto;
11 | max-width: 600px;
12 | width: 100%;
13 | min-height:200px ;
14 | height: 100vh;
15 | min-height: 500px;
16 | position: relative;
17 | }
18 |
19 | #message{
20 | width: 100%;
21 | height: 80%;
22 | overflow-y: scroll;
23 | box-sizing: border-box;
24 | background-image: url("/static/img/bg.jpg");
25 | background-size: cover;
26 | }
27 |
28 | #input{
29 | width: 100%;
30 | height: 15%;
31 | }
32 |
33 | #text{
34 | resize: none;
35 | width: 80%;
36 | height: 100%;
37 | float: left;
38 | }
39 |
40 | #send{
41 | height: 70%;
42 | width: 20%;
43 | text-align: center;
44 | float: right;
45 | }
46 |
47 | #item{
48 | padding: 0 20px;
49 | margin-bottom:5px;
50 | word-wrap: break-word;
51 | word-wrap: normal;
52 | word-break:break-all;
53 | word-wrap:break-word;
54 | }
55 |
56 | #name{
57 | height: 30%;
58 | width: 20%;
59 | }
60 |
61 | #person_list{
62 | position: absolute;
63 | top: 0;
64 | left: 0;
65 | width: 100%;
66 | z-index: 11;
67 | background-color: blue;
68 | padding: 20px;
69 | display: none;
70 | background-image: url("/static/img/pl.jpg");
71 | background-size: cover;
72 | }
73 |
74 | #retract{
75 | text-align: center;
76 | }
77 |
78 | #spread{
79 | #retract;
80 | height: 5%;
81 | }
82 | /*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/
83 | ::-webkit-scrollbar
84 | {
85 | width: 15px;
86 | height: 16px;
87 | background-color: #F5F5F5;
88 | }
89 |
90 | /*定义滚动条轨道 内阴影+圆角*/
91 | ::-webkit-scrollbar-track
92 | {
93 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
94 | border-radius: 10px;
95 | background-color: #F5F5F5;
96 | }
97 |
98 | /*定义滑块 内阴影+圆角*/
99 | ::-webkit-scrollbar-thumb
100 | {
101 | border-radius: 10px;
102 | -webkit-box-shadow: inset 0 0 6px pink;
103 | background-color: pink;
104 | }
105 |
106 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var express = require('express');
3 | var socket = require('socket.io');
4 | var fs = require('fs');
5 |
6 |
7 | var config=JSON.parse(fs.readFileSync( 'config.json')); //读取配置文件
8 |
9 | var person = [];//记录在线情况
10 | var history = [];//需要缓存的消息
11 | var history_num = config.history_num ; //服务器缓存的历史消息条数
12 | var port = config.sever_port; //端口号
13 | var backup = config.backup; //是否开启备份
14 | var backup_filename = config.backup_filename; //备份文件名字
15 |
16 | var app = express();
17 | var server = app.listen(port);
18 | var io = new socket(server);
19 |
20 | app.use(express.static('node_modules'));
21 | app.use('/static',express.static('public'));
22 |
23 | app.get('/', (req, res) => {
24 | res.sendFile(__dirname + '/index.html');
25 | });
26 |
27 | io.on('connection', (socket) => {
28 | var user = '';
29 | var backup_file = fs.readFileSync(backup_filename);
30 | var backup_msg= backup_file!='' ? JSON.parse(backup_file) : [];
31 | var history = backup_msg.length<=history_num ? backup_msg : backup_msg.slice(backup_msg.length-history_num,backup_msg.length+history_num);
32 |
33 | socket.emit('history',history); //发送服务器记录的历史消息
34 | io.sockets.emit('updatePerson', person);
35 |
36 | socket.on('sendMsg', (data) => {
37 | var obj = new Object();
38 | obj.content = data;
39 | obj.time = Now();
40 | obj.name = user;
41 | if (history.length==history_num) {
42 | history.shift();
43 | }
44 | if (backup) {
45 | backupMsg(backup_filename,obj);
46 | }
47 | history.push(obj);
48 | io.sockets.emit('news',obj);
49 | });
50 |
51 | socket.on('setUserName',(data) => {
52 | user = data;
53 | person.push(user);
54 | io.sockets.emit('updatePerson',person);
55 | io.sockets.emit('news',{content:user+'进入房间',time:Now(),name:'系统消息'});
56 | });
57 |
58 | socket.on('disconnect', (socket) => {
59 | if (user!='') {
60 | person.forEach((value,index)=>{
61 | if (value===user) {
62 | person.splice(index,1);
63 | }
64 | });
65 | io.sockets.emit('news', {content: user + '离开房间', time: Now(), name: '系统消息'});
66 | io.sockets.emit('updatePerson', person);
67 | }
68 | });
69 |
70 | });
71 |
72 | function Now() {
73 | var date = new Date();
74 | return date.getFullYear()+'-'+(date.getMonth()+1)+'-'+date.getDate()+' '+date.getHours()+':'+date.getMinutes()+':'+date.getSeconds();
75 | }
76 |
77 | function backupMsg(filename,obj) {
78 | var backup_file = fs.readFileSync(backup_filename);
79 | var msg= backup_file!='' ? JSON.parse(backup_file) : [];
80 | msg.push(obj);
81 | var str = '[\n'
82 | msg.forEach((value,index) =>{
83 | if (index!==0) {
84 | str+=',\n';
85 | }
86 | str += ' {\n "name":"'+value.name+'",\n "time":"'+value.time+'",\n "content":"'+value.content+'"\n }';
87 | } );
88 | str += '\n]';
89 | fs.writeFile(filename, str, (err) => {
90 | if (err) {
91 | console.log("fail write :" + arr + " "+Date() + "\n error:"+err);
92 | }
93 | });
94 | }
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------