项目开始是由于工做须要一个聊天室功能,可是由于某些缘由最终选用的是基于xmpp协议的Strophe.js写的。因而就想用node本身写一套,原本只是想简单的写个聊天页面,可是写完了又不满意,因此不断的重构(彷佛能够理解产品经理为何总是改需求了๑乛◡乛๑)。javascript
不少东西,好比mongodb,我也是第一次用,之前只接触过mysql。因此都是一边学一边写,利用工做之余的时间,断断续续的写了几个月,包含了一整套的先后端交互。uI是按照本身的感受来的,没有设计天分(话说主题切换到如今还只有一套主题,实在是很差设计啊~),轻喷---。项目还有不少须要优化完善的地方,欢迎你们提到issues(文末有q群,欢迎一块儿学习交流)。html
闲话少说,本文主要讲项目的设计流程,以及部分功能实现思路。对项目感兴趣的同窗请移步源码 Vchat — 从头到脚,撸一个在线聊天的web应用(vue + node + mongodb)。前端
这是分隔线---------------------------------------深夜码字,最近真冷vue
前端主要采用了vue全家桶,没什么多说的,脚手架构建项目,vuex状态管理,vue-router控制路由,axios进行先后端交互。后端是基于node搭的服务,用的是express。我为何不用koa呢,纯粹是图方便,由于koa不熟(捂脸)。聊天最重要的固然是通讯,项目用socket.io来进行先后端通讯。java
数据库是mongoDB,主要有用户、好友、群聊、消息、表情、号码池等。node
Vchat中用户注册时,会随机指定一个code号码,而这个code号是从预先生成的一个号码池(号码池存在mongodb)中取的。初始指定10000001-10001999的号码段为用户code, 100001-100999的号码段为群聊code。用户能够凭借code号或者帐号登陆。mysql
// 号码池设计 * code 号码 * status 1 已使用 0 未使用 * type 1 用户 2 群聊 * random 随机数索引,用于随机查找某一条 // user表主要字段 * name 帐号 * pass 密码 * avatar 头像 * signature 个性签名 * nickname 昵称 * email 邮件 * phone 手机 * sex 性别 * bubble 气泡 * projectTheme 项目主题 * wallpaper 聊天壁纸 * signUpTime 注册时间 * lastLoginTime 最后一次登陆时间 * chatColor 聊天文字颜色 * province 省 * city 市 * town 县 * conversationsList 会话列表 * cover 封面列表 复制代码
注册时,须要判断帐号是否已存在,以及随机取得的code须要在号码池中标记为已被使用,用户密码用md5加密等。ios
// md5 密码加密 const md5 = pass => { // 避免屡次调用MD5报错 let md5 = crypto.createHash('md5'); return md5.update(pass).digest("hex"); }; 复制代码
登陆一样须要判断用户是否已注册,以及支持帐号和code两种方式登陆。git
const login = (params, callback) => { // 登陆 baseList.users .find({ // mongodb中能够直接用$or表示或关系 $or: [{"name": params.name}, {"code": params.name}] }) .then(r => { if (r.length) { let pass = md5(params.pass); if (r[0]['pass'] === pass) { //更新最后一次登陆时间 此处直接写Date.now 会报错 须要Date.now()!!!; baseList.users.update({name: params.name}, {lastLoginTime: Date.now()}).then(raw => { console.log(raw); }); callback({code: 0, data: {name: r[0].name, photo: r[0].photo}}); } else { callback({code: -1}); } } else { callback({code: -1}); } }) }; 复制代码
app.use('/v*', (req, res, next) => { if (req.session.login) { next(); } else { if (req.originalUrl === '/v/user/login' || req.originalUrl === '/v/user/signUp') { next(); } else { res.json({ status: 0 }); } } }); 复制代码
// http response 服务器响应拦截器,这里拦截未登陆和401错误,并从新跳入登页从新获取token instance.interceptors.response.use( response => { // 拦截未登陆 if (response.data.status === 0) { router.replace('/'); } return response; }, error => { if (error.response) { switch (error.response.status) { case 401: // 这里写清除token的代码 router.replace('/'); } } return Promise.reject(error.response.data) }); 复制代码
vchat中,消息种类包括好友或者加群申请、回复申请(赞成or拒绝)、入群通知、聊天消息(文字、图片、表情、文件)github
在实现消息发送以前,须要大致的了解一些socket.io
的api。详细api文档能够查看socket.io
// 全部的消息请求都是创建在已链接的基础上的 io.on('connect', onConnect); // 发送给当前客户端 socket.emit('hello', 'can you hear me?', 1, 2, 'abc'); // 发送给全部客户端,除了发送者 socket.broadcast.emit('broadcast', 'hello friends!'); // 发送给同在 'game' 房间的全部客户端,除了发送者 socket.to('game').emit('nice game', "let's play a game"); // 发送给同在 'game' 房间的全部客户端,包括发送者 io.in('game').emit('big-announcement', 'the game will start soon'); 复制代码
加入会话列表中的房间,会话列表在好友申请成功或者加群成功时会自动添加。可是你也能够手动移除或添加,移除后将不会再收到被移除会话的消息(相似于屏蔽)。
// 前端 发起加入房间的请求 this.conversationsList.forEach(v => { let val = { name: this.user.name, time: utils.formatTime(new Date()), avatar: this.user.photo, roomid: v.id }; this.$socket.emit('join', val); }); // 后端 接受请求后执行加入操做,记录每一个房间加入的成员,以及回信告知指定房间已上线成员 socket.on('join', (val) => { socket.join(val.roomid, () => { if (OnlineUser[val.name]) { return; } OnlineUser[val.name] = socket.id; io.in(val.roomid).emit('joined', OnlineUser); // 包括发送者 }); }); 复制代码
mes(r) { // 只有本房间的消息才展现 if (r.roomid === this.currSation.id) { this.chatList.push(Object.assign({}, r, {type: 'other'})); } } 复制代码
// 前端 send(params, type = 'mess') { // 发送消息 if (!this.message && !params) { return; } let val = { name: this.user.name, mes: this.message, time: utils.formatTime(new Date()), avatar: this.user.photo, nickname: this.user.nickname, read: [this.user.name], roomid: this.currSation.id, style: 'mess', userM: this.user.id }; this.chatList.push(Object.assign({},val,{type: 'mine'})); // 更新视图 this.$socket.emit('mes', val); this.message = ''; } // 后端 接收消息后存储到数据库,并转发给房间内其余成员,不包括发送者。 socket.on('mes', (val) => { // 聊天消息 apiList.saveMessage(val); socket.to(val.roomid).emit('mes', val); }); 复制代码
// 前端 获取指定房间的历史消息 this.$socket.emit('getHistoryMessages', {roomid: v.id, offset: 1, limit: 100}); // 后端 关联表、分页、排序 messages.find({roomid: params.roomid}) .populate({path: 'userM', select: 'signature photo nickname'}) // 关联用户基本信息 .sort({'time': -1}) .skip((params.offset - 1) * params.limit) .limit(params.limit) .then(r => { r.forEach(v => { // 防止用户修改资料后,信息未更新 if (v.userM) { v.nickname = v.userM.nickname; v.photo = v.userM.photo; v.signature = v.userM.signature; } }); r.reverse(); callback({code: 0, data: r, count: count}); }).catch(err => { console.log(err); callback({code: -1}); }); 复制代码
主页
聊天窗口,可拖拽或缩放,聊天壁纸及文字颜色设置。
我的设置
应用空间
qq前端交流群:960807765,欢迎各类技术交流,期待你的加入
欢迎关注公众号 前端发动机,江三疯的前端二三事,专一技术,也会时常迷糊。但愿在将来的前端路上,与你一同成长。
本文主要讲了Vchat的总体设计以及一些主要功能的实现,其实写项目过程当中坑仍是挺多的,好比mongoose联表查询、文件上传等等,这里就不在细说,之后有时间再更新。若是Vchat对你有帮助,记得star一下哟^_^。