├── 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 |
2 | 4 |
-------------------------------------------------------------------------------- /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 |
2 |
3 |
4 |
    5 |
    6 |
    7 |
    -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /views/room/roomList.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/common/infoTop.ejs: -------------------------------------------------------------------------------- 1 |
    2 | <% if (typeof prevButton !== 'undefined') { %> 3 | 4 | ><%= prevButton.name %> 5 | 6 | <% } %> 7 | <% if (typeof nextButton !== 'undefined') { %> 8 | 9 | ><%= nextButton.name %> 10 | 11 | <% } %> 12 |

    <%= infoTopTitle %>

    13 |
    14 | -------------------------------------------------------------------------------- /views/user/userInfo.ejs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | > 4 |
    5 | 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 | ![nodejs-chat-nick-chat](http://atmp.oss-cn-qingdao.aliyuncs.com/img/nodejs-chat-nick-chat.gif) 6 | 7 | > 用户聊天 8 | 9 | ![nodejs-chat-user-chat](http://atmp.oss-cn-qingdao.aliyuncs.com/img/nodejs-chat-user-chat.gif) 10 | 11 | > 成员&房间 12 | 13 | ![nodejs-chat-memb-room](http://atmp.oss-cn-qingdao.aliyuncs.com/img/nodejs-chat-memb-room.gif) 14 | 15 | > 离线通知 16 | 17 | ![nodejs-chat-user-gone](http://atmp.oss-cn-qingdao.aliyuncs.com/img/nodejs-chat-user-gone.gif) 18 | 19 | > 更多房间 20 | 21 | ![nodejs-chat-more-rooms](http://atmp.oss-cn-qingdao.aliyuncs.com/img/nodejs-chat-more-rooms.gif) 22 | 23 | > 房间独立 24 | 25 | ![nodejs-chat-room-diff](http://atmp.oss-cn-qingdao.aliyuncs.com/img/nodejs-chat-room-diff.gif) -------------------------------------------------------------------------------- /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 | 8 |

    这是一个全栈式开发的应用

    9 | 14 |

    未来的版本

    15 | 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 |
    3 | 6 |
    7 |
    8 | 11 | 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 | ![NChat-qrcode](http://nchat.oss-cn-beijing.aliyuncs.com/img/NChat-qrcode3.1.png) 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 | ` 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 |
    179 |

    ${msg}

    180 |
    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 | --------------------------------------------------------------------------------