├── public
└── src
│ ├── css
│ ├── userInfoMod.less
│ ├── roomList.less
│ ├── roomAdd.less
│ ├── index.less
│ ├── home.less
│ ├── userInfo.less
│ ├── about.less
│ ├── roomMember.less
│ ├── common.less
│ ├── login.less
│ ├── components.less
│ └── room.less
│ └── js
│ ├── roomList.js
│ ├── room.js
│ ├── mobile.js
│ ├── roomMember.js
│ ├── roomAdd.js
│ ├── userInfoMod.js
│ ├── login.js
│ └── index.js
├── .gitignore
├── views
├── room
│ ├── roomMember.ejs
│ ├── roomAdd.ejs
│ └── roomList.ejs
├── common
│ ├── footerScriptAxios.ejs
│ ├── head.ejs
│ └── infoTop.ejs
├── chat
│ ├── chatCtx.ejs
│ └── chatCtrl.ejs
├── special
│ ├── forkButton.ejs
│ ├── homeList.ejs
│ └── about.ejs
├── home.ejs
├── userInfo.ejs
├── error.ejs
├── about.ejs
├── userList.ejs
├── roomMember.ejs
├── roomList.ejs
├── userLogin.ejs
├── userInfoMod.ejs
├── roomAdd.ejs
├── room.ejs
├── user
│ ├── userInfo.ejs
│ ├── userList.ejs
│ ├── userLogin.ejs
│ └── userInfoMod.ejs
└── tipGoToMobile.ejs
├── bin
├── database
│ ├── scheme
│ │ ├── base.js
│ │ ├── room.js
│ │ ├── user.js
│ │ ├── mess.js
│ │ ├── info.js
│ │ └── index.js
│ ├── connect
│ │ └── index.js
│ └── model
│ │ └── index.js
├── router
│ ├── base.js
│ ├── index.js
│ ├── routerRoom.js
│ └── routerUser.js
├── socket
│ ├── data.js
│ ├── index.js
│ ├── io.js
│ ├── method.js
│ └── event.js
├── jwt
│ └── index.js
└── www
├── routes
├── index.js
├── basic.js
├── home.js
├── room.js
└── user.js
├── DEMO.md
├── package.json
├── gulpfile.js
├── app.js
├── README.md
└── API.md
/public/src/css/userInfoMod.less:
--------------------------------------------------------------------------------
1 | .info-gender {
2 | padding: 0;
3 | width: auto;
4 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | *.sock
4 | sessions
5 | public/dist
6 | npm-debug.log
7 |
--------------------------------------------------------------------------------
/views/room/roomMember.ejs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/views/common/footerScriptAxios.ejs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/src/css/roomList.less:
--------------------------------------------------------------------------------
1 | .box-list-wrap {
2 | font-size: @fontTitle;
3 | }
4 | .full-list {
5 | overflow-y: scroll;
6 | }
7 |
--------------------------------------------------------------------------------
/bin/database/scheme/base.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 |
4 | module.exports = Schema
5 |
--------------------------------------------------------------------------------
/bin/database/connect/index.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const db = mongoose.connect('mongodb://localhost/nodejsChat');
3 |
4 | module.exports = db
5 |
--------------------------------------------------------------------------------
/bin/router/base.js:
--------------------------------------------------------------------------------
1 | const {express, app} = require('../../app')
2 | const router = express.Router()
3 |
4 | module.exports = {
5 | express,
6 | app,
7 | router
8 | }
9 |
--------------------------------------------------------------------------------
/bin/database/scheme/room.js:
--------------------------------------------------------------------------------
1 | const Schema = require('./base')
2 |
3 | const roomScheme = new Schema({
4 | name: String,
5 | desc: String
6 | });
7 |
8 | module.exports = roomScheme
9 |
--------------------------------------------------------------------------------
/bin/database/scheme/user.js:
--------------------------------------------------------------------------------
1 | const Schema = require('./base')
2 |
3 | const userScheme = new Schema({
4 | name: String,
5 | pass: String
6 | });
7 |
8 | module.exports = userScheme
9 |
--------------------------------------------------------------------------------
/views/chat/chatCtx.ejs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/src/css/roomAdd.less:
--------------------------------------------------------------------------------
1 | .add-info {
2 | padding: @paddingBox @paddingBox * 2;
3 | .add-info-tip {
4 | color: red;
5 | font-size: @fontInput;
6 | padding: @paddingBox @paddingInput;
7 | }
8 | }
--------------------------------------------------------------------------------
/views/special/forkButton.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/bin/socket/data.js:
--------------------------------------------------------------------------------
1 | const data = {
2 | // 所有在线用户
3 | user: [],
4 | // 所有聊天室
5 | roomTest: {},
6 | room: [],
7 | roomList: [],
8 | currentRoomID: null,
9 | currentRoomIndex: null
10 | }
11 |
12 | module.exports = data
13 |
--------------------------------------------------------------------------------
/bin/socket/index.js:
--------------------------------------------------------------------------------
1 | const chatData = require('./data')
2 | const chatMethod = require('./method')
3 | const chatEvent = require('./event')
4 | const port = 9998
5 |
6 | chatEvent(chatData, chatMethod, port)
7 |
8 | module.exports = chatEvent
9 |
--------------------------------------------------------------------------------
/views/home.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%- include common/head.ejs %>
5 |
6 |
7 | <%- include common/infoTop.ejs %>
8 | <%- include special/homeList.ejs %>
9 |
10 |
11 |
--------------------------------------------------------------------------------
/views/userInfo.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%- include common/head.ejs %>
5 |
6 |
7 | <%- include common/infoTop.ejs %>
8 | <%- include user/userInfo.ejs %>
9 |
10 |
11 |
--------------------------------------------------------------------------------
/bin/socket/io.js:
--------------------------------------------------------------------------------
1 | const app = require('express')();
2 | const server = require('http').Server(app)
3 | const io = require('socket.io')(server)
4 |
5 | const list = {
6 | server: server,
7 | io: io
8 | }
9 |
10 | module.exports = list
11 |
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 | const home = require('./home');
2 | const room = require('./room');
3 | const user = require('./user');
4 |
5 | const route = {
6 | home: home,
7 | room: room,
8 | user: user
9 | }
10 |
11 | module.exports = route;
12 |
--------------------------------------------------------------------------------
/views/error.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%- include common/head.ejs %>
5 |
6 |
7 | <%- include common/infoTop.ejs %>
8 | <%= message %>
9 | <%= error.status %>
10 |
11 |
12 |
--------------------------------------------------------------------------------
/bin/database/scheme/mess.js:
--------------------------------------------------------------------------------
1 | const Schema = require('./base')
2 |
3 | const messScheme = new Schema({
4 | room: String,
5 | user: String,
6 | mess: String,
7 | time: Number,
8 | img: String
9 | });
10 |
11 | module.exports = messScheme
12 |
--------------------------------------------------------------------------------
/bin/database/scheme/info.js:
--------------------------------------------------------------------------------
1 | const Schema = require('./base')
2 |
3 | const infoScheme = new Schema({
4 | user: String,
5 | gender: String,
6 | img: String,
7 | city: String,
8 | hobbies: Array
9 | });
10 |
11 | module.exports = infoScheme
12 |
--------------------------------------------------------------------------------
/views/about.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%- include common/head.ejs %>
5 |
6 |
7 | <%- include special/forkButton.ejs %>
8 | <%- include common/infoTop.ejs %>
9 | <%- include special/about.ejs %>
10 |
11 |
--------------------------------------------------------------------------------
/routes/basic.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const {jwtDec} = require('../bin/jwt');
4 |
5 | const conf = {
6 | express: express,
7 | router: router,
8 | jwtDec: jwtDec,
9 | }
10 |
11 | module.exports = conf
12 |
--------------------------------------------------------------------------------
/public/src/css/index.less:
--------------------------------------------------------------------------------
1 | @import 'common';
2 | @import 'components';
3 | @import 'home';
4 | @import 'room';
5 | @import 'login';
6 | @import 'roomList';
7 | @import 'roomAdd';
8 | @import 'roomMember';
9 | @import 'userInfo';
10 | @import 'userInfoMod';
11 | @import 'about';
--------------------------------------------------------------------------------
/views/room/roomAdd.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/views/userList.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%- include common/head.ejs %>
5 |
6 |
7 | <%- include common/infoTop.ejs %>
8 | <%- include user/userList.ejs %>
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/views/roomMember.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%- include common/head.ejs %>
5 |
6 |
7 | <%- include common/infoTop.ejs %>
8 | <%- include room/roomMember.ejs %>
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/bin/database/scheme/index.js:
--------------------------------------------------------------------------------
1 | const userScheme = require('./user.js')
2 | const infoScheme = require('./info.js')
3 | const roomScheme = require('./room.js')
4 | const messScheme = require('./mess.js')
5 |
6 | const allScheme = {
7 | userScheme,
8 | infoScheme,
9 | roomScheme,
10 | messScheme
11 | }
12 |
13 | module.exports = allScheme
14 |
--------------------------------------------------------------------------------
/views/roomList.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%- include common/head.ejs %>
5 |
6 |
7 |
8 | <%- include common/infoTop.ejs %>
9 | <%- include room/roomList.ejs %>
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/views/userLogin.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%- include common/head.ejs %>
5 |
6 |
7 | <%- include special/forkButton.ejs %>
8 | <%- include user/userLogin.ejs %>
9 |
10 | <%- include common/footerScriptAxios.ejs %>
11 |
12 |
13 |
--------------------------------------------------------------------------------
/public/src/css/home.less:
--------------------------------------------------------------------------------
1 | .home-metro-box-wrap {
2 | overflow: hidden;
3 | }
4 | .home-metro-box {
5 | width: @metroBoxWidth;
6 | height: @metroBoxHeight;
7 | line-height: @metroBoxHeight;
8 | text-align: center;
9 | float: left;
10 | background-color: white;
11 | color: black;
12 | font-size: @fontMetroBox;
13 | margin: @metroBoxGap 0 0 @metroBoxGap;
14 | }
--------------------------------------------------------------------------------
/views/userInfoMod.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%- include common/head.ejs %>
5 |
6 |
7 | <%- include common/infoTop.ejs %>
8 | <%- include user/userInfoMod.ejs %>
9 |
10 | <%- include common/footerScriptAxios.ejs %>
11 |
12 |
13 |
--------------------------------------------------------------------------------
/bin/database/model/index.js:
--------------------------------------------------------------------------------
1 | const db = require('../connect')
2 | const dbScheme = require('../scheme')
3 |
4 | const allModel = {
5 | user: db.model('users', dbScheme.userScheme),
6 | info: db.model('infos', dbScheme.infoScheme),
7 | room: db.model('rooms', dbScheme.roomScheme),
8 | mess: db.model('messes', dbScheme.messScheme)
9 | }
10 |
11 | module.exports = allModel
12 |
--------------------------------------------------------------------------------
/public/src/css/userInfo.less:
--------------------------------------------------------------------------------
1 | .user-info-wrap {
2 | margin-top: @marginBox;
3 | }
4 | .user-info {
5 | padding: 20px;
6 | font-size: @fontInfo;
7 | }
8 | // Fix: Img can not be wrapped.
9 | .user-info-img-wrap {
10 | height: 2rem;
11 | width: 100%;
12 | text-align: center;
13 | }
14 | .user-info-img {
15 | width: 2rem;
16 | height: 2rem;
17 | border-radius: 1rem;
18 | }
--------------------------------------------------------------------------------
/views/common/head.ejs:
--------------------------------------------------------------------------------
1 | <%= headTitle %>
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/views/roomAdd.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%- include common/head.ejs %>
5 |
6 |
7 |
8 | <%- include common/infoTop.ejs %>
9 | <%- include room/roomAdd.ejs %>
10 |
11 |
12 | <%- include common/footerScriptAxios.ejs %>
13 |
14 |
15 |
--------------------------------------------------------------------------------
/views/room.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%- include common/head.ejs %>
5 |
6 |
7 | <%- include common/infoTop.ejs %>
8 | <%- include chat/chatCtx.ejs %>
9 | <%- include chat/chatCtrl.ejs %>
10 |
11 | <%- include common/footerScriptAxios.ejs %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/views/chat/chatCtrl.ejs:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 | 发送
13 |
14 |
15 |
--------------------------------------------------------------------------------
/views/special/homeList.ejs:
--------------------------------------------------------------------------------
1 |
2 | -
3 | 用户
4 |
5 | -
6 | 房间
7 |
8 | -
9 | >个人
10 |
11 | -
12 | 关于
13 |
14 |
--------------------------------------------------------------------------------
/views/room/roomList.ejs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/views/common/infoTop.ejs:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/views/user/userInfo.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |

>
4 |
5 |
6 | - 用户 <%= user %>
7 | - 性别 <%= gender %>
8 | - 城市 <%= city %>
9 | - 爱好 <%= hobbies === '' ? 未填写 : hobbies %>
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/public/src/css/about.less:
--------------------------------------------------------------------------------
1 | .about-card {
2 | margin-top: @paddingMainDivWidth;
3 | padding: @paddingMainDivWidth;
4 | background-color: white;
5 | }
6 | .about-card-title {
7 | text-align: center;
8 | padding: @paddingMainDivWidth * 3;
9 | color: @colorMain;
10 | font-size: @fontLogo;
11 | }
12 | .about-card-subtitle {
13 | border-bottom: 0.02rem solid @colorMain;
14 | padding-bottom: @paddingMainDivWidth / 2;
15 | font-size: @fontInfo;
16 | }
17 | .about-card-content {
18 | font-size: @fontInfo / 2;
19 | }
--------------------------------------------------------------------------------
/public/src/css/roomMember.less:
--------------------------------------------------------------------------------
1 | .room-member {
2 | margin-top: @paddingMainDivWidth / 2;
3 | }
4 | .room-member-list-wrap {
5 | overflow: hidden;
6 | }
7 | .room-member-list {
8 | float: left;
9 | }
10 | .room-member-list,.room-member-img {
11 | display: block;
12 | }
13 | .room-member-info {
14 | width: @baseWidth / 5;
15 | text-align: center;
16 | font-size: @fontInfo;
17 | }
18 | .room-member-img {
19 | margin: 0 auto;
20 | padding-top: @paddingMainDivWidth / 2;
21 | }
22 | .room-member-name {
23 | display: block;
24 | padding: 0.1rem 0;
25 | color: black;
26 | }
--------------------------------------------------------------------------------
/routes/home.js:
--------------------------------------------------------------------------------
1 | const {express, router, jwtDec} = require('./basic');
2 | const {room} = require('../bin/database/model')
3 |
4 | router.get('/', (req, res, next) => {
5 |
6 | const infoTopTitle = 'NChat'
7 | const headTitle = infoTopTitle
8 | const token = req.cookies.token
9 | jwtDec(token).then((tokenObj) => {
10 | const tokenObjUser = tokenObj.user
11 | const nextButton = {
12 | name: '✿',
13 | href: `/user/${tokenObjUser}`
14 | }
15 | res.render('home', {
16 | infoTopTitle,
17 | headTitle,
18 | nextButton,
19 | user: tokenObjUser,
20 | });
21 | })
22 | });
23 |
24 | module.exports = router
25 |
--------------------------------------------------------------------------------
/views/tipGoToMobile.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= headTitle %>
5 |
6 |
7 |
8 |
9 |
10 |
11 | <%- include common/infoTop.ejs %>
12 |
13 |
项目只支持移动端访问
14 |

15 |
PC端请打开浏览器的移动端视图,然后点击这里进入移动端页面。
16 |
17 |
18 |
--------------------------------------------------------------------------------
/views/user/userList.ejs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/src/js/roomList.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | const roomListDOM = document.getElementsByClassName('full-list')[0]
3 | const topTitleDOM = document.getElementsByClassName('top-title')[0]
4 |
5 | // 通过计算获取房间列表的合适高度
6 | function changeRoomListHeight() {
7 | const documentHeight = document.documentElement.clientHeight
8 | const topTitleDOMHeight = topTitleDOM.offsetHeight
9 | const roomListDOMHeight = documentHeight - topTitleDOMHeight
10 | roomListDOM.style.height = `${roomListDOMHeight}px`
11 | }
12 |
13 | // 视窗改变时重新计算高度
14 | window.addEventListener('resize', () => changeRoomListHeight(), false)
15 |
16 | // 文档加载完成时重新计算高度
17 | document.body.onload = () => changeRoomListHeight()
18 | })()
19 |
--------------------------------------------------------------------------------
/DEMO.md:
--------------------------------------------------------------------------------
1 | # 演示
2 |
3 | > 匿名聊天
4 |
5 | 
6 |
7 | > 用户聊天
8 |
9 | 
10 |
11 | > 成员&房间
12 |
13 | 
14 |
15 | > 离线通知
16 |
17 | 
18 |
19 | > 更多房间
20 |
21 | 
22 |
23 | > 房间独立
24 |
25 | 
--------------------------------------------------------------------------------
/public/src/js/room.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | /*
3 | * Why: Socket的端口和页面端口不一致,所以不能用相对路径,只能用绝对路径。
4 | * What: 本地开发的Host是localhost,在线的是对应的主机名。因此需要动态获取host名。
5 | * How: 为了保持开发和部署的一直,使用JS动态输出script标签。
6 | */
7 | const socketScriptTag = document.createElement('script')
8 | const socketScriptSrc = `//${document.location.hostname}:9998/socket.io/socket.io.js`
9 | socketScriptTag.src = socketScriptSrc
10 | document.head.appendChild(socketScriptTag)
11 | // index.min.js依赖socket.io.js
12 | // 所以要在socket文件加载完成后再加载index.min.js
13 | socketScriptTag.onload = () => {
14 | const indexScriptTag = document.createElement('script')
15 | indexScriptTag.src = '/js/index.min.js'
16 | document.body.appendChild(indexScriptTag)
17 | }
18 | })();
19 |
--------------------------------------------------------------------------------
/views/special/about.ejs:
--------------------------------------------------------------------------------
1 |
2 |
NChat
3 |
感谢你来体验NChat
4 |
5 | - 体验过程有什么疑问请去反馈房间提问
6 | - 如果你感觉这个项目不错,请给颗STAR,谢谢
7 |
8 |
这是一个全栈式开发的应用
9 |
10 | - 前端:Express+EJS+ES6+Less+Gulp
11 | - 后端:REST API+SocketIO+MongoDB
12 | - 项目:https://github.com/bergwhite/nchat
13 |
14 |
未来的版本
15 |
16 | - 后台:Vue
17 | - PC:React
18 | - WIN:Electron
19 | - 安卓:React Native
20 |
21 |
--------------------------------------------------------------------------------
/bin/jwt/index.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 |
3 | // 密钥
4 | const pubKey = 'secr'
5 |
6 | // JWT加密
7 | function jwtEnc(user, pass) {
8 | const token = jwt.sign({
9 | user: user,
10 | pass: pass,
11 | isToken: true,
12 | iat: Math.floor(Date.now() / 1000) + (60 * 60 * 24 * 1)
13 | }, pubKey)
14 | return token
15 | }
16 |
17 | // JWT解密
18 | // 返回Promise对象
19 | function jwtDec(token) {
20 | return new Promise((resolve, reject) => {
21 | jwt.verify(token, pubKey, function(err, tokenObj){
22 | // 捕获到错误或token过期则拒绝
23 | if (err || tokenObj.iat < (Date.now() / 1000)) {
24 | reject('Tooken is invaild')
25 | }
26 | // 否则返回处理成功
27 | else {
28 | resolve(tokenObj)
29 | }
30 | })
31 | })
32 | }
33 |
34 | module.exports = {
35 | jwtEnc,
36 | jwtDec
37 | }
--------------------------------------------------------------------------------
/views/user/userLogin.ejs:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
22 |
23 |
--------------------------------------------------------------------------------
/public/src/css/common.less:
--------------------------------------------------------------------------------
1 | // 布局宽度
2 |
3 | @baseWidth: 7.5rem;
4 |
5 | // 布局高度
6 |
7 | @boxTopHeight: @chatNameHeight;
8 |
9 | // 配色方案
10 |
11 | @colorMain: #2196f3;
12 | @colorMainSub: rgba(33, 150, 243, 0.7);
13 |
14 | // 字体方案
15 |
16 | @fontTitle: 0.3rem;
17 | @fontInfo: 0.4rem;
18 | @fontInput: 0.6rem;
19 | @fontLogo: 0.8rem;
20 | @fontMetroBox: @fontLogo;
21 |
22 | // 填充方案
23 |
24 | @paddingInput: 0.4rem;
25 | @paddingHorizontal: 0 0.5rem;
26 | @paddingMainDivWidth: 0.2rem;
27 |
28 | // CSS RESET
29 |
30 | * {
31 | box-sizing: border-box;
32 | }
33 | body {
34 | margin: 0;
35 | background-color: #EFEFEF;
36 | color: black;
37 | overflow: hidden;
38 | font-size: 16px;
39 | }
40 | ul,li,p,h1 {
41 | padding: 0;
42 | margin: 0;
43 | }
44 | li {
45 | list-style-type: none;
46 | }
47 | a {
48 | text-decoration: none;
49 | }
--------------------------------------------------------------------------------
/public/src/js/mobile.js:
--------------------------------------------------------------------------------
1 | (() => {
2 |
3 | // 判断否是移动端
4 | function isMobile() {
5 | const checkList = ['Android', 'iPhone', 'SymbianOS', 'Windows Phone', 'iPad', 'iPod']
6 | let checkState = false
7 | checkList.map((e) => {
8 | if(navigator.userAgent.indexOf(e) !== -1) checkState = true
9 | })
10 | return checkState
11 | }
12 |
13 | // 非移动端跳转至PC页
14 | if (!isMobile()) {
15 | document.location = '/tip/pc'
16 | }
17 |
18 | // 适配不同的高精度屏幕
19 | function compatibleDifferentDPI() {
20 | // 检测视图缩放比
21 | const pageScale = 1 / window.devicePixelRatio;
22 | // 缩放视图
23 | document.querySelector('meta[name="viewport"]').setAttribute('content',`initial-scale=${pageScale}, maximum-scale=${pageScale}, minimum-scale=${pageScale}, user-scalable=no`);
24 | // 设置根字体大小
25 | document.documentElement.style.fontSize = document.documentElement.clientWidth / 7.5 + 'px'
26 | }
27 |
28 | compatibleDifferentDPI()
29 |
30 | window.onresize = () => compatibleDifferentDPI()
31 |
32 | })();
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nodejs-chat",
3 | "version": "3.0.21",
4 | "private": true,
5 | "scripts": {
6 | "build": "gulp build",
7 | "start": "node ./bin/www",
8 | "mongod": "start X:/MongoDB/bin/mongod --dbpath=X:\\MongoDB\\data\\db"
9 | },
10 | "dependencies": {
11 | "body-parser": "^1.16.1",
12 | "cookie": "^0.3.1",
13 | "cookie-parser": "^1.4.3",
14 | "cors": "^2.8.3",
15 | "debug": "~2.6.0",
16 | "ejs": "~2.5.5",
17 | "express": "~4.14.1",
18 | "express-session": "^1.15.3",
19 | "gulp-less": "^3.3.2",
20 | "jsonwebtoken": "^7.4.3",
21 | "mongoose": "^4.10.0",
22 | "morgan": "~1.7.0",
23 | "serve-favicon": "~2.3.2",
24 | "session-file-store": "^1.0.0",
25 | "socket.io": "^2.0.1"
26 | },
27 | "devDependencies": {
28 | "babel-preset-es2015": "^6.24.1",
29 | "gulp": "^3.9.1",
30 | "gulp-babel": "^6.1.2",
31 | "gulp-clean-css": "^3.4.0",
32 | "gulp-rename": "^1.2.2",
33 | "gulp-uglifyjs": "^0.6.2",
34 | "http-proxy-middleware": "^0.17.4"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/public/src/css/login.less:
--------------------------------------------------------------------------------
1 | .login {
2 | width: 7rem;
3 | height: 3.3rem;
4 | position: absolute;
5 | left: 50%;
6 | margin-left: -3.5rem;
7 | top: 50%;
8 | margin-top: -1.65rem;
9 | }
10 | .login-info-wrap {
11 | overflow: hidden;
12 | }
13 | .login-user,.login-pass,.register-confirm, .login-confirm {
14 | padding: 0.1rem 0.2rem;
15 | width: 6.6rem;
16 | height: 1rem;
17 | margin-top: 0.3rem;
18 | font-size: 25px;
19 | }
20 | .login-user,.login-pass {
21 | width: 100%;
22 | }
23 | .register-confirm, .login-confirm {
24 | width: 7rem / 2 - 0.2;
25 | color: white;
26 | }
27 | .register-confirm {
28 | float: right;
29 | background-color: @colorMain;
30 | }
31 | .login-confirm {
32 | float: left;
33 | background-color: @colorMainSub;
34 | }
35 | .login-tip {
36 | display: block;
37 | color: red;
38 | position: absolute;
39 | left: 0;
40 | top: 0;
41 | }
42 | .login-confirm-wrap {
43 | position: relative;
44 | }
45 | .login-repass {
46 | position: absolute;
47 | right: 0;
48 | top: 0;
49 | }
50 |
51 | .login-logo-wrap {
52 | position: relative;
53 | }
54 | .login-logo {
55 | position: absolute;
56 | top: -2rem;
57 | width: 7rem;
58 | text-align: center;
59 | font-size: @fontLogo;
60 | color: @colorMain;
61 | }
--------------------------------------------------------------------------------
/public/src/js/roomMember.js:
--------------------------------------------------------------------------------
1 | const roomMember = document.getElementsByClassName('room-member-list-wrap')[0]
2 | const currentUrlOrigin = document.location.origin
3 | const socketScriptTag = document.createElement('script')
4 | const socketScriptSrc = `//${document.location.hostname}:9998/socket.io/socket.io.js`
5 | socketScriptTag.src = socketScriptSrc
6 | document.head.appendChild(socketScriptTag)
7 |
8 | socketScriptTag.onload = () => {
9 | const socketHostName = document.location.hostname
10 | const socketURI = `//${socketHostName}:9998/`
11 | const socket = io(socketURI)
12 | // 发送更新求
13 | socket.emit('user list req')
14 | // 更新在线列表
15 | socket.on('user list res', (v) => {
16 | // 每次更新前后清空内容
17 | roomMember.innerHTML = ''
18 | renderRoomMemberDOM(v)
19 | console.log(v)
20 | })
21 | }
22 |
23 | function insertToRoomMemberDOM({img, name}){
24 | const roomMemberTemplate = `
25 |
26 |
27 |
28 |

29 |
${name}
30 |
31 |
32 |
33 | `
34 | roomMember.innerHTML += roomMemberTemplate
35 | }
36 |
37 | function renderRoomMemberDOM(v){
38 | v.map((e, i)=>setTimeout(() => insertToRoomMemberDOM(e),200 * i))
39 | }
40 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp'),
2 | lessToCSS = require('gulp-less'),
3 | minifyCSS = require('gulp-clean-css'),
4 | minifyJS = require('gulp-uglifyjs'),
5 | babel = require('gulp-babel'),
6 | rename = require('gulp-rename');
7 |
8 | // 配置信息
9 | // 文件所在目录和输出目录
10 | const compileDir = {
11 | css: {
12 | src: 'public/src/css/index.less',
13 | dest: 'public/dist/css'
14 | },
15 | js: {
16 | src: 'public/src/js/',
17 | dest: 'public/dist/js'
18 | }
19 | };
20 |
21 | // 打包任务
22 | // 打包成一个CSS并压缩
23 | gulp.task('compile-css', () => {
24 | return gulp.src(compileDir.css.src)
25 | .pipe(lessToCSS())
26 | .pipe(minifyCSS())
27 | .pipe(rename((path) => {
28 | path.basename += '.min'
29 | }))
30 | .pipe(gulp.dest(compileDir.css.dest))
31 | });
32 |
33 | // 打包任务
34 | // 逐个文件ES6转ES5并压缩
35 | gulp.task('compile-js', () => {
36 | const JSTaskList = ['index', 'login', 'mobile', 'room', 'roomAdd', 'userInfoMod', 'roomMember', 'roomList']
37 | return JSTaskList.map((e) => {
38 | gulp.src(`${compileDir.js.src}${e}.js`)
39 | .pipe(babel({
40 | presets: ['es2015']
41 | }))
42 | .pipe(minifyJS())
43 | .pipe(rename((path) => {
44 | path.basename += '.min'
45 | }))
46 | .pipe(gulp.dest(compileDir.js.dest))
47 | })
48 | });
49 |
50 | // 构建任务
51 | // 构建线上代码
52 | gulp.task('build', ['compile-css', 'compile-js'])
--------------------------------------------------------------------------------
/public/src/css/components.less:
--------------------------------------------------------------------------------
1 | // fork按钮
2 |
3 | @forkMeButtomWidth: 4rem;
4 | @forkMeButtomHeight: @forkMeButtomWidth;
5 |
6 | // 顶栏组件
7 |
8 | .top-title {
9 | height: @boxTopHeight;
10 | line-height: @boxTopHeight;
11 | background-color: @colorMain;
12 | margin: 0;
13 | position: relative;
14 | text-align: center;
15 | font-size: @fontTitle;
16 | color: white;
17 | }
18 | .top-prev, .top-next {
19 | top: 0;
20 | color: white;
21 | width: 0.8rem;
22 | }
23 | .top-prev {
24 | position: absolute;
25 | left: 0;
26 | }
27 | .top-next {
28 | position: absolute;
29 | right: 0;
30 | }
31 |
32 | // MetroBox
33 |
34 | @metroBoxGap: 0.1rem;
35 | @metroBoxWidth: (@baseWidth - @metroBoxGap * 3) / 2;
36 | @metroBoxHeight: @metroBoxWidth;
37 |
38 | // 盒子组件
39 |
40 | @marginBox: 0.2rem;
41 | @paddingBox: 0.2rem;
42 |
43 | .box-list-wrap {
44 | margin: @marginBox 0;
45 | overflow-y: scroll;
46 | height: 12rem;
47 | .box-list {
48 | background-color: white;
49 | & + .box-list {
50 | margin-top: @marginBox / 2;
51 | }
52 | }
53 | .box-link {
54 | padding: @paddingBox;
55 | display: block;
56 | .box-name, .box-desc {
57 | color: black;
58 | }
59 | .box-desc {
60 | padding-top: @marginBox / 2;
61 | }
62 | }
63 | }
64 |
65 | // 输入框样式
66 |
67 | .input-default {
68 | width: @baseWidth - @paddingBox * 4;
69 | padding: @paddingInput;
70 | font-size: @fontInput;
71 | margin-bottom: @paddingBox * 1.5;
72 | }
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const path = require('path');
3 | const proxy = require('http-proxy-middleware');
4 | const favicon = require('serve-favicon');
5 | const logger = require('morgan');
6 | const cookieParser = require('cookie-parser');
7 | const bodyParser = require('body-parser');
8 | const cors = require('cors')
9 | const session = require('express-session');
10 | const FileStore = require('session-file-store')(session);
11 | const router = require('./routes/index');
12 | const app = express();
13 |
14 | // view engine setup
15 | app.set('views', path.join(__dirname, 'views'));
16 | app.set('view engine', 'ejs');
17 |
18 | // uncomment after placing your favicon in /public
19 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
20 | app.use(cors())
21 | app.use(logger('dev'));
22 | app.use(bodyParser.json());
23 | app.use(bodyParser.urlencoded({ extended: false }));
24 | app.use(cookieParser());
25 | app.use(express.static(path.join(__dirname, 'public/dist')));
26 |
27 | // 对未登录的页面进行重定向
28 | app.use((req, res, next) => {
29 | console.log(req.originalUrl)
30 | const redrictWihteList = ['/login', '/api/', '/tip/']
31 | var pageWillRedirct = redrictWihteList.every((e) => {
32 | return req.originalUrl.indexOf(e) === -1
33 | })
34 | if (pageWillRedirct && !req.cookies.token) {
35 | res.redirect('/login')
36 | }
37 | else {
38 | next()
39 | }
40 | })
41 |
42 | app.use('/', router.home);
43 | app.use('/room', router.room);
44 | app.use('/user', router.user);
45 |
46 | module.exports = {
47 | express,
48 | app,
49 | };
50 |
--------------------------------------------------------------------------------
/public/src/js/roomAdd.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | const roomName = document.getElementsByClassName('add-info-name')[0]
3 | const roomDesc = document.getElementsByClassName('add-info-desc')[0]
4 | const infoSubmitBtn = document.getElementsByClassName('top-next')[0]
5 | const infoSubmitTip = document.getElementsByClassName('add-info-tip')[0]
6 | const siteOrigin = document.location.origin
7 | const ajaxUrl = `${siteOrigin}/api/room/add`
8 |
9 | // 页面加载完成时,聚焦房间名字输入框
10 | document.body.onload = () => roomName.focus()
11 |
12 | // 监听提交按钮
13 | infoSubmitBtn.addEventListener('click', (e) => {
14 | // 阻止链接默认事件
15 | e.preventDefault()
16 | // 信息输入完整则提交,未输入完整则提示
17 | if (roomName.value === '') {
18 | infoSubmitTip.innerText = '请输入名称'
19 | roomName.focus()
20 | }
21 | else if (roomDesc.value === '') {
22 | infoSubmitTip.innerText = '请输入描述'
23 | roomDesc.focus()
24 | }
25 | else {
26 | submitNewRoomInfo()
27 | }
28 | }, false)
29 |
30 | // 新房间提交函数
31 | function submitNewRoomInfo() {
32 | const token = localStorage.getItem('token')
33 | axios.post(`${ajaxUrl}?token=${token}`, {
34 | name: roomName.value,
35 | desc: roomDesc.value
36 | }).then((res) => {
37 | // 成功则跳转到房间列表
38 | if (res.data.msgCode === 200) {
39 | document.location = '/room'
40 | }
41 | // 否则显示返回的信息
42 | else {
43 | infoSubmitTip.innerText = res.data.msgCtx
44 | roomName.focus()
45 | }
46 | }).catch((err) => {
47 | // 如果捕获到错误,则进行提示
48 | infoSubmitTip.innerText = err
49 | roomName.focus()
50 | })
51 | }
52 | })();
53 |
--------------------------------------------------------------------------------
/bin/router/index.js:
--------------------------------------------------------------------------------
1 | const {express, app} = require('./base.js')
2 | const {jwtEnc, jwtDec} = require('../jwt');
3 | const cookieParser = require('cookie-parser');
4 | const bodyParser = require('body-parser');
5 | const proxy = require('http-proxy-middleware');
6 | const session = require('express-session');
7 | const FileStore = require('session-file-store')(session);
8 | const cors = require('cors')
9 | const routerRoom = require('./routerRoom')
10 | const routerUser = require('./routerUser')
11 |
12 | // 支持跨域
13 | app.use(cors())
14 |
15 | // body和cookie中间件
16 | app.use(bodyParser.json());
17 | app.use(bodyParser.urlencoded({ extended: false }));
18 | app.use(cookieParser());
19 |
20 |
21 | // Token认证中间件
22 | app.use((req, res, next) => {
23 | // token认证白名单
24 | const whiteList = ['/api/user/register', '/api/user/login', '/api/user/logout', '/favicon.ico']
25 | const robotWhiteList = ['/api/robot']
26 | const originalUrl = req.originalUrl
27 | if (whiteList.indexOf(originalUrl) !== -1 || originalUrl.indexOf(robotWhiteList[0]) !== -1) {
28 | next()
29 | }
30 | else if (req.query.token) {
31 | const token = req.query.token
32 | jwtDec(token).then((tokenObjUser) => {
33 | next();
34 | })
35 | .catch((err) => {
36 | res.json({msgCode: 500,msgCtx: err})
37 | })
38 | }
39 | else {
40 | res.json({msgCode: 401,msgCtx: 'Please request with token.'})
41 | }
42 | });
43 |
44 | // POST中间件
45 | app.post((req, res, next) => {
46 | if (!req.body) {
47 | res.send({
48 | msgCode:304,
49 | msgCtx: 'Please enter user info.',
50 | })
51 | }
52 | else {
53 | next()
54 | }
55 | });
56 |
57 | // 启用路由
58 | app.use(routerRoom)
59 | app.use(routerUser)
60 |
61 | // 支持跨域访问聊天机器人
62 | app.use('/api/robot', proxy({
63 | target: 'http://www.tuling123.com',
64 | changeOrigin: true
65 | }));
66 |
67 | module.exports = app
--------------------------------------------------------------------------------
/views/user/userInfoMod.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

