资讯专栏INFORMATION COLUMN

PAMPANG / 2685人阅读

前言

</>复制代码

  1. 项目开始是因为工作需要一个聊天室功能,但是因为某些原因最终选用的是基于xmpp协议的Strophe.js写的。于是就想用node自己写一套,本来只是想简单的写个聊天页面,但是写完了又不满意,所以不断的重构(似乎可以理解产品经理为什么老是改需求了๑乛◡乛๑)。

    很多东西,比如mongodb,我也是第一次用,以前只接触过mysql。所以都是一边学一边写,利用工作之余的时间,断断续续的写了几个月(这次讲的是V0.9.0版本,项目还在更新中···),包含了一整套的前后端交互。uI是按照自己的感觉来的,没有设计天分(话说主题切换到现在还只有一套主题,实在是不好设计啊~),轻喷---。项目还有很多需要优化完善的地方,欢迎大家提到issues(文末有q群,欢迎一起学习交流)。

  2. 闲话少说,本文主要讲项目的设计流程,以及部分功能实现思路。对项目感兴趣的同学请移步源码 Vchat — 从头到脚,撸一个在线聊天的web应用(vue + node + mongodb)。

*这是分隔线---------------------------------------深夜码字,最近真冷

相关地址

在线预览

github

码云

简书

知乎

项目架构

技术栈

</>复制代码

  1. 前端主要采用了vue全家桶,没什么多说的,脚手架构建项目,vuex状态管理,vue-router控制路由,axios进行前后端交互。后端是基于node搭的服务,用的是express。我为什么不用koa呢,纯粹是图方便,因为koa不熟(捂脸)。聊天最重要的当然是通信,项目用socket.io来进行前后端通信。

    数据库是mongoDB,主要有用户、好友、群聊、消息、表情、号码池等。

功能概览

功能设计

登录注册

</>复制代码

  1. Vchat中用户注册时,会随机指定一个code号码,而这个code号是从预先生成的一个号码池(号码池存在mongodb)中取的。初始指定10000001-10001999的号码段为用户code, 100001-100999的号码段为群聊code。用户可以凭借code号或者账号登录。

</>复制代码

  1. // 号码池设计
  2. * code 号码
  3. * status 1 已使用 0 未使用
  4. * type 1 用户 2 群聊
  5. * random 随机数索引,用于随机查找某一条
  6. // user表主要字段
  7. * name 账号
  8. * pass 密码
  9. * avatar 头像
  10. * signature 个性签名
  11. * nickname 昵称
  12. * email 邮件
  13. * phone 手机
  14. * sex 性别
  15. * bubble 气泡
  16. * projectTheme 项目主题
  17. * wallpaper 聊天壁纸
  18. * signUpTime 注册时间
  19. * lastLoginTime 最后一次登录时间
  20. * chatColor 聊天文字颜色
  21. * province 省
  22. * city 市
  23. * town 县
  24. * conversationsList 会话列表
  25. * cover 封面列表

</>复制代码

  1. 注册时,需要判断账号是否已存在,以及随机取得的code需要在号码池中标记为已被使用,用户密码用md5加密等。

</>复制代码

  1. // md5 密码加密
  2. const md5 = pass => { // 避免多次调用MD5报错
  3. let md5 = crypto.createHash("md5");
  4. return md5.update(pass).digest("hex");
  5. };

</>复制代码

  1. 登录同样需要判断用户是否已注册,以及支持账号和code两种方式登录。

</>复制代码

  1. const login = (params, callback) => { // 登录
  2. baseList.users
  3. .find({ // mongodb中可以直接用$or表示或关系
  4. $or: [{"name": params.name}, {"code": params.name}]
  5. })
  6. .then(r => {
  7. if (r.length) {
  8. let pass = md5(params.pass);
  9. if (r[0]["pass"] === pass) {
  10. //更新最后一次登录时间 此处直接写Date.now 会报错 需要Date.now()!!!;
  11. baseList.users.update({name: params.name}, {lastLoginTime: Date.now()}).then(raw => {
  12. console.log(raw);
  13. });
  14. callback({code: 0, data: {name: r[0].name, photo: r[0].photo}});
  15. } else {
  16. callback({code: -1});
  17. }
  18. } else {
  19. callback({code: -1});
  20. }
  21. })
  22. };

</>复制代码

  1. 登录权限管理

后端设置全局中间件,将没有登录的api请求统一返回status: 0

</>复制代码

  1. app.use("/v*", (req, res, next) => {
  2. if (req.session.login) {
  3. next();
  4. } else {
  5. if (req.originalUrl === "/v/user/login" || req.originalUrl === "/v/user/signUp") {
  6. next();
  7. } else {
  8. res.json({
  9. status: 0
  10. });
  11. }
  12. }
  13. });

前端用axios统一设置拦截器

</>复制代码

  1. // http response 服务器响应拦截器,这里拦截未登录和401错误,并重新跳入登页重新获取token
  2. instance.interceptors.response.use(
  3. response => { // 拦截未登录
  4. if (response.data.status === 0) {
  5. router.replace("/");
  6. }
  7. return response;
  8. },
  9. error => {
  10. if (error.response) {
  11. switch (error.response.status) {
  12. case 401:
  13. // 这里写清除token的代码
  14. router.replace("/");
  15. }
  16. }
  17. return Promise.reject(error.response.data)
  18. });

消息

</>复制代码

  1. vchat中,消息种类包括好友或者加群申请、回复申请(同意or拒绝)、入群通知、聊天消息(文字、图片、表情、文件)

</>复制代码

  1. 在实现消息发送之前,需要大体的了解一些socket.ioapi。详细api文档可以查看socket.io

</>复制代码

  1. // 所有的消息请求都是建立在已连接的基础上的
  2. io.on("connect", onConnect);
  3. // 发送给当前客户端
  4. socket.emit("hello", "can you hear me?", 1, 2, "abc");
  5. // 发送给所有客户端,除了发送者
  6. socket.broadcast.emit("broadcast", "hello friends!");
  7. // 发送给同在 "game" 房间的所有客户端,除了发送者
  8. socket.to("game").emit("nice game", "let"s play a game");
  9. // 发送给同在 "game" 房间的所有客户端,包括发送者
  10. io.in("game").emit("big-announcement", "the game will start soon");

加入房间

</>复制代码

  1. 加入会话列表中的房间,会话列表在好友申请成功或者加群成功时会自动添加。但是你也可以手动移除或添加,移除后将不会再收到被移除会话的消息(类似于屏蔽)。

</>复制代码

  1. // 前端 发起加入房间的请求
  2. this.conversationsList.forEach(v => {
  3. let val = {
  4. name: this.user.name,
  5. time: utils.formatTime(new Date()),
  6. avatar: this.user.photo,
  7. roomid: v.id
  8. };
  9. this.$socket.emit("join", val);
  10. });
  11. // 后端 接受请求后执行加入操作,记录每个房间加入的成员,以及回信告知指定房间已上线成员
  12. socket.on("join", (val) => {
  13. socket.join(val.roomid, () => {
  14. if (OnlineUser[val.name]) {
  15. return;
  16. }
  17. OnlineUser[val.name] = socket.id;
  18. io.in(val.roomid).emit("joined", OnlineUser); // 包括发送者
  19. });
  20. });

多房间

</>复制代码

  1. 同时加入多个聊天房间会出现一个问题,socket可以加入多个房间并给指定房间发送消息,但是接受消息的时候并不会区分房间。换句话说,所有房间的消息,会一起发送给客户端。所以我们需要自己区分哪条消息是哪个房间的并进行分发。这样就需要一个房间标识来过滤,Vchat用的是房间id

</>复制代码

  1. mes(r) { // 只有本房间的消息才展示
  2. if (r.roomid === this.currSation.id) {
  3. this.chatList.push(Object.assign({}, r, {type: "other"}));
  4. }
  5. }

发消息

</>复制代码

  1. // 前端
  2. send(params, type = "mess") { // 发送消息
  3. if (!this.message && !params) {
  4. return;
  5. }
  6. let val = {
  7. name: this.user.name,
  8. mes: this.message,
  9. time: utils.formatTime(new Date()),
  10. avatar: this.user.photo,
  11. nickname: this.user.nickname,
  12. read: [this.user.name],
  13. roomid: this.currSation.id,
  14. style: "mess",
  15. userM: this.user.id
  16. };
  17. this.chatList.push(Object.assign({},val,{type: "mine"})); // 更新视图
  18. this.$socket.emit("mes", val);
  19. this.message = "";
  20. }
  21. // 后端 接收消息后存储到数据库,并转发给房间内其他成员,不包括发送者。
  22. socket.on("mes", (val) => { // 聊天消息
  23. apiList.saveMessage(val);
  24. socket.to(val.roomid).emit("mes", val);
  25. });

消息记录

</>复制代码

  1. 所有的消息都会存到mongodb中,当切换房间的时候,会获取历史消息。而处在当前房间时,只会把最新消息追加到dom中,不会从数据库获取。聊天窗口默认只展示最新100条消息,更多消息可在聊天记录中查看。

</>复制代码

  1. // 前端 获取指定房间的历史消息
  2. this.$socket.emit("getHistoryMessages", {roomid: v.id, offset: 1, limit: 100});
  3. // 后端 关联表、分页、排序
  4. messages.find({roomid: params.roomid})
  5. .populate({path: "userM", select: "signature photo nickname"}) // 关联用户基本信息
  6. .sort({"time": -1})
  7. .skip((params.offset - 1) * params.limit)
  8. .limit(params.limit)
  9. .then(r => {
  10. r.forEach(v => { // 防止用户修改资料后,信息未更新
  11. if (v.userM) {
  12. v.nickname = v.userM.nickname;
  13. v.photo = v.userM.photo;
  14. v.signature = v.userM.signature;
  15. }
  16. });
  17. r.reverse();
  18. callback({code: 0, data: r, count: count});
  19. }).catch(err => {
  20. console.log(err);
  21. callback({code: -1});
  22. });
项目展示

</>复制代码

  1. 主页

</>复制代码

  1. 聊天窗口,可拖拽或缩放,聊天壁纸及文字颜色设置。

</>复制代码

  1. 个人设置

</>复制代码

  1. 应用空间

相关阅读

Mongoose基础入门

socket.io文档

Vchat主题切换实现方案来自于 d2-admin

交流群

群内有丰富学习资料^_^

写在后面

</>复制代码

  1. 本文主要讲了Vchat的整体设计以及一些主要功能的实现,其实写项目过程中坑还是挺多的,比如mongoose联表查询、文件上传等等,这里就不在细说,以后有时间再更新。如果Vchat对你有帮助,记得star一下哟^_^。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/19463.html

相关文章

  • vue中如何实现的自定义按钮

    摘要:在实际开发项目中,有时我们会用到自定义按钮因为一个项目中,众多的页面,为了统一风格,我们会重复用到很多相同或相似的按钮,这时候,自定义按钮组件就派上了大用场,我们把定义好的按钮组件导出,在全局引用,就可以在其他组件随意使用啦,这样可以大幅度 在实际开发项目中,有时我们会用到自定义按钮;因为一个项目中,众多的页面,为了统一风格,我们会重复用到很多相同或相似的按钮,这时候,自定义按钮组件就...

    biaoxiaoduan 评论0 收藏0
  • JavaScript代码整洁之道

    摘要:代码整洁之道整洁的代码不仅仅是让人看起来舒服,更重要的是遵循一些规范能够让你的代码更容易维护,同时降低几率。另外这不是强制的代码规范,就像原文中说的,。里式替换原则父类和子类应该可以被交换使用而不会出错。注释好的代码是自解释的。 JavaScript代码整洁之道 整洁的代码不仅仅是让人看起来舒服,更重要的是遵循一些规范能够让你的代码更容易维护,同时降低bug几率。 原文clean-c...

    liaorio 评论0 收藏0
  • 前端经典面试题总结

    摘要:接着我之前写的一篇有关前端面试题的总结,分享几道比较经典的题目第一题考点作用域,运算符栗子都会进行运算,但是最后之后输出最后一个也就是那么其实就是而且是个匿名函数,也就是属于,就输出第二和第三个都是类似的,而且作用域是都是输出最后一个其实就 接着我之前写的一篇有关前端面试题的总结,分享几道比较经典的题目: 第一题: showImg(https://segmentfault.com/im...

    BlackMass 评论0 收藏0
  • 私有云那家好-六大私有云厂商详细对比!

    对比内容UCloudStackZStackVMwareQingCloud腾讯TStack华为云Stack优势总结•基于公有云自主可控•公有云架构私有化部署•轻量化/轻运维/易用性好•政府行业可复制案例轻量化 IaaS 虚拟化平台•轻量化、产品成熟度高•业内好评度高•功能丰富、交付部署快•中小企业案例多全套虚拟产品及云平台产品•完整生态链、技术成熟•比较全面且健全的渠道•产品成熟度被市场认可,市场占...

    ernest.wang 评论0 收藏0
  • cross-env使用记录

    摘要:能跨平台地设置及使用环境变量让这一切变得简单,不同平台使用唯一指令,无需担心跨平台问题安装方式改写使用了环境变量的常见如在脚本多是里这么配置运行,这样便设置成功,无需担心跨平台问题关于跨平台兼容,有几点注意 cross-env能跨平台地设置及使用环境变量, cross-env让这一切变得简单,不同平台使用唯一指令,无需担心跨平台问题 1、npm安装方式 npm i --save-de...

    Michael_Ding 评论0 收藏0
  • webpack打包插件

    摘要:引入的模块引入的使用将打包打包的拆分将一部分抽离出来物理地址拼接优化打包速度压缩代码,这里使用的是,同样在的里面添加 const path = require(path); //引入node的path模块const webpack = require(webpack); //引入的webpack,使用lodashconst HtmlWebpackPlugin = require(ht...

    ChanceWong 评论0 收藏0

发表评论

0条评论

PAMPANG

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<