>
5 |
6 |
7 | >
8 |
9 |
10 |
37 |
38 |
39 | >
40 |
41 |
42 | >
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/public/src/js/userInfoMod.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | const userInfoGenderWrap = document.getElementsByClassName('info-gender-wrap')[0]
3 | const userInfoImg = document.getElementsByClassName('info-img')[0]
4 | const userDisImg = document.getElementsByClassName('user-info-img')[0]
5 | const userInfoCity = document.getElementsByClassName('info-city')[0]
6 | const userInfoHobbies = document.getElementsByClassName('info-hobbies')[0]
7 | const infoTip = document.getElementsByClassName('info-tip')[0]
8 | const infoModBtn = document.getElementsByClassName('top-next')[0]
9 | const siteOrigin = document.location.origin
10 | const ajaxUrl = `${siteOrigin}/api/user/info`
11 | const currentGender = ['男', '女', '保密']
12 | const changImgArr = ['men', 'women', 'random']
13 |
14 | // 监听点击头像换图片
15 | userInfoImg.addEventListener('click', (e) => {
16 | userDisImg.src = getRandomImg(changImgArr[userInfoGenderWrap.value])
17 | }, false)
18 |
19 | // 监听信息修改按钮的点击事件
20 | infoModBtn.addEventListener('click', (e) => {
21 | e.preventDefault()
22 | submitModUserInfo()
23 | }, false)
24 |
25 | function getRandomImg(gender) {
26 | if (gender === 'random' ) {
27 | gender = Math.random() > 0.5 ? changImgArr[0] : changImgArr[1]
28 | }
29 | const randomNumber = parseInt(Math.random() * 100)
30 | return 'https://randomuser.me/api/portraits/' + gender + '/' + randomNumber + '.jpg'
31 | }
32 |
33 | // 提交修改后的用户信息的函数
34 | function submitModUserInfo() {
35 | const token = localStorage.getItem('token')
36 | axios.put(`${ajaxUrl}?token=${token}`,{
37 | gender: currentGender[userInfoGenderWrap.value],
38 | img: userDisImg.src,
39 | city: userInfoCity.value,
40 | hobbies: userInfoHobbies.value
41 | }).then((res) => {
42 | // 成功则跳转到首页
43 | if (res.data.msgCode === 200) {
44 | document.location = '/'
45 | }
46 | // 否则显示返回的提示信息
47 | else {
48 | infoTip.innerText = res.data.msgCtx
49 | }
50 | // 捕获到错误则进行提示
51 | }).catch((err) => infoTip.innerText = err)
52 | }
53 | })();
54 |
--------------------------------------------------------------------------------
/public/src/js/login.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | const userName = document.getElementsByClassName('login-user')[0]
3 | const userPass = document.getElementsByClassName('login-pass')[0]
4 | const userLoginBtn = document.getElementsByClassName('login-confirm')[0]
5 | const userRegisterBtn = document.getElementsByClassName('register-confirm')[0]
6 | const userTip = document.getElementsByClassName('login-tip')[0]
7 |
8 | const siteOrigin = document.location.origin
9 | const ajaxUrl = {
10 | login: `${siteOrigin}/api/user/login`,
11 | register: `${siteOrigin}/api/user/register`,
12 | }
13 |
14 | // 默认使用登陆请求
15 | ajaxUrl.current = ajaxUrl.login
16 |
17 | document.body.onload = () => {
18 |
19 | // 焦点聚焦用户名
20 | userName.focus()
21 |
22 | // 监听用户登陆事件
23 | userLoginBtn.addEventListener('click', () => userHandle('login'),false)
24 |
25 | // 监听用户注册事件
26 | userRegisterBtn.addEventListener('click', () => userHandle('register'),false)
27 | }
28 |
29 | // 跳转到首页函数
30 | function jumpToMainPage() {
31 | document.location = siteOrigin
32 | }
33 |
34 | function userHandle (type) {
35 |
36 | // 根据传进来的类型,修改请求的Url
37 | if (type === 'login') {
38 | ajaxUrl.current = ajaxUrl.login
39 | }
40 | else if (type === 'register') {
41 | ajaxUrl.current = ajaxUrl.register
42 | }
43 |
44 | // 用户名为空则显示提示信息
45 | if (userName.value === '') {
46 | userName.focus()
47 | userTip.innerHTML = '请输入用户名'
48 | }
49 | // 账号只允许1-15个中文、字母、数字或下划线
50 | else if (userName.value.match(/^[\u4E00-\u9FA50-9a-zA-z_]{1,15}$/) === null) {
51 | userName.focus()
52 | userTip.innerHTML = '账号只允许1-15个中文、字母、数字或下划线'
53 | }
54 |
55 | // 密码为空则显示提示信息
56 | else if (userPass.value === '') {
57 | userPass.focus()
58 | userTip.innerHTML = '请输入密码'
59 | }
60 |
61 | // 密码只允许1-15个非空白字符
62 | else if (userPass.value.match(/\s/) !== null || userPass.value.match(/^\S{1,15}$/) === null) {
63 | userPass.focus()
64 | userTip.innerHTML = '密码只允许1-15个非空白字符'
65 | }
66 |
67 | // 否则提交请求
68 | else {
69 | console.log(ajaxUrl.current)
70 | axios.post(ajaxUrl.current, {
71 | name: userName.value,
72 | pass: userPass.value
73 | })
74 | .then((res) => {
75 | // 处理成功则跳转到首页
76 | if (res.data.msgCode === 200) {
77 | const token = res.data.token
78 | localStorage.setItem('token', token)
79 | jumpToMainPage()
80 | }
81 | // 否则显示错误信息
82 | else {
83 | userTip.innerHTML = res.data.msgCtx
84 | }
85 | // 捕获到错误则显示错误信息
86 | }).catch((err) => userTip.innerHTML = err)
87 | }
88 | }
89 | })();
90 |
--------------------------------------------------------------------------------
/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | const app = require('./router');
8 | const {routerApp} = require('./router')
9 | const debug = require('debug')('nodejs-note:server');
10 | const http = require('http');
11 | require('./socket')
12 |
13 | /**
14 | * Get port from environment and store in Express.
15 | */
16 |
17 | // catch 404 and forward to error handler
18 | app.use((req, res, next) => {
19 | const err = new Error('Not Found');
20 | err.status = 404;
21 | next(err);
22 | });
23 |
24 | // error handler
25 | app.use((err, req, res, next) => {
26 | // set locals, only providing error in development
27 | res.locals.message = err.message;
28 | res.locals.error = req.app.get('env') === 'development' ? err : {};
29 |
30 | // render the error page
31 | res.status(err.status || 500);
32 | const prevButton = {
33 | name: '<',
34 | href: '/',
35 | }
36 | const baseTitle = '访问的页面不存在'
37 | res.render('error', {
38 | headTitle: `${baseTitle} - NChat`,
39 | infoTopTitle: baseTitle,
40 | prevButton,
41 | });
42 | });
43 |
44 | const port = normalizePort(process.env.PORT || '8086');
45 | console.log('express-server on ' + port)
46 | app.set('port', port);
47 |
48 | /**
49 | * Create HTTP server.
50 | */
51 |
52 | const server = http.createServer(app);
53 |
54 | /**
55 | * Listen on provided port, on all network interfaces.
56 | */
57 |
58 | server.listen(port);
59 | server.on('error', onError);
60 | server.on('listening', onListening);
61 |
62 | /**
63 | * Normalize a port into a number, string, or false.
64 | */
65 |
66 | function normalizePort(val) {
67 | const port = parseInt(val, 10);
68 |
69 | if (isNaN(port)) {
70 | // named pipe
71 | return val;
72 | }
73 |
74 | if (port >= 0) {
75 | // port number
76 | return port;
77 | }
78 |
79 | return false;
80 | }
81 |
82 | /**
83 | * Event listener for HTTP server "error" event.
84 | */
85 |
86 | function onError(error) {
87 | if (error.syscall !== 'listen') {
88 | throw error;
89 | }
90 |
91 | const bind = typeof port === 'string'
92 | ? 'Pipe ' + port
93 | : 'Port ' + port;
94 |
95 | // handle specific listen errors with friendly messages
96 | switch (error.code) {
97 | case 'EACCES':
98 | console.error(bind + ' requires elevated privileges');
99 | process.exit(1);
100 | break;
101 | case 'EADDRINUSE':
102 | console.error(bind + ' is already in use');
103 | process.exit(1);
104 | break;
105 | default:
106 | throw error;
107 | }
108 | }
109 |
110 | /**
111 | * Event listener for HTTP server "listening" event.
112 | */
113 |
114 | function onListening() {
115 | const addr = server.address();
116 | const bind = typeof addr === 'string'
117 | ? 'pipe ' + addr
118 | : 'port ' + addr.port;
119 | debug('Listening on ' + bind);
120 | }
121 |
--------------------------------------------------------------------------------
/bin/router/routerRoom.js:
--------------------------------------------------------------------------------
1 | const {jwtEnc, jwtDec} = require('../jwt');
2 | const {router} = require('./base')
3 | const {room, mess} = require('../database/model')
4 |
5 | // 请求房间列表
6 | router.get('/api/room', (req, res, next) => {
7 |
8 | // 如果查询到了房间则返回
9 | // 否则提示暂时没有任何房间
10 | // 操作数据库的过程中遇到错误则把错误返回给前端
11 | room.find({}, (err, val) => {
12 | if (err) {
13 | res.send({
14 | msgCode:500,
15 | msgCtx: err,
16 | })
17 | }
18 | else if (val!==null) {
19 | res.send({
20 | msgCode:200,
21 | msgCtx: val,
22 | })
23 | }
24 | else {
25 | res.send({
26 | msgCode:404,
27 | msgCtx: 'Has not any room.',
28 | })
29 | }
30 | })
31 | })
32 |
33 | // 请求房间信息
34 | router.get('/api/room/info/:id', (req, res, next) => {
35 |
36 | // 如果房间存在则返回信息
37 | // 不存在则返回房间不存在的提示
38 | room.findOne({
39 | name: req.params.id,
40 | }, (err, val) => {
41 | if (val !== null) {
42 | res.send({msgCode: 200, msgCtx: val})
43 | }
44 | else {
45 | res.send({msgCode:404, msgCtx: 'Room is not exist.'})
46 | }
47 | })
48 | })
49 |
50 | // 请求添加房间
51 | router.post('/api/room/add', (req, res, next) => {
52 |
53 | // 如果当前房间存在则添加
54 | // 否则提示当前房间已存在
55 | // 操作数据库的过程中遇到错误则返回错误信息给前端
56 | room.findOne({
57 | name: req.body.name,
58 | }, (err, val) => {
59 | if (err) {
60 | res.send({
61 | msgCode: 500,
62 | msgCtx: err,
63 | })
64 | }
65 | else if (val !== null) {
66 | res.send({
67 | msgCode: 304,
68 | msgCtx: 'Room is exist.',
69 | })
70 | }
71 | else {
72 | name = req.body.name
73 | desc = req.body.desc || '暂时没有简介。'
74 | roomSave = new room({
75 | name: name,
76 | desc: desc,
77 | })
78 | roomSave.save()
79 | res.send({
80 | msgCode:200,
81 | msgCtx: 'Room add success.',
82 | })
83 | }
84 | })
85 | })
86 |
87 | // 请求房间聊天记录
88 | router.get('/api/room/mess/:id', (req, res, next) => {
89 |
90 | // 如果有这个房间则继续请求房间的聊天记录
91 | // 否则提示当前房间不存在
92 | room.findOne({
93 | name: req.params.id,
94 | }, (err, val) => {
95 | if (val !== null) {
96 |
97 | // 如果查询到当前房间的聊天记录则返回
98 | // 否则提示未查询到消息
99 | mess.find({
100 | room: req.params.id,
101 | }, (errChild, valChild) => {
102 | if (val !== null) {
103 | res.send({
104 | msgCode:200,
105 | msgCtx: valChild,
106 | })
107 | }
108 | else {
109 | res.send({
110 | msgCode:404,
111 | msgCtx: 'Has not found any mess.',
112 | })
113 | }
114 | })
115 | }
116 | else {
117 | res.send({
118 | msgCode:404,
119 | msgCtx: 'This room is not exist.',
120 | })
121 | }
122 | })
123 | })
124 |
125 | module.exports = router
126 |
--------------------------------------------------------------------------------
/bin/socket/method.js:
--------------------------------------------------------------------------------
1 | const io = require('./io.js').io
2 | const chatData = require('./data')
3 | const method = {
4 |
5 | // 判断房间是否存在
6 | isRoomExistReally (roomName) {
7 | if (typeof chatData.roomTest[roomName] !== 'undefined') {
8 | return true
9 | }
10 | return false
11 | },
12 |
13 | // 判断用户在指定房间的索引
14 | getUserIndexOfRoom (roomName, userName) {
15 | return chatData.roomTest[roomName].findIndex((e)=>e.name === userName)
16 | },
17 |
18 | // 添加房间
19 | setRoomList (roomName) {
20 | chatData.roomTest[roomName] = []
21 | },
22 |
23 | // 获取房间列表
24 | getRoomList () {
25 | return Object.keys(chatData.roomTest)
26 | },
27 |
28 | // 添加用户到指定房间
29 | addUserToTheRoom (roomName, userInfo) {
30 | if (!this.isRoomExistReally(roomName)) {
31 | this.setRoomList(roomName)
32 | }
33 | if (this.getUserIndexOfRoom(roomName, userInfo.name) === -1) {
34 | chatData.roomTest[roomName].push(userInfo)
35 | }
36 | },
37 |
38 | // 从指定房间删除用户
39 | delUserFromTheRoom (roomName, userName) {
40 | const userIndex = this.getUserIndexOfRoom(roomName, userName)
41 | if( userIndex !== -1) {
42 | chatData.roomTest[roomName].splice(userIndex ,1)
43 | }
44 | },
45 |
46 | // 获取某个房间所有用户
47 | getAllUsersFromOneRoom (roomName) {
48 | if (this.isRoomExistReally(roomName)) {
49 | return Object.keys(chatData.roomTest)
50 | }
51 | return []
52 | },
53 |
54 | // 初始化房间索引
55 | getCurrentRoomIndex (roomID) {
56 | return chatData.room.findIndex((val,index) => {
57 | console.log('name: ' + val.name)
58 | console.log('roomID: ' + roomID)
59 | return val.name === roomID
60 | })
61 | },
62 |
63 | // 获取当前房间ID
64 | getCurrentRoomID (socket) {
65 | const URI = socket.request.headers.referer
66 | const decodeURI = URI.match(/room\/(.*)?/)
67 | return decodeURI === null ? 'Chat Room' : decodeURIComponent(decodeURI[1].replace('/',''))
68 | },
69 |
70 | // 判断当前房间是否存在
71 | isRoomExist (arr, roomID) {
72 | const a = arr.filter((val) => {
73 | if(val.name === roomID) {
74 | return true
75 | }
76 | })
77 | return a.length !== 0
78 | },
79 |
80 | // 添加用户到指定房间
81 | addUserToRoom (name, roomID, userImg) {
82 | chatData.room.findIndex((val,index) => {
83 | if(val.name === roomID) {
84 | if(chatData.room[index].user.indexOf('name') === -1) {
85 | chatData.room[index].user.push(name)
86 | chatData.room[index].img.push(userImg)
87 | chatData.addUserStatus = true
88 | }
89 | else {
90 | chatData.addUserStatus = false
91 | }
92 | }
93 | })
94 | },
95 |
96 | // 删除指定房间用户
97 | delUserFromRoom (user, roomIndex) {
98 | const userIndex = chatData.room[roomIndex].user.indexOf(user)
99 | chatData.room[roomIndex].user.splice(userIndex, 1)
100 | chatData.room[roomIndex].img.splice(userIndex, 1)
101 | },
102 |
103 | // 向指定房间
104 | welcomeUser (roomID, msg) {
105 | io.to(roomID).emit('user login req', msg);
106 | }
107 | }
108 |
109 | module.exports = method
110 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 全栈式的开发多人在线聊天室
2 |
3 | * 3.1.0 / 项目已从Session认证改为Token认证,更好的支持跨域用户认证。
4 | * 3.1.1 / 修复PC页面访问的重定向问题。
5 |
6 | > 技术栈
7 |
8 | 觉得好的欢迎点个star ^_^。
9 |
10 | * 前端:Express & EJS & ES6 & Less & Gulp
11 | * 后端:Express & SocketIO & MongoDB & [REST API](API.md) & Token
12 | * 部署:Linux & PM2
13 |
14 | > 演示
15 |
16 | 
17 |
18 | * 全栈式的开发多人在线聊天室
19 | * 项目只适配了移动端,请使用浏览器的手机视图查看。
20 | * 项目源码:[https://github.com/bergwhite/nchat](https://github.com/bergwhite/nchat)
21 | * 在线演示:[http://y.bw2.me:8086](http://y.bw2.me:8086)
22 |
23 | > 目录
24 |
25 | ```
26 |
27 | ├─bin
28 | │ www // 后端 服务器
29 | │ database // 后端 数据库
30 | │ socket // 后端 socket
31 | | router // 后端 路由
32 | ├─sessions // 后端 session
33 | ├─public
34 | │ src // 前端 开发目录
35 | │ dist // 前端 线上目录
36 | ├─routes // 前端 路由
37 | ├─view // 前端 页面
38 | ├─app.js // 前端 服务器
39 | ├─gulpfile.js // 前端 Gulp
40 | ├─package.json
41 |
42 | ```
43 |
44 | > 安装
45 |
46 | * 项目基于MIT协议开源
47 | * 启动项目以前,请确保已经安装mongodb,并在package.json中修改MongoDB的安装路径(--dbpath)
48 |
49 | [Windows安装教程](https://jockchou.gitbooks.io/getting-started-with-mongodb/content/book/install.html) | Linux安装教程
50 |
51 | ```
52 |
53 | git clone https://github.com/bergwhite/nodejs-chat // 克隆项目
54 | cd nodejs-chat // 进入目录
55 | npm install // 安装依赖
56 | npm run build // 构建 线上代码
57 | npm run mongod // 开启 数据库
58 | npm run start // 开启 聊天室
59 |
60 | ```
61 |
62 | > 功能
63 |
64 | * 聊天
65 | - √ 群聊
66 | - × 私聊
67 | - √ 表情
68 | - × 斗图
69 | - × 更多表情
70 | - √ 聊天机器人(图灵)
71 |
72 | * 用户
73 | - √ 在线清单
74 | - √ 随机头像
75 | - √ 上线通知
76 | - √ 离线通知
77 | - √ 消息推送
78 | - × 上传头像
79 | - √ 在线统计
80 |
81 | * 房间
82 | - √ 房间列表
83 | - √ 添加房间
84 | - × 搜索房间
85 |
86 | > 优化
87 |
88 | * 基础
89 | - √ 代码压缩
90 |
91 | * 展示
92 | - √ 以前未设置头像的,显示默认头像
93 | - √ 加载速度优化
94 | - × 界面换肤
95 |
96 | * 开发
97 | - √ 组件化开发
98 | - √ 模块化开发
99 | - √ [REST API](API.md)
100 | - √ 使用PM2部署
101 | - √ 前后端分离
102 | - × 代码规范
103 | - × 测试用例
104 |
105 | * 安全
106 | - √ 密码使用MD5+SALT保存
107 | - √ 聊天内容过滤`< >`等特殊标签
108 |
109 | * 认证
110 | - √ Session
111 |
112 | * 部署
113 |
114 | - Linux & PM2
115 |
116 | > 踩坑
117 |
118 | 图灵机器人不支持跨域,通过代理中间件把请求代理到本地。
119 |
120 | ```
121 |
122 | var proxy = require('http-proxy-middleware');
123 |
124 | app.use('/api/robot', proxy({
125 | target: 'http://www.tuling123.com',
126 | changeOrigin: true
127 | }));
128 |
129 | ```
130 |
131 | Gulp使用通配符对多个文件处理,会压缩到一个文件中。以下是分别进行压缩的方式。
132 |
133 | ```
134 |
135 | const gulp = require('gulp'),
136 | minifyJS = require('gulp-uglifyjs'),
137 | babel = require('gulp-babel'),
138 | rename = require('gulp-rename');
139 |
140 | const compileDir = {
141 | css: {
142 | src: 'public/src/css/index.less',
143 | dest: 'public/dist/css'
144 | },
145 | js: {
146 | src: 'public/src/js/',
147 | dest: 'public/dist/js'
148 | }
149 | };
150 |
151 | gulp.task('compile-js', () => {
152 | const JSTaskList = ['index', 'login', 'mobile', 'room', 'roomAdd', 'userInfoMod', 'roomMember']
153 | return JSTaskList.map((e) => {
154 | gulp.src(`${compileDir.js.src}${e}.js`)
155 | .pipe(babel({
156 | presets: ['es2015']
157 | }))
158 | .pipe(minifyJS())
159 | .pipe(rename((path) => {
160 | path.basename += '.min'
161 | }))
162 | .pipe(gulp.dest(compileDir.js.dest))
163 | })
164 | });
165 |
166 | ```
167 |
168 | gulp-uglifyjs - No files given; aborting minification
169 |
170 | ```
171 |
172 | 之前删除了一个JS文件,但是没有删除JSTaskList中的对应值。编译时会报上面的错误。删除对应的值就编译成功了。
173 |
174 | ```
--------------------------------------------------------------------------------
/bin/socket/event.js:
--------------------------------------------------------------------------------
1 | const {jwtDec} = require('../jwt');
2 | const io = require('./io.js').io
3 | const server = require('./io.js').server
4 | const mess = require('../database/model').mess;
5 | const info = require('../database/model').info;
6 | const cookie = require('cookie')
7 | const cookieParser = require('cookie-parser')
8 |
9 | const event = function (chatData, chatMethod, port) {
10 |
11 | // socket链接时执行
12 | io.on('connection', (socket) => {
13 | const cookieData = cookie.parse(socket.handshake.headers.cookie);
14 | const token = cookieData.token
15 | jwtDec(token).then(function(tokenObj) {
16 | const currentRoomName = chatMethod.getCurrentRoomID(socket)
17 | let loginedUserName = ''
18 | let loginedUserImg = ''
19 | socket.join(currentRoomName) // 进入房间
20 | loginedUserName = tokenObj.user
21 |
22 | // 通过session中的用户名在数据库中查询用户信息
23 | info.findOne({user: loginedUserName}, (err, val) => {
24 |
25 | // 如果出错则打印出来
26 | if (err) {
27 | console.log('findInfoFromDB / err : ' + err)
28 | }
29 |
30 | // 如果查询到用户数据则保持图片Url到loginedUserImg变量里
31 | else if (val !== null) {
32 | loginedUserImg = val.img
33 |
34 | console.log(`${loginedUserName} joined ${currentRoomName}`)
35 |
36 | // 发送请求当前房间号事件
37 | socket.emit('room id req', {name: loginedUserName, img: loginedUserImg})
38 |
39 | // 添加用户到当前房间
40 | chatMethod.addUserToTheRoom(currentRoomName, {
41 | name: loginedUserName,
42 | img: loginedUserImg
43 | })
44 |
45 | // 发送用于调试的状态信息
46 | socket.emit('current status', chatData)
47 | console.log('currentRoomName: ' + currentRoomName)
48 | console.log('findInfoFromDB / loginedUserName: ' + loginedUserName)
49 | console.log('findInfoFromDB / loginedUserImg: ' + loginedUserImg)
50 | }
51 | })
52 |
53 | // 初始化房间
54 | chatData.currentRoomName = chatMethod.getCurrentRoomID(socket)
55 |
56 | // 获取房间成员列表
57 |
58 | socket.on('user list req', () => {
59 | socket.emit('user list res', chatData.roomTest[currentRoomName])
60 | })
61 |
62 | // 监听到相应后,存储当前的房间号
63 | socket.on('room id res', (currentRoomName) => {
64 |
65 | // 读取当前房间的聊天信息
66 | mess.find({'room': currentRoomName}).sort({'_id': -1}).limit(100).exec((err, data) => {
67 | console.log('room data ready / ' + (data.lenth !== 0))
68 | socket.emit('mess show res', data)
69 | })
70 |
71 | // 存储房间ID
72 | chatData.currentRoomName = currentRoomName
73 | console.log('connection / currentRoom: ' + chatData.currentRoomName)
74 |
75 | // 不存在则创建新房间
76 | if(!chatMethod.isRoomExist(chatData.room, currentRoomName)) {
77 | chatData.roomList.push(currentRoomName)
78 | chatData.room.push({
79 | name: currentRoomName,
80 | desc: null,
81 | user: [],
82 | img: []
83 | })
84 | }
85 | })
86 |
87 | // 处理发送消息事件
88 | socket.on('send message req', (time, id, msg) => {
89 | msg.user = msg.user || loginedUserName
90 | msg.img = msg.img || loginedUserImg
91 | // 把消息广播到相同房间
92 | socket.broadcast.to(id).emit('send message res', msg)
93 | // 存储消息到数据库
94 | const messEntity = new mess({
95 | room: id,
96 | user: msg.user,
97 | mess: msg.msg,
98 | time: time,
99 | img: msg.img
100 | })
101 | messEntity.save()
102 | })
103 |
104 | // 处理断开连接事件
105 | socket.on('disconnect', () => {
106 |
107 | // 重新获取房间名称和索引
108 | chatData.currentRoomName = chatMethod.getCurrentRoomID(socket)
109 | chatData.currentRoomIndex = chatMethod.getCurrentRoomIndex(chatData.currentRoomName)
110 | chatMethod.delUserFromTheRoom(chatData.currentRoomName, loginedUserName)
111 |
112 | // 向当前房间广播用户退出信息
113 | socket.broadcast.to(chatData.currentRoomName).emit('user logout req', {
114 | currentUser: loginedUserName,
115 | })
116 | console.log('disconnect / getCurrentRoomID / ' + chatData.currentRoomName)
117 | console.log('disconnect / getCurrentRoomIndex / ' + chatData.currentRoomIndex)
118 | console.log('disconnect / getCurrentUser / ' + loginedUserName)
119 | });
120 | }).catch((err) => {
121 | console.log('err: ' + err)
122 | })
123 | })
124 | server.listen(port)
125 | console.log(`socket-server on ${port}`)
126 | }
127 | module.exports = event
128 |
--------------------------------------------------------------------------------
/routes/room.js:
--------------------------------------------------------------------------------
1 | const {router, jwtDec} = require('./basic');
2 | const {room, mess} = require('../bin/database/model')
3 |
4 | const siteName = 'NChat'
5 |
6 | // 添加房间页面
7 | router.get('/rooms/add', (req, res, next) => {
8 |
9 | const infoTopTitle = '添加房间'
10 | const headTitle = `${infoTopTitle} - ${siteName}`
11 | const prevButton = {
12 | name: '<',
13 | href: '/room',
14 | }
15 | const nextButton = {
16 | name: '√',
17 | href: '',
18 | }
19 | res.render('roomAdd', {
20 | headTitle,
21 | infoTopTitle,
22 | prevButton,
23 | nextButton,
24 | })
25 | })
26 |
27 | // 用户信息页面
28 | router.get('/room/:id/member', (req, res, next) => {
29 |
30 | const infoTopTitle = '在线成员'
31 | const headTitle = `${infoTopTitle} - ${siteName}`
32 | const prevButton = {
33 | name: '<',
34 | href: `/room/${req.params.id}`,
35 | }
36 | res.render('roomMember', {
37 | headTitle,
38 | infoTopTitle,
39 | prevButton,
40 | room: req.params.id,
41 | })
42 | })
43 |
44 | // 房间列表页面
45 | router.get('/room', (req, res, next) => {
46 |
47 | const infoTopTitle = '房间列表'
48 | const headTitle = `${infoTopTitle} - ${siteName}`
49 | const prevButton = {
50 | name: '<',
51 | href: '/',
52 | }
53 | const nextButton = {
54 | name: '+',
55 | href: '/rooms/add',
56 | }
57 | room.find({}, (err, val) => {
58 |
59 | // 处理错误
60 | if (err) {
61 | res.send(`err: ${err}
`)
62 | }
63 |
64 | // 房间为空时
65 | else if (val === null) {
66 | res.render('roomList', {
67 | headTitle,
68 | infoTopTitle,
69 | prevButton,
70 | nextButton,
71 | room: [],
72 | });
73 | }
74 |
75 | // 渲染房间列表
76 | else {
77 | room.find({}, (err, val) => {
78 | res.render('roomList', {
79 | headTitle,
80 | infoTopTitle,
81 | prevButton,
82 | nextButton,
83 | room: val,
84 | });
85 | })
86 | }
87 | })
88 | })
89 |
90 | // 具体房间页面
91 | router.get('/room/:id', (req, res, next) => {
92 |
93 | const infoTopTitle = req.params.id
94 | const headTitle = `${infoTopTitle} - ${siteName}`
95 | const prevButton = {
96 | name: '<',
97 | href: '/room',
98 | }
99 | const nextButton = {
100 | name: '&',
101 | href: `/room/${req.params.id}/member`,
102 | }
103 |
104 | // 查询房间是否存在
105 | // 操作数据库的过程中发生错误或房间不存在,则把提示发送到前端页面
106 | room.findOne({
107 | name: req.params.id,
108 | }, (err, val) => {
109 | if (err) {
110 | res.send(`err: ${err}
`)
111 | }
112 | else if (val === null) {
113 | res.send('Room is not exist.
')
114 | }
115 | else {
116 |
117 | // 查询当前房间的所有聊天记录
118 | // 查询到了则渲染页面
119 | // 如果操作数据库的过程中发生错误则把错误信息发送过去
120 | mess.find({
121 | room: req.params.id,
122 | }, (err, val) => {
123 | if (err) {
124 | res.send(`err: ${err}
`)
125 | }
126 | else {
127 | res.render('room', {
128 | headTitle,
129 | infoTopTitle,
130 | prevButton,
131 | nextButton,
132 | room: val,
133 | roomId: req.params.id,
134 | })
135 | }
136 | })
137 | }
138 | })
139 |
140 | // 批量修改线上代码的房间名
141 | // 如果发送错误或房间未找到,则打印错误信息
142 | room.find({
143 | name: 'NodeJS Chat Room',
144 | }, (err, val) => {
145 | if (err) {
146 | console.log(`roomFindErr: ${err}`)
147 | }
148 | else if (val === null) {
149 | console.log('roomFind404: No room NodeJS Chat Room found.')
150 | }
151 | else {
152 | for(let i = 0; i < val.length; i++){
153 | room.update({
154 | name: 'NodeJS Chat Room',
155 | }, {
156 | $set: {
157 | name: 'center',
158 | }
159 | }, (err) => {
160 | if (err) {
161 | console.log(`roomUpdateErr: ${err}`)
162 | }
163 | })
164 | }
165 | }
166 | })
167 |
168 | // 批量修改线上代码聊天记录中的房间名
169 | // 如果发送错误或房间未找到,则打印错误信息
170 | mess.find({
171 | room: 'Chat Room',
172 | }, (err, val) => {
173 | if (err) {
174 | console.log(`roomChatErr: ${err}
`)
175 | }
176 | else if (val === null) {
177 | console.log('roomFind404: No info about Chat Room.')
178 | }
179 | else {
180 | for(let i = 0; i < val.length; i++){
181 | mess.update({room: 'Chat Room'}, {
182 | $set: {
183 | room: 'center',
184 | }
185 | }, (err) => {
186 | if (err) {
187 | console.log(`roomChatUpdateErr: ${err}`)
188 | }
189 | })
190 | }
191 | }
192 | })
193 | });
194 |
195 | module.exports = router
196 |
--------------------------------------------------------------------------------
/routes/user.js:
--------------------------------------------------------------------------------
1 | const {router, jwtDec} = require('./basic');
2 | const {info, user} = require('../bin/database/model')
3 | const crypto = require('crypto')
4 | const md5 = crypto.createHash('md5');
5 |
6 | const siteName = 'NChat'
7 |
8 | function cryptoPass (user, pass) {
9 | const uniquePassKey = '2333666'
10 | const uniqueString = `${user}${uniquePassKey}${pass}`
11 | return require('crypto').createHash('md5').update(uniqueString).digest('hex');
12 | }
13 |
14 | // 用户列表
15 | router.get('/user', (req, res, next) => {
16 |
17 | const infoTopTitle = `用户列表`
18 | const headTitle = `${infoTopTitle} - ${siteName}`
19 | const prevButton = {
20 | name: '<',
21 | href: '/',
22 | }
23 |
24 | // 把查询到的用户信息渲染到页面
25 | // 如果查询过程中出现错误或用户不存在,则发送对应信息到前端页面
26 | info.find({}, (err,val) => {
27 | if (err) {
28 | res.send(`err: ${err}
`)
29 | }
30 | else if (val === null) {
31 | res.send('用户不存在,请注册
')
32 | }
33 | else {
34 | const userList = val
35 | const renderObj = {
36 | headTitle,
37 | infoTopTitle,
38 | prevButton,
39 | userList,
40 | }
41 | res.render('userList', renderObj)
42 | }
43 | })
44 | })
45 |
46 | // 用户资料页面
47 | router.get('/user/:id', (req, res, next) => {
48 |
49 | const infoTopTitle = `${req.params.id}的主页`
50 | const headTitle = `${infoTopTitle} - ${siteName}`
51 | jwtDec(req.cookies.token).then((tokenObj) => {
52 | const tokenObjUser = tokenObj.user
53 | const prevButton = {
54 | name: '<',
55 | href: '/',
56 | }
57 | const nextButton = {
58 | name: '?',
59 | href: `/user/${tokenObjUser}/mod`,
60 | }
61 |
62 | // 把查询到的用户信息渲染到页面
63 | // 如果查询过程中出现错误或用户不存在,则发送对应信息到前端页面
64 | info.findOne({user: req.params.id}, (err,val) => {
65 | if (err) {
66 | res.send(`err: ${err}
`)
67 | }
68 | else if (val === null) {
69 | res.send('用户不存在,请注册
')
70 | }
71 | else {
72 | const renderObjBase = {
73 | headTitle,
74 | infoTopTitle,
75 | prevButton,
76 | user: val.user,
77 | gender: val.gender,
78 | img: val.img,
79 | city: val.city,
80 | hobbies: val.hobbies,
81 | }
82 | let renderObj = {}
83 | if (tokenObjUser === req.params.id) {
84 | renderObj = Object.assign({}, renderObjBase, {nextButton})
85 | }
86 | else {
87 | renderObj = Object.assign({}, renderObjBase)
88 | }
89 | res.render('userInfo', renderObj)
90 | }
91 | })
92 | })
93 | })
94 |
95 | // 用户资料修改页面
96 | router.get('/user/:id/mod', (req, res, next) => {
97 |
98 | jwtDec(req.cookies.token).then((tokenObj) => {
99 | const tokenObjUser = tokenObj.user
100 | const infoTopTitle = `修改资料`
101 | const headTitle = `${infoTopTitle} - ${siteName}`
102 | const prevButton = {
103 | name: '<',
104 | href: `/user/${tokenObjUser}`,
105 | }
106 | const nextButton = {
107 | name: '√',
108 | href: '',
109 | }
110 | if (tokenObjUser !== req.params.id) {
111 | res.redirect('/')
112 | }
113 | info.findOne({user: tokenObjUser}, (err,val) => {
114 | if (val === null) {
115 | res.send('用户不存在,请注册
')
116 | }
117 | else {
118 | const renderObj = {
119 | headTitle,
120 | infoTopTitle,
121 | prevButton,
122 | nextButton,
123 | user: val.user,
124 | gender: val.gender,
125 | img: val.img,
126 | city: val.city,
127 | hobbies: val.hobbies,
128 | }
129 | res.render('userInfoMod', renderObj)
130 | }
131 | })
132 | })
133 | })
134 |
135 | // 登陆页面
136 | router.get('/login', (req, res, next) => {
137 |
138 | user.find({}, (err, val) => {
139 | if (err) {
140 | console.log(err)
141 | }
142 | else if (val !== null) {
143 | // 对未更新成加密密码的进行替换
144 | for(let i = 0; i < val.length; i++){
145 | const userName = val[i].name
146 | const userPass = val[i].pass
147 | const encPass = cryptoPass(userName, userPass)
148 | if (userPass.length < 30) {
149 | user.update({name: userName}, {
150 | $set: {
151 | pass: encPass,
152 | }
153 | }, (err) => {
154 | if (err) {
155 | console.log(`updateCryptoedPass: ${err}`)
156 | }
157 | })
158 | }
159 | }
160 | }
161 | })
162 |
163 | const infoTopTitle = `登陆`
164 | const headTitle = `${infoTopTitle} - ${siteName}`
165 |
166 | res.render('userLogin', {headTitle,})
167 | })
168 |
169 | // 忘记密码页面
170 | router.get('/forget', (req, res, next) => res.send('Page is building.
') )
171 |
172 | // 修改密码页面
173 | router.get('/changepass', (req, res, next) => res.send('Page is building.
') )
174 |
175 | // 关于页面
176 |
177 | router.get('/about', (req, res, next) => {
178 |
179 | const infoTopTitle = `关于`
180 | const headTitle = `${infoTopTitle} - ${siteName}`
181 | const prevButton = {
182 | name: '<',
183 | href: `/`,
184 | }
185 | res.render('about', {
186 | headTitle,
187 | infoTopTitle,
188 | prevButton,
189 | })
190 |
191 | })
192 |
193 | router.get('/tip/pc', (req, res, next) => {
194 |
195 | const infoTopTitle = `跳转提示`
196 | const headTitle = `${infoTopTitle} - ${siteName}`
197 |
198 | res.render('tipGoToMobile', {
199 | infoTopTitle,
200 | headTitle,
201 | })
202 | })
203 |
204 | module.exports = router
205 |
--------------------------------------------------------------------------------
/bin/router/routerUser.js:
--------------------------------------------------------------------------------
1 | const {jwtEnc, jwtDec} = require('../jwt');
2 | const {router} = require('./base')
3 | const {info, user} = require('../database/model')
4 | const crypto = require('crypto')
5 | const md5 = crypto.createHash('md5');
6 |
7 | function cryptoPass (user, pass) {
8 | const uniquePassKey = '2333666'
9 | const uniqueString = `${user}${uniquePassKey}${pass}`
10 | return require('crypto').createHash('md5').update(uniqueString).digest('hex');
11 | }
12 |
13 | // 用户列表
14 | router.get('/api/user', (req, res, next) => {
15 | info.find({}, (err, val) => {
16 | if (val!==null) {
17 | res.send({
18 | msgCode:200,
19 | msgCtx: val,
20 | })
21 | }
22 | else {
23 | res.send({
24 | msgCode:404,
25 | msgCtx: 'Has not any user.',
26 | })
27 | }
28 | })
29 | })
30 |
31 | // 用户注册
32 | router.post('/api/user/register', (req, res, next) => {
33 |
34 | user.findOne({
35 | name: req.body.name,
36 | }, (err, val) => {
37 |
38 | // 用户已存在则返回已存在信息
39 | // 数据库操作过程中发生错误则进行相关提示
40 | // 否则继续执行
41 | if (err) {
42 | res.send({
43 | msgCode:500,
44 | msgCtx: err,
45 | })
46 | }
47 | else if (val !== null) {
48 | res.send({
49 | msgCode:304,
50 | msgCtx: 'User is exist.',
51 | })
52 | }
53 | else {
54 | const defaultUserImg = 'https://randomuser.me/api/portraits/men/1.jpg'
55 |
56 | // 设置别名
57 | const name = req.body.name
58 | const pass = cryptoPass(name, req.body.pass)
59 |
60 | // 保存账号
61 | userSave = new user({
62 | name: name,
63 | pass: pass,
64 | })
65 | userSave.save()
66 |
67 | // 保存资料
68 | infoSava = new info({
69 | user: name,
70 | gender: 'secure',
71 | img: defaultUserImg,
72 | city: 'beijing',
73 | hobbies: [],
74 | })
75 | infoSava.save()
76 |
77 | // 生成token
78 | const token = jwtEnc(name, pass)
79 | res.cookie('token', token, { expires: new Date(Date.now() + 60*60*24*1*1000), httpOnly: true });
80 | res.send({
81 | msgCode:200,
82 | msgCtx: 'Reg success.',
83 | token: token
84 | })
85 | }
86 | })
87 | });
88 |
89 | // 用户登陆
90 | router.post('/api/user/login', (req, res, next) => {
91 |
92 | // 设置别名
93 | const name = req.body.name
94 | const pass = cryptoPass(name, req.body.pass)
95 | console.log(`name: ${name} , pass: ${pass}`)
96 |
97 | // 查询数据库中发生错误或者用户名不存在、密码错误则进行相应的提示
98 | user.findOne({
99 | name: name,
100 | }, (err, val) => {
101 | if (err) {
102 | res.send({
103 | msgCode:500,
104 | msgCtx: err,
105 | })
106 | }
107 | else if (val === null) {
108 | res.send({
109 | msgCode:404,
110 | msgCtx: 'User is not exist.',
111 | })
112 | }
113 | else if(val.pass !== pass) {
114 | res.send({
115 | msgCode:403,
116 | msgCtx: `Pass is incorrect.`,
117 | })
118 | }
119 | else {
120 | // 生成token
121 | const token = jwtEnc(name, pass)
122 | res.cookie('token', token, { expires: new Date(Date.now() + 60*60*24*1*1000), httpOnly: true });
123 | res.send({
124 | msgCode:200,
125 | msgCtx: 'Login success.',
126 | token: token
127 | })
128 | }
129 | })
130 | });
131 |
132 | // 用户注销
133 |
134 | router.post('/api/user/logout', (req, res, next) => {
135 | const isCookieExist = req.cookies.token
136 | if (isCookieExist) {
137 | res.clearCookie('token')
138 | res.send({
139 | msgCode:200,
140 | msgCtx: 'Logout success.'
141 | })
142 | }
143 | else {
144 | res.send({
145 | msgCode:304,
146 | msgCtx: 'You have not login.'
147 | })
148 | }
149 | })
150 |
151 | // 用户资料
152 | router.get('/api/user/info/:id', (req, res, next) => {
153 |
154 | const token = req.query.token
155 | jwtDec(token).then((tokenObj) => {
156 | // 获取用户资料
157 | info.findOne({
158 | user: req.params.id,
159 | }, (err,val) => {
160 | if (err) {
161 | res.send({
162 | msgCode:500,
163 | msgCtx: err,
164 | })
165 | }
166 | else if (val === null) {
167 | res.send({
168 | msgCode:404,
169 | msgCtx: 'User not exist.',
170 | })
171 | }
172 | else {
173 | res.send({
174 | msgCode:200,
175 | msgCtx: val,
176 | });
177 | }
178 | })
179 | })
180 | });
181 |
182 | // 修改密码
183 | router.put('/api/user/pass', (req, res, next) => {
184 |
185 | const token = req.query.token
186 | jwtDec(token).then((tokenObj) => {
187 | const userName = tokenObj.user
188 | const passOld = cryptoPass(userName, req.body.passOld)
189 | const passNew = cryptoPass(userName, req.body.passNew)
190 |
191 | // 查询当前用户的账号
192 | user.findOne({
193 | name: userName,
194 | }, (err, val) => {
195 |
196 | // 输入的旧密码等于原始密码则执行
197 | // 不相等则返回提示信息
198 | if (val.pass === passOld) {
199 |
200 | // 更新成新密码
201 | user.update({
202 | name: userName,
203 | }, {
204 | $set: {
205 | pass: passNew,
206 | },
207 | }, (err) => {
208 | if (err) {
209 | res.send({
210 | msgCode:500,
211 | msgCtx: err,
212 | })
213 | }
214 | else {
215 | res.send({
216 | msgCode:200,
217 | msgCtx: 'Pass is changed.',
218 | })
219 | }
220 | })
221 | }
222 | else {
223 | res.send({
224 | msgCode:304,
225 | msgCtx: 'Old password is incorrect.',
226 | })
227 | }
228 | })
229 | })
230 | })
231 |
232 | // 修改个人信息
233 | router.put('/api/user/info', (req, res, next) => {
234 |
235 | const token = req.query.token
236 | jwtDec(token).then((val) => {
237 | // 用户资料别名
238 | const userName = val.user
239 | const userGender = req.body.gender
240 | const userImg = req.body.img
241 | const userCity = req.body.city
242 | const userHobbies = req.body.hobbies.split(',')
243 |
244 | // 查询当前用户的账号
245 | info.findOne({
246 | user: userName,
247 | }, (err, val) => {
248 |
249 | // 更新资料
250 | info.update({
251 | user: userName,
252 | }, {$set: {
253 | gender: userGender,
254 | img: userImg,
255 | city: userCity,
256 | hobbies: userHobbies,
257 | }}, (err) => {
258 |
259 | // 提示错误信息
260 | if (err) {
261 | res.send({
262 | msgCode:500,
263 | msgCtx: err,
264 | })
265 | }
266 |
267 | // 提示成功
268 | else {
269 | console.log(userGender)
270 | console.log(userImg)
271 | console.log(userCity)
272 | console.log(userHobbies)
273 | res.send({
274 | msgCode:200,
275 | msgCtx: 'User info is changed.',
276 | })
277 | }
278 | })
279 | })
280 | })
281 | })
282 |
283 | module.exports = router
284 |
--------------------------------------------------------------------------------
/public/src/css/room.less:
--------------------------------------------------------------------------------
1 | // 消息气泡
2 |
3 | @chatBubbleHeadWidth: 0.4rem;
4 | @chatBubbleCtxWidth: 4.3rem;
5 |
6 | // 工具栏
7 |
8 | @chatToolWidth: 0.4rem;
9 | @chatToolHeight: 0.4rem;
10 |
11 | // 发送框
12 |
13 | @chatSendBoxWidth: 6.5rem;
14 | @chatSendBoxHeight: 0.8rem;
15 |
16 | // 发送按钮
17 |
18 | @chatSendButtonWidth: 1rem;
19 |
20 | // 布局宽度
21 |
22 | @chatMoreBoxWidth: 5.6rem;
23 | @chatMoreBoxChildWidth: @chatMoreBoxWidth / 14;
24 |
25 | // 布局高度
26 |
27 | @chatNameHeight: 1rem;
28 | @chatMainHeight: 12.34rem;
29 | @chatCtxHeight: 11.14rem;
30 |
31 | .chat-name {
32 | height: @chatNameHeight;
33 | line-height: @chatNameHeight;
34 | background-color: @colorMain;
35 | color: white;
36 | text-align: center;
37 | }
38 | .chat {
39 | width: @baseWidth;
40 | margin: auto;
41 | position: relative;
42 | font-size: @fontTitle;
43 | line-height: @fontTitle;
44 | }
45 | .chat-ctx {
46 | height: @chatCtxHeight;
47 | overflow-y: scroll;
48 | }
49 | .chat-ctx::-webkit-scrollbar {
50 | width: 0.12rem;
51 | }
52 | .chat-ctx::-webkit-scrollbar-thumb {
53 | border-radius: 0.06rem;
54 | background-color: gray;
55 | }
56 |
57 | .room-link {
58 | display: block;
59 | padding: 0.05rem;
60 | color: black;
61 | }
62 | .room-link:hover {
63 | background-color: rgb(33,150,243);
64 | }
65 | .text-center {
66 | text-align: center;
67 | }
68 | /* fork按钮 */
69 | .fork-me-wrap {
70 | display: block;
71 | position: absolute;
72 | right: 0;
73 | top: 0;
74 | width: @forkMeButtomWidth;
75 | height: @forkMeButtomHeight;
76 | z-index: 999;
77 | }
78 | .fork-me {
79 | width: @forkMeButtomWidth;
80 | height: @forkMeButtomHeight;
81 | }
82 | /* 用户列表 */
83 | /* 用户列表的用户头像 && 保存用户名界面的用户头像 */
84 | .user-img {
85 | width: 0.4rem;
86 | height: 0.4rem;
87 | border-radius: 0.2rem;
88 | float: left;
89 | }
90 | .user-img-choose {
91 | width: 0.8rem;
92 | height: 0.8rem;
93 | border-radius: 0.4rem;
94 | }
95 | .user-name {
96 | padding-left: 0.1rem;
97 | display: inline-block;
98 | height: 0.4rem;
99 | line-height: 0.4rem;
100 | overflow: hidden;
101 | }
102 | .info-title {
103 | height: 0.3rem;
104 | line-height: 0.3rem;
105 | }
106 | .info-user {
107 | text-align: center;
108 | }
109 | #user-reg {
110 | padding: 0.1rem 0.2rem;
111 | }
112 | .chat-info-confirm {
113 | background-color: rgb(68,138,255);
114 | padding: 0.05rem;
115 | cursor: pointer;
116 | }
117 | #user-reg, .chat-info-confirm, #user-reg-tip {
118 | width: 80%;
119 | margin: 0.05rem auto 0 auto;
120 | }
121 | .user-list-wrap {
122 | width: 1.81rem;
123 | height: 4rem;
124 | border: 0.01rem solid white;
125 | float: left;
126 | border-left: 0;
127 | overflow: hidden;
128 | }
129 | .user-lists {
130 | padding: 0 0.2rem;
131 | height: 1.8rem;
132 | overflow-y: scroll;
133 | }
134 | /*.room-list {
135 | padding: 0.05rem 0.1rem;
136 | height: 3.48rem;
137 | overflow-y: scroll;
138 | }*/
139 | .chat-ctx-wrap {
140 | overflow: hidden;
141 | }
142 | .chat-ctrl {
143 | position: fixed;
144 | left: 0;
145 | bottom: 0;
146 | height: @chatToolWidth + @chatSendBoxHeight;
147 | font-size: 0.3rem;
148 | }
149 | .chat-more {
150 | height: @chatToolWidth;
151 | line-height: @chatToolHeight;
152 | border-bottom: 0.01rem solid white;
153 | }
154 | .chat-more > li {
155 | float: left;
156 | width: 0.4rem;
157 | text-align: center;
158 | cursor: pointer;
159 | position: relative;
160 | }
161 | .chat-more-box {
162 | position: absolute;
163 | left: 0;
164 | top: -0.8rem;
165 | width: @chatMoreBoxWidth;
166 | height: 0.8rem;
167 | text-align: left;
168 | visibility: hidden;
169 | background-color: #EFEFEF;
170 | }
171 | .chat-more-box > li {
172 | width: @chatMoreBoxChildWidth;
173 | text-align: center;
174 | float: left;
175 | }
176 | .chat-msg-list > li {
177 | word-break:break-all;
178 | }
179 | .chat-msg-send {
180 | overflow-y: auto;
181 | width: @chatSendBoxWidth;
182 | height: @chatSendBoxHeight;
183 | float: left;
184 | padding: 0.1rem 0.2rem;
185 | outline: none;
186 | color: black;
187 | resize: none;
188 | font-size: 16px;
189 | border: 0;
190 | }
191 | .chat-send-btn {
192 | float: left;
193 | width: @chatSendButtonWidth;
194 | height: @chatSendBoxHeight;
195 | overflow: hidden;
196 | text-align: center;
197 | line-height: @chatSendBoxHeight;
198 | cursor: pointer;
199 | background-color: @colorMain;
200 | color: white;
201 | }
202 | .info-tab-cut {
203 | overflow: hidden;
204 | }
205 | .info-tab {
206 | transition: all .7s;
207 | width: 1.8rem;
208 | height: 4rem;
209 | overflow: hidden;
210 | }
211 | .info-tab-cut,.info-user,.info-menber,.info-room {
212 | width: 1.8rem;
213 | }
214 | .info-room {
215 | position: absolute;
216 | left: -1.8rem;
217 | top: 0.3rem;
218 | border: 0.01rem solid white;
219 | border-right: 0;
220 | height: 4rem;
221 | padding: 0.1rem;
222 | }
223 | .info-user {
224 | height: 1.9rem;
225 | }
226 | .info-menber {
227 | height: 2.1rem;
228 | }
229 | .info-tab > li {
230 | float: left;
231 | }
232 | /* 泡泡组件 */
233 | .bubble {
234 | color: black;
235 | overflow: hidden;
236 | padding: 0.1rem;
237 | }
238 | /* 左边泡泡 */
239 | .bubble-left {
240 | // background-color: #03a9f4;
241 | }
242 | /* 右边泡泡 */
243 | .bubble-right {
244 | // background-color: #009688;
245 | }
246 | /* 左边泡泡 头部信息 */
247 | .bubble-left > .bubble-head {
248 | float: left;
249 | }
250 | /* 右边泡泡 头部信息 */
251 | .bubble-right > .bubble-head {
252 | float: right;
253 | margin-top: 0;
254 | }
255 | /* 左边泡泡 内容 */
256 | .bubble-left > .bubble-ctx {
257 | float: left;
258 | margin-left: 0.2rem;
259 | }
260 | .bubble-ctx-show::before {
261 | content: '';
262 | width: 0;
263 | height: 0;
264 | position: absolute;
265 | }
266 | .bubble-ctx-show::after {
267 | content: '';
268 | width: 0;
269 | height: 0;
270 | position: absolute;
271 | }
272 | .bubble-left .bubble-ctx-show::before {
273 | border-top: 0.05rem solid transparent;
274 | border-bottom: 0.05rem solid transparent;
275 | border-right: 0.1rem solid #00bcd4;
276 | left: -0.1rem;
277 | top: 0.1rem;
278 | }
279 | /* 左边泡泡 三角形 */
280 | .bubble-left .bubble-ctx-show::after {
281 | border-top: 0.05rem solid transparent;
282 | border-bottom: 0.05rem solid transparent;
283 | border-right: 0.1rem solid #EFEFEF;
284 | left: -0.09rem;
285 | top: 0.1rem;
286 | }
287 | /* 左边泡泡 内容 */
288 | .bubble-right > .bubble-ctx {
289 | margin-right: 0.2rem;
290 | float: right;
291 | }
292 | /* 右边泡泡 三角形 */
293 | .bubble-right .bubble-ctx-show::before {
294 | border-top: 0.05rem solid transparent;
295 | border-bottom: 0.05rem solid transparent;
296 | border-left: 0.1rem solid #00bcd4;
297 | right: -0.1rem;
298 | top: 0.1rem;
299 | }
300 | .bubble-right .bubble-ctx-show::after {
301 | border-top: 0.05rem solid transparent;
302 | border-bottom: 0.05rem solid transparent;
303 | border-left: 0.1rem solid #EFEFEF;
304 | right: -0.09rem;
305 | top: 0.1rem;
306 | }
307 | /* 全局泡泡 头部 */
308 | .bubble-head {
309 | width: @chatBubbleHeadWidth;
310 | height: 0.4rem;
311 | margin-top: @fontTitle + 0.1;
312 | }
313 | /* 全局泡泡 内容 */
314 | .bubble-ctx {
315 | word-break: break-all;
316 | width: @chatBubbleCtxWidth;
317 | }
318 | .bubble-ctx-border {
319 | border: 0.01rem solid #00bcd4;
320 | border-radius: 0.1rem;
321 | margin: 0;
322 | padding: 0.05rem;
323 | position: relative;
324 | }
325 | .bubble-ctx-show {
326 | margin: 0;
327 | padding: 0.05rem 0.1rem;
328 | font-size: @fontTitle - 0.1;
329 | }
330 | /* 全局泡泡 信息 */
331 | .bubble-info {
332 | overflow: hidden;
333 | padding-bottom: 0.05rem;
334 | }
335 | /* 全局泡泡 信息列表 */
336 | .bubble-left .bubble-info-user {
337 | float: left;
338 | color: #00bcd4;
339 | padding-left: 0.05rem
340 | }
341 | .bubble-info-time {
342 | font-size: @fontTitle / 8;
343 | line-height: @fontTitle;
344 | }
345 | .bubble-left .bubble-info-time {
346 | float: right;
347 | padding-right: 0.16rem;
348 | }
349 | .bubble-right .bubble-info-user {
350 | color: #ffc107;
351 | float: right;
352 | margin-right: 0;
353 | padding-right: 0.05rem
354 | }
355 | .bubble-right .bubble-info-time {
356 | float: left;
357 | padding-left: 0.16rem;
358 | }
--------------------------------------------------------------------------------
/API.md:
--------------------------------------------------------------------------------
1 | ## 目录
2 |
3 | * 用户
4 |
5 | * [用户注册](/API.md#用户注册)
6 | * [用户登陆](/API.md#用户登陆)
7 | * [注销登陆](/API.md#注销登陆)
8 | * [用户资料](/API.md#用户资料)
9 | * [用户列表](/API.md#用户列表)
10 | * [修改密码](/API.md#修改密码)
11 | * [修改资料](/API.md#修改资料)
12 |
13 | * 房间
14 |
15 | * [添加房间](/API.md#添加房间)
16 | * [房间描述](/API.md#房间描述)
17 | * [聊天记录](/API.md#聊天记录)
18 | * [房间列表](/API.md#房间列表)
19 |
20 | ## 开始
21 |
22 | > API 说明
23 |
24 | 在线服务器提供支持CORS的REST API,请合理使用在线API。API的测试数据来自Postman。
25 |
26 | * 在线:http://y.bw2.me:8086
27 | * 本地:http://localhost:8086
28 |
29 | ### API 示例
30 |
31 | > 请求地址
32 |
33 | ```
34 |
35 | GET http://47.93.252.247:8086/api/user/info/admin?token=xxx
36 |
37 | ```
38 |
39 | > 请求参数
40 |
41 | 无
42 |
43 | > 返回结果
44 |
45 | ```
46 | {
47 | "msgCode": 200,
48 | "msgCtx": {
49 | "_id": "596b71a6cbed776ffee90c4c",
50 | "user": "admin",
51 | "gender": "男",
52 | "img": "https://randomuser.me/api/portraits/men/2.jpg",
53 | "city": "北京",
54 | "__v": 0,
55 | "hobbies": [
56 | ""
57 | ]
58 | }
59 | }
60 |
61 | ```
62 |
63 | ### msgCode 通用定义
64 |
65 | 状态码 | 说明
66 | ------|------------
67 | 200 | 操作成功
68 | 304 | 未执行操作
69 | 401 | 未登陆
70 | 403 | 禁止操作
71 | 404 | 未找到信息
72 | 500 | 发生错误
73 |
74 |
75 | ## 用户 API
76 |
77 | ### 用户注册
78 |
79 | * 非登陆状态才能操作
80 |
81 | > 请求地址
82 |
83 | 方法 | 地址
84 | -----|-----
85 | POST | /api/user/register
86 |
87 | > 请求参数
88 |
89 | 参数 | 类型 | 必须 | 描述
90 | -----|------|------|-------
91 | name | String | √ | 昵称
92 | pass | String | √ | 密码
93 |
94 | > 返回结果
95 |
96 | ```
97 |
98 | {
99 | "msgCode": 200,
100 | "msgCtx": "Reg success.",
101 | "token": "xxx"
102 | }
103 |
104 | ```
105 |
106 | > 返回结果(失败)
107 |
108 | ```
109 |
110 | 你当前已经登录
111 | ------------------------
112 | {
113 | "msgCode": 304,
114 | "msgCtx": "You have logined."
115 | }
116 |
117 | ```
118 |
119 | ### 用户登陆
120 |
121 | * 非登陆状态才能操作
122 |
123 | > 请求地址
124 |
125 | 方法 | 地址
126 | -----|-----
127 | POST | /api/user/login
128 |
129 | > 请求参数
130 |
131 | 参数 | 类型 | 必须 | 描述
132 | -----|------|------|-------
133 | name | String | √ | 昵称
134 | pass | String | √ | 密码
135 |
136 | > 返回结果
137 |
138 | ```
139 |
140 | {
141 | "msgCode": 200,
142 | "msgCtx": "Login success.",
143 | "token": "xxx"
144 | }
145 |
146 | ```
147 |
148 | > 返回结果(失败)
149 |
150 | ```
151 |
152 | 用户名不存在
153 | ----------------------
154 | {
155 | "msgCode": 404,
156 | "msgCtx": "User is not exist."
157 | }
158 |
159 | 用户名存在但密码错误
160 | ------------------------------
161 | {
162 | "msgCode": 403,
163 | "msgCtx": "Pass is incorrect."
164 | }
165 |
166 | 当前已登录
167 | ------------------------
168 | {
169 | "msgCode": 304,
170 | "msgCtx": "You have already logined."
171 | }
172 |
173 | ```
174 |
175 | ### 注销登陆
176 |
177 | * 登陆状态才能操作
178 |
179 | > 请求地址
180 |
181 | 方法 | 地址
182 | -----|-----
183 | POST | /api/user/logout
184 |
185 | > 请求参数
186 |
187 | 无
188 |
189 | > 返回结果
190 |
191 | ```
192 |
193 | {
194 | "msgCode": 200,
195 | "msgCtx": "Logout success."
196 | }
197 |
198 | ```
199 |
200 | > 返回结果(失败)
201 |
202 | ```
203 |
204 | 当前未登录,不需要注销
205 | ------------------
206 | {
207 | "msgCode": 304,
208 | "msgCtx": "You have not login."
209 | }
210 |
211 | ------------
212 |
213 | ```
214 |
215 | ### 用户资料
216 |
217 | > 请求地址
218 |
219 | 方法 | 地址
220 | -----|-----
221 | GET | /api/user/info/admin
222 |
223 | > 请求参数
224 |
225 | 无
226 |
227 | > 返回结果
228 |
229 | ```
230 |
231 | {
232 | "msgCode": 200,
233 | "msgCtx": {
234 | "_id": "596b71a6cbed776ffee90c4c",
235 | "user": "admin",
236 | "gender": "男",
237 | "img": "https://randomuser.me/api/portraits/men/2.jpg",
238 | "city": "北京",
239 | "__v": 0,
240 | "hobbies": [
241 | ""
242 | ]
243 | }
244 | }
245 |
246 | ```
247 |
248 | > 返回结果(失败)
249 |
250 | ```
251 |
252 | 用户不存在
253 | -----------
254 | {
255 | "msgCode": 404,
256 | "msgCtx": "User not exist."
257 | }
258 |
259 | ```
260 |
261 | ### 用户列表
262 |
263 | > 请求地址
264 |
265 | 方法 | 地址
266 | -----|-----
267 | GET | /api/user
268 |
269 | > 请求参数
270 |
271 | 无
272 |
273 | > 返回结果
274 |
275 |
276 | ```
277 |
278 | {
279 | "msgCode": 200,
280 | "msgCtx": [
281 | {
282 | "_id": "596b71a6cbed776ffee90c4c",
283 | "user": "admin",
284 | "gender": "男",
285 | "img": "https://randomuser.me/api/portraits/men/2.jpg",
286 | "city": "北京",
287 | "__v": 0,
288 | "hobbies": [
289 | ""
290 | ]
291 | },
292 | ...
293 | ]
294 | }
295 |
296 | ```
297 |
298 | ### 修改密码
299 |
300 | * 登陆状态才能操作
301 |
302 | > 请求地址
303 |
304 | 方法 | 地址
305 | -----|-----
306 | PUT | /api/user/pass
307 |
308 | > 请求参数
309 |
310 | 参数 | 类型 | 必须 | 描述
311 | -----|------|------|-------
312 | passOld | String | √ | 旧密码
313 | passNew | String | √ | 新密码
314 |
315 | > 返回结果
316 |
317 | ```
318 |
319 | {
320 | "msgCode": 200,
321 | "msgCtx": "Pass is changed."
322 | }
323 |
324 | ```
325 |
326 | > 返回结果(失败)
327 |
328 | ```
329 |
330 | 未登录无权限访问当前API
331 | -----------------------
332 | {
333 | "msgCode": 401,
334 | "msgCtx": "Please login."
335 | }
336 |
337 | 旧密码错误
338 | --------------------
339 | {
340 | "msgCode": 304,
341 | "msgCtx": "Old password is incorrect."
342 | }
343 |
344 | ```
345 |
346 | ### 修改资料
347 |
348 | * 登陆状态才能操作
349 |
350 | > 请求地址
351 |
352 | 方法 | 地址
353 | -----|-----
354 | PUT | /api/user/info
355 |
356 | > 请求参数
357 |
358 | 参数 | 类型 | 必须 | 描述
359 | -----|------|------|-------
360 | gender | String | √ | 性别
361 | img | String | √ | 头像
362 | city | String | √ | 城市
363 | hobbies | String | √ | 爱好
364 |
365 | > 返回结果
366 |
367 | ```
368 |
369 | {
370 | "msgCode": 200,
371 | "msgCtx": "User info is changed."
372 | }
373 |
374 | ```
375 |
376 | > 返回结果(失败)
377 |
378 | ```
379 |
380 | 未登录无权限访问当前API
381 | -----------------------
382 | {
383 | "msgCode": 401,
384 | "msgCtx": "Please login."
385 | }
386 |
387 | ```
388 |
389 | ## 房间 API
390 |
391 | ### 添加房间
392 |
393 | * 登陆状态才能操作
394 |
395 | > 请求地址
396 |
397 | 方法 | 地址
398 | -----|-----
399 | POST | /api/room/add
400 |
401 | > 请求参数
402 |
403 | 参数 | 类型 | 必须 | 描述
404 | -----|------|------|-------
405 | name | String | √ | 房间名
406 | desc | String | √ | 房间描述
407 |
408 | > 返回结果
409 |
410 | ```
411 |
412 | {
413 | "msgCode": 200,
414 | "msgCtx": "Room add success."
415 | }
416 |
417 | ```
418 |
419 | > 返回结果(失败)
420 |
421 | ```
422 |
423 | 未登录无权限访问当前API
424 | -----------------------
425 | {
426 | "msgCode": 401,
427 | "msgCtx": "You cannot access the api. Please login."
428 | }
429 |
430 | ```
431 |
432 | ### 房间描述
433 |
434 | > 请求地址
435 |
436 | 方法 | 地址
437 | -----|-----
438 | GET | /api/room/info/:id
439 |
440 | > 请求参数
441 |
442 | 无
443 |
444 | > 返回结果
445 |
446 | ```
447 |
448 | {
449 | "msgCode": 200,
450 | "msgCtx": {
451 | "_id": "596cece4531efc30af8da9ca",
452 | "name": "Test",
453 | "desc": "Test for anything.",
454 | "__v": 0
455 | }
456 | }
457 |
458 | ```
459 |
460 | > 返回结果(失败)
461 |
462 | ```
463 |
464 | {
465 | "msgCode": 404,
466 | "msgCtx": "Room is not exist."
467 | }
468 |
469 | ```
470 |
471 | ### 聊天记录
472 |
473 | > 请求地址
474 |
475 | 方法 | 地址
476 | -----|-----
477 | GET | /api/room/mess/:id
478 |
479 | > 请求参数
480 |
481 | 无
482 |
483 | > 请求结果
484 |
485 | ```
486 | {
487 | "msgCode": 200,
488 | "msgCtx": [
489 | {
490 | "_id": "596cecfe531efc30af8da9cb",
491 | "room": "Test",
492 | "user": "admin",
493 | "mess": "hello",
494 | "time": 1500310756,
495 | "img": "https://randomuser.me/api/portraits/men/2.jpg",
496 | "__v": 0
497 | },
498 | {
499 | "_id": "596ced01531efc30af8da9cc",
500 | "room": "Test",
501 | "user": "admin",
502 | "mess": "world",
503 | "time": 1500310760,
504 | "img": "https://randomuser.me/api/portraits/men/2.jpg",
505 | "__v": 0
506 | }
507 | ]
508 | }
509 |
510 | ```
511 |
512 | > 请求结果(失败)
513 |
514 | ```
515 |
516 | 当前房间不存在
517 | ---------
518 | {
519 | "msgCode": 404,
520 | "msgCtx": "This room is not exist."
521 | }
522 |
523 | ```
524 |
525 | ### 房间列表
526 |
527 | > 请求地址
528 |
529 | 方法 | 地址
530 | -----|-----
531 | GET | /api/room
532 |
533 | > 请求参数
534 |
535 | 无
536 |
537 | > 返回结果
538 |
539 | ```
540 |
541 | {
542 | "msgCode": 200,
543 | "msgCtx": [
544 | {
545 | "_id": "596b7224cbed776ffee90c4d",
546 | "name": "小美",
547 | "desc": "我是小美,今年14岁,一起唠嗑吧",
548 | "__v": 0
549 | },
550 | {
551 | "_id": "596b723acbed776ffee90c4e",
552 | "name": "center",
553 | "desc": "中心聊天室",
554 | "__v": 0
555 | },
556 | ...
557 | ]
558 | }
559 |
560 | ```
561 |
--------------------------------------------------------------------------------
/public/src/js/index.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | const chatMsgSend = document.getElementsByClassName('chat-msg-send')[0]
3 | const chatMsgSendBtn = document.getElementsByClassName('chat-send-btn')[0]
4 | const chatEmojiList = document.getElementsByClassName('chat-emoji-list')[0]
5 | const infoTab = document.getElementsByClassName('info-tab')[0]
6 | const chatMsgList = document.getElementsByClassName('chat-msg-list')[0]
7 | const chatMoreBox = document.getElementsByClassName('chat-more-box')[0]
8 | const topTitleDOM = document.getElementsByClassName('top-title')[0]
9 | const chatDOM = document.getElementsByClassName('chat-ctx')[0]
10 | const chatCtrl = document.getElementsByClassName('chat-ctrl')[0]
11 |
12 | // 为socket.io设置别名
13 | const socketHostName = document.location.hostname
14 | const socketURI = `//${socketHostName}:9998/`
15 | const socket = io(socketURI)
16 |
17 | // 把聊天室所有的操作封装在命名空间内
18 | const nChat = {}
19 |
20 | // 数据(存放变量)
21 | nChat.data = {
22 | // TODO: set a default img
23 | isRoomInit: false,
24 | messIsFirst: true,
25 | messIsFoucs: false,
26 | isInitInsertEmoji: false,
27 | onlineUserCount: 0,
28 | onlineUserList: [],
29 | onlineUserListImg: [],
30 | defaultUserImg: 'https://randomuser.me/api/portraits/women/50.jpg',
31 | welcomeInfo: '系统: 欢迎来到 ',
32 | // 房间ID
33 | currentRoomName: null,
34 | // 用户资料
35 | user: {
36 | name: null,
37 | pass: null,
38 | desc: null,
39 | img: 'https://randomuser.me/api/portraits/men/1.jpg',
40 | sex: 'men'
41 | },
42 | robot: {
43 | api: `${document.location.origin}/api/robot/openapi/api`,
44 | key: '57a5b6849e2b4d47ae0badadf849c261',
45 | nick: '小美',
46 | img: 'https://randomuser.me/api/portraits/women/60.jpg'
47 | }
48 | }
49 | // 房间(socket通讯)
50 | nChat.room = {
51 | // 初始化
52 | init () {
53 | socket.on('room id req', (msg) => {
54 | nChat.data.user.name = msg.name
55 | nChat.data.user.img = msg.img
56 | // 把当前房间id返回给后台
57 | socket.emit('room id res', nChat.data.currentRoomName)
58 | // 为当前房间发送欢迎消息
59 | nChat.method.insertToList(chatMsgList, 'li', `${nChat.data.welcomeInfo} ${nChat.data.currentRoomName}`)
60 | // 初始化输入框内容为空
61 | chatMsgSend.value = ''
62 | chatMsgSend.focus()
63 | // 初始化表情框为不可见
64 | chatMoreBox.style.visibility = 'hidden'
65 | // 监听输入框点击事件
66 | chatMsgSend.onclick = () => {
67 | // 隐藏表情框
68 | chatMoreBox.style.visibility = 'hidden'
69 | nChat.data.messIsFoucs = true
70 | }
71 | })
72 |
73 | socket.on('user login req', (data) => {
74 | nChat.method.insertToList(chatMsgList, 'li', data)
75 | })
76 | socket.on('user logout req', (data) => {
77 | console.log(data)
78 | // 发送用户离开通知
79 | nChat.method.insertToList(chatMsgList, 'li', `${data.currentUser} 离开了房间`)
80 | // 滚动到最新消息
81 | nChat.method.scrollToBottom()
82 | })
83 | // 读取当前房间的聊天信息
84 | socket.on('mess show res', (data) => {
85 | console.log(data)
86 | const len = data.length
87 | for(let i = len - 1; i >= 0; i--){
88 | const leftBubble = nChat.method.renderBubbleMsg('left', data[i].user, nChat.method.parseTime(data[i].time), nChat.method.parseMsgVal(data[i].mess), data[i].img)
89 | nChat.method.insertToList(chatMsgList, 'li', leftBubble)
90 | }
91 | nChat.method.scrollToBottom()
92 | })
93 | },
94 | // 渲染
95 | render () {
96 | socket.on('current status', (data) => {
97 | console.log(data)
98 | })
99 | // 把最新的消息添加进DOM
100 | socket.on('send message res', (data) => {
101 | const time = nChat.method.parseTime(data.time)
102 | const leftBubble = nChat.method.renderBubbleMsg('left', data.user, time, nChat.method.parseMsgVal(data.msg), data.img)
103 | nChat.method.insertToList(chatMsgList, 'li', leftBubble)
104 | const len = data.length
105 | console.log(`total message / ${len}`)
106 | // 滚动到最新消息
107 | nChat.method.scrollToBottom()
108 | })
109 | }
110 | }
111 | // 方法(存放函数)
112 | nChat.method = {
113 | // 获取房间ID
114 | getCurrentRoomName () {
115 | const pathName = document.location.pathname
116 | const isHome = pathName === '/'
117 | let roomId = null
118 | if (!isHome && (pathName.indexOf('room') !== -1)) {
119 | roomId = pathName.replace(/\/.*?\//,'')
120 | }
121 | return roomId === null ? roomId = 'Chat Room' : decodeURIComponent(roomId)
122 | },
123 | // 清空节点内容
124 | initList (node) {
125 | node['innerHTML'] = ''
126 | },
127 | // 渲染列表
128 | renderList (parentNode, childArr, template) {
129 | // 设置父节点别名
130 | const type = {
131 | chat: chatMsgList,
132 | emoji: chatMoreBox
133 | }
134 | // 逐个渲染
135 | for(let i = 0; i < childArr.length; i++){
136 | this.insertToList(type[parentNode], 'li', childArr[i])
137 | }
138 | },
139 | // 插入值到节点
140 | insertToList (parentDOM, childType, childCtx) {
141 | const childDOM = document.createElement(childType)
142 | childDOM.innerHTML = childCtx
143 | parentDOM.appendChild(childDOM)
144 | },
145 | renderUserList (userImg, userName) {
146 | const ctx = `
147 | ${userName}`
148 | return ctx
149 | },
150 | // 左右泡泡组件模板
151 | renderBubbleMsg (type, user, time, msg, img) {
152 | user = nChat.method.parseMsgVal(user)
153 | time = nChat.method.parseMsgVal(time)
154 | msg = nChat.method.parseMsgVal(msg)
155 | let bubbleInfoEl = ''
156 | if (time !== '') {
157 | bubbleInfoEl = `
158 |
159 | - ${user}
160 | - ${time}
161 |
`
162 | }
163 | else {
164 | bubbleInfoEl = ''
165 | }
166 | if (typeof img === 'undefined' || img === null) {
167 | img = nChat.data.defaultUserImg
168 | img = nChat.method.parseMsgVal(img)
169 | }
170 | img = nChat.method.parseMsgVal(img)
171 | img = img.replace(/on[a-zA-z]+=/, '')
172 | const ctx = `
173 |
174 |

175 |
176 |
177 | ${bubbleInfoEl}
178 |
181 |
182 |
`
183 | return ctx
184 | },
185 | parseMsgVal (v) {
186 | let val = v.replace(//g,'>')
188 | val = val.replace(/"/g,'\"')
189 | val = val.replace(/'/g,'\'')
190 | return val
191 | },
192 | // 获取时间戳
193 | getTime (t) {
194 | return Date.parse(t) / 1000
195 | },
196 | // 解析时间戳
197 | parseTime (t) {
198 | const tm = new Date()
199 | tm.setTime(t * 1000)
200 | return tm.toLocaleString()
201 | },
202 | // 发送消息
203 | sendMessage () {
204 | if (chatMsgSend.value !== '') {
205 | // 隐藏表情框
206 | chatMoreBox.style.visibility = 'hidden'
207 | // 获取当前时间戳
208 | const time = nChat.method.getTime(new Date())
209 | // const timeShow = nChat.method.parseTime(time)
210 | const name = nChat.data.user.name !== null ? nChat.data.user.name : '神秘人'
211 | const parsedMessage = nChat.method.parseMsgVal(chatMsgSend.value)
212 | // 添加内容到当前界面
213 | const rightBubble = nChat.method.renderBubbleMsg('right', name, '', parsedMessage, nChat.data.user.img)
214 | // 添加内容到当前房间的其他用户界面
215 | socket.emit('send message req', time, nChat.data.currentRoomName , {
216 | time: time,
217 | msg: parsedMessage,
218 | })
219 | nChat.method.insertToList(chatMsgList, 'li', rightBubble)
220 | // 发送完消息清空内容
221 | chatMsgSend.value = ''
222 | // 发送完消息重新把焦点放置在输入框
223 | chatMsgSend.focus()
224 | // 滚动到最新消息
225 | nChat.method.scrollToBottom()
226 | if (nChat.data.currentRoomName === '小美') {
227 | // 调用图灵机器人
228 | // TODO: post not work
229 | axios.get(nChat.data.robot.api, {
230 | params: {
231 | key: nChat.data.robot.key,
232 | info: parsedMessage
233 | }
234 | }).then((res) => {
235 | const tm = nChat.method.getTime(new Date())
236 | const tmParsed = nChat.method.parseTime(tm)
237 | const leftBubble = nChat.method.renderBubbleMsg('left', nChat.data.robot.nick, tmParsed, res.data.text, nChat.data.robot.img)
238 | nChat.method.insertToList(chatMsgList, 'li', leftBubble)
239 | socket.emit('send message req', time, nChat.data.currentRoomName , {
240 | user: nChat.data.robot.nick,
241 | time: tm,
242 | msg: res.data.text,
243 | img: nChat.data.robot.img
244 | })
245 | nChat.method.scrollToBottom()
246 | }).catch((err) => console.log(err))
247 | }
248 | }
249 | },
250 | // 获取在线列表
251 | getOnlineList (arr, type) {
252 | arr.filter((val) => {
253 | if (val.name === type) {
254 | const newArr = val.user.concat()
255 | const newImg = val.img.concat()
256 | nChat.data.onlineUserCount = val.user.length
257 | nChat.data.onlineUserList = newArr
258 | nChat.data.onlineUserListImg = newImg
259 | }
260 | })
261 | },
262 | // 获取随机图片
263 | getRandomImg (gender) {
264 | // example / https://randomuser.me/api/portraits/men/100.jpg
265 | const randomNumber = parseInt(Math.random() * 100)
266 | return `https://randomuser.me/api/portraits/${gender}/${randomNumber}.jpg`
267 | },
268 | // 获取随机昵称
269 | getRandomNick (region,gender) {
270 | // example / https://uinames.com/api/?region=china&gender=female&amount=1
271 | return `https://uinames.com/api/?region=${region}&gender=${gender}&amount=1`
272 | },
273 | // 渲染表情包
274 | getEmoji (node) {
275 | const emojiList = ['😅', '😂', '🙂', '🙃', '😉', '😘', '😗', '😜', '😎', '😏', '😔', '🙁', '😶', '😢', '🤔', '👏', '🤝', '👍', '👎', '✌', '❤', '🐶', '🐱', '🐰', '🐭', '🐷', '🐸', '🙈',]
276 | const nodeName = node || chatMoreBox
277 | this.initList(nodeName)
278 | nodeName.style.visibility === 'hidden' ? chatMoreBox.style.visibility = 'visible' : chatMoreBox.style.visibility = 'hidden'
279 | this.renderList('emoji', emojiList)
280 | // 只初始化一次事件监听
281 | nChat.data.isInitInsertEmoji === false ? this.initInsertEmoji() : ''
282 | },
283 | // 用事件代理监听所有的标签添加事件
284 | initInsertEmoji () {
285 | chatMoreBox.addEventListener('click', (e) => {
286 | // 如果当前值的目标标签的小写字母是select
287 | if (e.target.tagName.toLowerCase() === 'li') {
288 | // 则显示监听到的值
289 | nChat.method.insertEmojiToText(e.target.innerText)
290 | }
291 | },false)
292 | // 设置事件监听初始化状态为真
293 | nChat.data.isInitInsertEmoji = true
294 | },
295 | // 插入表情包
296 | insertEmojiToText (type) {
297 | const messVal = chatMsgSend.value // 表单值
298 | let index = chatMsgSend.selectionStart // 光标位置
299 | // 如果当前为第一次并且没有点击过输入框
300 | // 则把索引改为最后
301 | nChat.data.messIsFirst && (!nChat.data.messIsFoucs) ? index = messVal.length : ''
302 | // 执行完第一次则把是否是第一次的状态改为false
303 | nChat.data.messIsFirst = false
304 | // 首部插入
305 | if (messVal === '') {
306 | chatMsgSend.value = type
307 | }
308 | // 尾部插入
309 | else if (messVal.length === index) {
310 | chatMsgSend.value = chatMsgSend.value + type
311 | }
312 | // 中间插入
313 | else {
314 | chatMsgSend.value = messVal.slice(0,index) + type + messVal.slice(index,messVal.length)
315 | }
316 | chatMsgSend.focus()
317 | },
318 | // 滚动到最新消息
319 | scrollToBottom () {
320 | const div = document.getElementsByClassName("chat-ctx")[0];
321 | div.scrollTop = div.scrollHeight;
322 | }
323 | }
324 |
325 | // 通过计算获取聊天框的合适高度
326 | function changeChatHeight() {
327 | const documentHeight = document.documentElement.clientHeight
328 | const topTitleDOMHeight = topTitleDOM.offsetHeight
329 | const chatCtrlHeight = chatCtrl.offsetHeight
330 | const chatDOMHeight = documentHeight - topTitleDOMHeight - chatCtrlHeight
331 | chatDOM.style.height = `${chatDOMHeight}px`
332 | }
333 |
334 | // 视窗改变时重新计算高度
335 | window.addEventListener('resize', () => changeChatHeight(), false)
336 |
337 | document.body.onload = () => {
338 |
339 | // 页面加载完成后改变高度
340 | changeChatHeight()
341 |
342 | // 页面加载完成后,初始化房间名字
343 | // 发送消息的时候会把当前房间的名字发送过去
344 | nChat.data.currentRoomName = nChat.method.getCurrentRoomName()
345 | // 初始化
346 | // 为当前房间分配ID
347 | nChat.room.init()
348 | // 渲染
349 | nChat.room.render()
350 | // 测试随机图片
351 | console.log(nChat.method.getRandomImg('men'))
352 | // 测试随机昵称
353 | console.log(nChat.method.getRandomNick('china','male'))
354 | chatMsgSendBtn.addEventListener('click',() => nChat.method.sendMessage(), false)
355 | chatEmojiList.addEventListener('click', () => nChat.method.getEmoji(), false)
356 | }
357 | })();
358 |
--------------------------------------------------------------------------------