从 0 到 1 开发一个聊天通信 服务 复盘总结分享

2021年09月15日 阅读数:2
这篇文章主要向大家介绍从 0 到 1 开发一个聊天通信 服务 复盘总结分享,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

前言

在上个月初,接到一个需求,要开发一个 聊天通信 模块 而且 集成到 项目中的多个 入口,实现业务数据的记录追踪.javascript

接到需求后,还挺开心,这是我第一次 搞 通信 类的需求,以前一直是 B 端 的业务需求,不过如今也是在作这个方向,感受 B 端 方向 挺有意思,管理着项目的整个项目上游和下游,而后服务于 内部人员 和 外部人员 使用,感受挺自豪的。css

下面就就跟着我来看看 如何 开发一个 聊天通信 服务吧 ! (主要站在前端的角度来说如何开发设计 )html

技术栈

徽章.png

  • Vue 2.x
  • Websoket
  • Vuex
  • Element
  • vue-at

本项目是 以 Vue 技术栈生态开发的,其实无论用什么语言 , 思路是关键 ! 知道每一步须要干什么, 而后将每一步操做 整合起来 , 最终服务就跑起来了.前端

当中的每一步须要干什么 就是 编程 中的 function 功能,根据这个功能而后在细化分析须要有到哪些技术点 。在开发的过程当中,你不可能对整个链路的全部技术点 熟悉,这就须要遇到啥困难,临时学习就能够了。vue

开始分析需求

首先,咱们要等待 UI 设计师 的设计稿 画出来, 而后根据 UI 设计师的 设计稿分析总体 聊天通信 的结构,从view 结构 来 划分 应该 大致 包括哪些 component , 每一个component 中 又包括哪些小的 component , 这样从 大 到 小 的方向将 设计稿 转化为 程序员视角的 component .java

确立了有哪些component , 接下来 就是 肯定 每一个 小的 component 又有哪些 功能了。 如今 UI 设计师们,通常画完界面后,会经过第三方软件 / 平台 来将效果图 转化成网页,而且能够经过 URL 能够直接访问,当光标放到页面中的某个元素时,能够获取到当前元素的 css style , 不过,我建议不之 copy ,有时和本身写的布局代码会冲突,按需copy .git

效果图

真实效果图,我就在这里不放出来了,为了保密性,只把总体结构,列出来,而后带着你们分析结构和功能,如何进行编码设计和组件设计。程序员

功能分析图

根据效果图,在进行组件划分时,我要记住这个原则:高内聚,低耦合 , 组件职责单一性github

咱们将组件划分为:web

  • 联系人组件
  • 聊天组件 ---- 包括了 历史记录组件

功能根据 UI 设计师 提供的 URL 网页来看交互效果来定,并和组长 / 产品经理 交流需求,肯定需求,以及砍掉不合理需求。

需求肯定后,就是梳理组件部分的功能了。

组件构成

在分析组件以前,咱们须要先了解一下Vue Component ,使用Vue 的 朋友应该很熟悉了,一个组件的构成由如下组成:

  1. data 组件内部状态
  2. computed 计算属性,监听data 变化来实现对应的业务逻辑需求
  3. watch 监听state 变化
  4. method 组将的功能编写区
  5. props 组件接受父组件 传递来的值,进行约束类型等
  6. lifecycle 组件的生命周期, 能够在组件建立到销毁的过程当中执行对应的业务逻辑

联系人组件

这个组件主要是用来在聊天的时候,能够经过分组快速的找到某我的联系它,功能相对简单。

功能:

  1. 查找联系人
  2. 有通知某人操做

功能分析

功能1: 查找联系人

经过现有联系人json 数据来 查找输入的联系人进行匹配。 (简单)

功能2: 通知某人

当用户点击到某个联系人时,将点击的人 放到输入框里 显示 @xxx [ 通过格式化处理 ] , 并将选中的联系人信息加入到发送消息的 json 对象中。

有多种实现方案,当用户点击了某联系人时,将触发事件,携带值传递给父组件[聊天组件的入口 index.vue ] 接收,而后将值传递给 聊天主体组件 ,经过 在 聊天主体组件 中 经过 $refs 进行传递值。

下面只提供示例代码

从联系人列表获取选中联系人

//联系人组件 concat.vue
​
​
getLogname(val){
    this.$emit('toParent',{tag:'add',logname:val})
},

聊天框显示选中的联系人

在聊天入口组件 接收 子向父 组件传递 选中联系人数据,而后给 聊天主体 组件绑定 ref , 经过refs 来将联系人数据传递到 聊天主体 组件显示。 [这块 数据传递有多种方法,例如 Vuex]

//聊天组件入口 index.vue   它包括 联系人组件  聊天主体组件  历史记录组件
​
//联系人组件
<Concat @toParent='innerHtmlToChat'/>
​
//聊天主体组件    
<ChatRoom @fullScreen="getFullStatus" @closeWindow="close" ref="chatRoom"/>
​
​
    
 // 接受
 innerHtmlToChat(data){
    this.$refs.chatRoom.$refs.inputConents.innerHTML+=`&nbsp;@&nbsp;${data.logname}`  //拼接到聊天输入框里
},     
​

效果展现

从联系人列表选中人员,发送消息

@人 接收到推送消息

聊天主体组件

这个组件就负责的功能就多了,这块我主要把关键的功能带你们来分析过一遍

关键功能;

  1. @ 好友功能,实现推送通知(在线通知 / 离线-上线通知)
  2. 聊天工具 [ 支持表情 支持大文件上传 ]
  3. 发送消息 [ 这块就能够跟业务挂钩了,发送信息时,并携带一些符合你项目需求的数据]

功能分析

功能1 : @ 实现

vue-at 文档 : https://github.com/von7750/vu...

它的功能和 微信QQ @ 功能同样,在聊天输入框里,当你 输入 @ 键时, 弹出好友列表,而后从中选择联系人进行聊天。

@ 功能必须包括如下3个关键功能;

  • 能够弹出联系人列表
  • 能够监听输入字符内容进行过滤显示对应数据
  • 删除 @ 联系人
  • .......

一开始, 我是 本身造了个 @ 功能 轮子 搞了搞,后来才发现市场上有相应的轮子,直接用第三方了,挺不错的 vue-at

下面来跟着我,来捋一下思路如何实现这个轮子,此处就不放实现代码了。

先来分析一波:

当在编辑区,输入 @ 时, 弹出框

  1. 咱们能够在 mounted 生命周期中监听 按键 code = 50 / 229 (中文/英文) 时,作出处理
  2. 因为咱们这块采用的 div 可编辑属性 ,那么就获取到 可编辑属性的光标位置
  3. 而后经过光标位置 动态来改变 弹出框联系人列表的样式 top left , 实现跟着光标的 位置显示联系人列表。
  4. 而后 从列表中选择 联系人进行聊天,并将 联系人列表弹框 隐藏掉。

上面就实现了基本的 选中联系人功能

删除选中的联系人

因为这块是采用的可编辑属性, 咱们能够获取选中的人,但没法直接判断是删除的哪一个人,这时,只能经过判断 innerHTML 中是否包含某联系人,来进行删除已保存的联系人。

这时,已经基本知足了业务需求实现了。

第三方插件已经的够好了,咱们就不必再造轮子,浪费时间了, 但 实现思路 必须的懂。 下面,我就来演示如何使用 第三方插件vue-at 实现 @ 功能

1. 安装插件

npm i vue-at@2.x

2.组件 内部导入插件组件

import At from "vue-at";

3.注册插件组件

 components: {
        At
 },

4. 页面中使用

At 组件 必须包括 可编辑 输入内容区域, 这样,当输入 @ 时,会弹出联系人列表框。

  • members : 数据源
  • filter-match : 过滤数据
  • deleteMatch : 删除的联系人
  • insert : 获取联系人
<At
    :members="filtercontactListContainer"
    :filter-match="filterMatch"
    :deleteMatch="deleteMatch"
    @insert="getValue"
    >
    <template slot="item" slot-scope="s">
        <div v-text="s.item" style="width:100%"></div>
    </template>
    <div
         class="inputContent"
         contenteditable="true"
         ref="inputConents"
         ></div>
</At>
// 过滤联系人
filterMatch(name, chunk) {
    return name.toLowerCase().indexOf(chunk.toLowerCase()) === 0;
},
// 删除联系人
deleteMatch(name, chunk, suffix) {
    this.contactList = this.contactList.filter(
            item => item.logname != chunk.trim()
        );
  return chunk === name + suffix;
},
// 获取联系人
getValue(val) {
     this.contactList.push({ logname: val });
},

功能2:聊天工具箱

聊天软件除了普通文字聊天,还有一些辅助服务来增长聊天的丰富性,例如: 表情 , 文件上传, 截图上传 .... 功能

咱们先来看看 市场 热门聊天软件它们有哪些 聊天工具。

微信聊天工具箱

  • 表情
  • 文件上传
  • 截屏
  • 聊天记录
  • 视频聊天 / 语音聊天

QQ 聊天工具箱

  • 表情
  • GIF 动图
  • 截屏
  • 文件上传
  • 腾讯文档
  • 图片发送
  • ..... 腾讯业务相关功能

介绍了市场上热门聊天的工具箱有哪些工具,回归正题: 咱们的聊天工具箱 有哪些功能呢, 其实有哪些功能根据 业务来定,后期工具箱能够不断扩充。 咱们的工具箱基本上知足平常聊天需求

  • 表情
  • 文件上传 支持大文件 ( 几个G 均可以)
  • 截屏 Ctrl + Alt + A
  • 历史记录

下面我就来将比较几个重要的功能: 文件上传截屏 , 其它功能都很简单。

文件上传

上传组件我采用的是 Element el-upload 组件,因为我业务 要求上传文件支持大文件, 采用的 分片续传 方式来实现。

分片续传思路

  1. 咱们上传也是采用的 websoket 上传,首次发送时,必须发送一些必要的文件基本信息

    • 文件名
    • 文件大小
    • 发送者
    • 一些跟业务相关的字段数据
    • 时间
    • 文件分片大小
    • 文件分片片数
    • 上传进度标识
  2. 首次发送完文件的基本信息后,开始发送分片文件信息,首先将文件分片后,而后依次读取片文件流,发送时携带文件流,等文件分片循环结束后,发送一个结束标识告诉后台发送完毕了 [这块你能够和后端商量设计数据格式]

示例代码演示

<el-upload
           ref="upload"
           class="upload-demo"
           drag
           :auto-upload="false"
           :file-list="fileList"
           :http-request="httpRequest"
           style="width:200px"
           >
    <i class="el-icon-upload"></i>
    <div class="el-upload__text" trigger>
        <em> 将文件拖到此处而后点击上传文件</em>
    </div>
</el-upload>

覆盖掉 Element 默认上传方式,改用自定义上传方式。

开始分片上传

    // 上传文件
    httpRequest(options) {
      let that = this;
​
      //每一个文件切片大小
      const bytesPerPiece = 1024 * 2048;
     // 文件必要的信息
      const { name, size } = options.file;
     // 文件分割片数
      const chunkCount = Math.ceil(size / bytesPerPiece);
      
    // 获取到文件后,发送文件的基本信息
      const fileBaseInfo = {
        fileName: name,
        fileSize: size,
        segments: "historymessage",
        loginName: localStorage.getItem("usrname"),
        time: new Date().toLocaleString(),
        chunkSize: bytesPerPiece,
        chunkCount: chunkCount,
        messagetype: "bufferfile",
        process: "begin",
          
          
        ... 一些跟业务挂钩的 字段
​
      };
​
​
      that.$websoketGlobal.ws.send(JSON.stringify(fileBaseInfo));
      
      let start = 0;
​
      // 进行分片
      var blob = options.file.slice(start, start + bytesPerPiece);
      //建立`FileReader`
      var reader = new FileReader();
      //开始读取指定的 Blob中的内容, 一旦完成, result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象.
      reader.readAsArrayBuffer(blob);
      //读取操做完成时自动触发。
      reader.onload = function(e) {
        // 发送文件流
        that.$websoketGlobal.ws.send(reader.result);
        start += bytesPerPiece;
        if (start < size) {
          var blob = options.file.slice(start, start + bytesPerPiece);
          reader.readAsArrayBuffer(blob);
        } else {
          fileBaseInfo.process = "end";
          // 发送上传文件结束 标识
          that.$websoketGlobal.ws.send(JSON.stringify(fileBaseInfo));
        }
        that.uploadStatus = false;
        that.fileList = [];
      };
    },

效果演示

功能3: 截屏功能

PC 中,这是一个很重要的业务,经过这种技术能够从网上截取下本身感兴趣的文章图片供本身使用观看,能够帮助人们更好的去理解使用知识。

因为咱们的输入内容区域采用的 可编辑 区域,此处能够插入任意内容,也可使用外部 的截图功能,粘贴到输入框区域,这块就不必的造轮子了

1. 可编辑区域

咱们给 div 加上 该属性 contenteditable 就能够控制 div 中可输入哪些内容,外部复制过来内容也能够直接显示,还能够显示其带的css 效果。咱们先来看看 contenteditable 有哪些属性吧 !

描述
inherit 默认值继承自父元素
true 或空字符串,表示元素是可编辑的;
false 表示元素不是可编辑的。
plaintext-only 纯文本
caret 符号
events

注意

不容许简写为 <label contenteditable>Example Label</label>

正确的用法是 <label contenteditable="true">Example Label</label>

浏览器支持状况

使用

<div
     class="inputContent"
     contenteditable="true"
     ref="inputConents">
</div>

效果展现

2. 截屏

因为采用的是 可编辑 ,那么就能够随意从外部 copy , 哈哈,有意思的来了,支持 Windows 自带的截屏 + PC 第三方 截屏......

💥快捷操做方法:

  • windows 自带的的截屏快捷键

    截取整个屏幕 Print Screen

    截取当前活动屏幕 Alt+Print Screen

  • QQ 截屏功能,支持个性化操做截图 Ctrl + Alt + A
  • 微信 截屏功能, 支持个性化操做截图 Alt + A
  • 专门的截屏工具....

站在巨人的肩膀上, 直接起飞。😄 , 不过确实站在用户角度想,这点确实有点很差😘。

实际效果演示

2.1 微信截屏 show time

2.2 QQ 截屏

功能4: 发送功能

这个功能贯穿这个聊天项目,项目采用的是 websoket 实现的通讯服务,全双工通讯 , 发送聊天内容时,须要携带一些很业务相关的数据,来实现业务跟踪分析。下面,来简单复习过一下 websoket , 对没有使用过websoket 同窗也时学习。

WebSoket

WebSocket是一种在单个TCP链接上进行全双工通讯的协议。 WebSocket使得客户端和服务器之间的数据交换变得更加简单,容许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只须要完成一次握手,二者之间就直接能够建立持久性的链接,并进行双向数据传输。

WebSoket 特色

  • 服务器能够主动向客户端推送信息,客户端也能够主动向服务器发送信息,是真正的双向平等对话。
  • 属于服务器推送技术的一种。
  • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,而且握手阶段采用 HTTP 协议.
  • 数据格式比较轻量,性能开销小,通讯高效。
  • 能够发送文本,也能够发送二进制数据。
  • 没有同源限制,客户端能够与任意服务器通讯。
  • 协议标识符是ws(若是加密,则为wss),服务器网址就是 URL。

WebSoket 操做 API

建立Websoket链接🔗

let socket = new WebSocket("ws://域名/服务路径")

链接 Websoket 成功触发

open() 方法在链接成功时,触发

socket.onopen = function() {
    console.log("websocket链接成功");
};

发送消息

send()方法并传入一个字符串ArrayBufferBlob .

socket.send("公众号: 前端自学社区")

接收服务端返回的数据

message 事件会在 WebSocket 接收到新消息时被触发。

socket.onmessage = function(res) { 
 console.log(res.data)
}

关闭 WebSoket 链接

WebSocket.close() 方法关闭 WebSocke链接或链接尝试(若是有的话)。 若是链接已经关闭,则此方法不执行任何操做。

socket.onclose = function() {
    // 关闭 websocket
    console.log("链接已关闭...");
    //断线从新链接
    setTimeout(() => {
        that.initWebsoket();
    }, 2000);
};

WebSoket 错误处理

websocket的链接因为一些错误事件的发生 (例如没法发送一些数据)而被关闭时,一个error事件将被引起.

// 监听可能发生的错误
socket.addEventListener('error', function (event) {
  console.log('WebSocket error: ', event);
});

经过上面咱们了解了 Websoket 如何使用,接下来就是 实操了,下面走起!

项目采用的是 Vue 技术栈,更多写法偏向于 Vue 。 因为 WebSoket 贯穿整个项目,并且须要实时推送 @ , 咱们将 Websoket 尽可能放在全局入口,接收信息onmessage 事件也放在 入口文件中,这样全局都能接收到数据,接收到的数据 利用 Vuex 进行管理聊天的数据 [ 历史数据 推送数据 发送数据 ]

1. 新建 一个 websoket文件,用于全局使用

export default {
    ws: {},
    setWs: function(wsUrl) {
        this.ws = wsUrl
    }
}

2. 在Vue入口文件index.js中 全局注册

import Vue from 'vue'
import websoketGlobal from './utils/websoket'

Vue.prototype.$websoketGlobal = websoketGlobal

3. 在 App.vue 中 接收 Websoket 推送的消息

这块的设计很关键,决定了聊天数据的存储和设计,过多细节代码就不放了

大致思路我说说一下:

  • 传输格式上定了,那么接收的数据结构也就定了,更多的就是在数据结构上下文章了, 先后端须要约束好字段属性。

    从聊天页面显示状态来看:

    1. 区分数据类型的字段,这样前端在接收到推送的消息时,知道在页面中该如何显示,例如(该显示图片样式仍是文本样式)
    2. 区分发送消息显示左右的字段, 前端经过接收到推送的消息时, 会首先判断是否为本身,不是的话显示在左边样式
    3. 区分 系统的推送字段, 根据这个字段显示对应的样式。
    4. ........... 更多字段属性 须要根据你实际业务而来定

    从信息推送状态来看:

    1. @ 推送全局 Notification 通知 和 聊天内部推送 设计

      • @ 推送 根据指定字段类型判断 ,而后实现全局 推送
      • 聊天内容推送: 因为它和具体某个聊天有关系,它也属于历史聊天数据,在聊天中根据 内容数据类型 来肯定如何显示
mounted(){
    this.$websoketGlobal.ws.onmessage = res => {
        const result = JSON.parse(res.data);
​
        // 推送数据
​
        //聊天历史数据 新增长发送的数据
​
​
        // 获取聊天历史数据
​
        //聊天历史数据 新增长发送的数据
​
    };
}

4. 在聊天组件中使用 Websoket

在聊天组件中,其实使用的就是 发送功能 和 获取 历史记录 功能,还有就是根据 推送的消息内容字段来决定页面中数据如何显示。下面聊天的样式代码就不放了,主要放一下 发送消息的 示例代码

send() {
    let that = this;
​
    // 定义数据结构: 传递什么内容是 前提 前端和后端商量好的  
    const obj = {
        messageId: Number(
            Math.random()
            .toString()
            .substr(3, length) + Date.now()
        ).toString(36),
        //文件类型  
        messagetype: "textmessage",
        //@ 联系热
        call: that.contactList,
        //聊天输入内容  
        inputConent: that.$refs.inputConents.innerHTML ,
        // 当前时间  
        time: currentDate,
​
        ..... 再定义一些符合你业务的字段    
    };
    
    // 发送消息
    that.$websoketGlobal.ws.send(JSON.stringify(obj));
    that.$refs.inputConents.innerHTML = "";
    that.contactList = []
}
},

在每次进入聊天组件时,须要首先获取聊天的历史记录,聊天入口根据你的业务来定,传递必须参数.

mounted(){
    this.$websoketGlobal.ws.send(
        JSON.stringify({
            id: 1
            messagetype: "historymessage"
        })
    );
}

功能5: 离线 / 在线推送

这个至关于 微信 / QQ 在线 和 上线 收到的消息。 当 A 用户 @ 了 B 用户 (此时 B 用户 不在线),当 B 用户 上线时,它会收到 一条信息。这个是怎么实现呢?

我就结合项目来大致说一下思路,具体实现就不说了,实现主要在后端。 当时,向后端大佬同时还特地请教了一下。

\
当 A用户 登陆了 系统,此时就会和 Websoket 创建链接,后端会记录起来,该用户的标识,状态为登陆。

当 A 用户 @ 了 B 用户 ,正常逻辑会推送给B用户一条信息,B 不在线,就不推给他?

怎么知道B 用户是否在线呢?

前面也说到了,登陆系统就会创建链接,后端会暂时存储起来在线的用户,当A 用户 向 B 用户发送的消息后,后端看在线用户列表里没有B 用户,那么他就不会推送。当B用户上线了,会自动推送,前端接收,直接提醒用户。

聊天室入口组件

聊天室入口组件包括: 联系人组件 + 聊天主体组件 , 它作的事情其实很简单了。

  1. 如何打开聊天室 ?
  2. 如何给聊天室传递历史数据?

如何打开聊天室?

外部可能经过多个入口来打开聊天室,经过一个状态来控制显示聊天室,传递类型为Boolean

如何给聊天室传递历史数据?

外部经过给聊天室组件传递必要数据,这些必要数据而后在联系人组件聊天主体组件 内部消耗,获取各自须要的数据,这样聊天室入口组件的职责单一,很好进行管理。

下面来看看聊天室的入口组件:

<template>
  <div>
    <transition name="el-fade-in-linear" :duration="4000">
      <div
        class="chat-container"
      >
        <div
          class="left-concat"
        >
            //联系人组件
          <Concat @toParent="innerHtmlToChat" />
        </div>
        <div
          class="right-chatRoom"
        >
            // 聊天室主体组件
          <ChatRoom
            ref="chatRoom"
          />
        </div>
      </div>
    </transition>
  </div>
</template>

内部的通讯主要是由 Vuex 来进行管理, 因为聊天室在全局都须要唤醒,能够将聊天入口组件放到全局入口文件,这样,无论项目须要多少个入口,只须要传递唤醒聊天入口组件的状态入口组件须要的必要参数 来获取历史聊天数据。

<Chat
      // 控制是否显示聊天室
      v-if="$store.state.chatStore.roomStatus"
      //聊天室须要的必要数据
      :orderInfo="$store.state.chatStore"
 />

这样,当项目其它模块须要 聊天室 这个功能,只须要 一行代码 便可 接入,做为插槽接入。

<template slot="note" slot-scope="props">
    <i class="el-icon-chat-dot-square"  @click="openChatRoome(props.data.row)"></i>
</template>
openChat(row){
    this.$store.commit("Chat", { status: true, data: row });
},

总结

在开发这个 聊天服务 中也遇到了不少难点和坑,不过一个一个踩过来了,越日后作思路越开。 开发完这个 聊天服务 对技术理解又有更深的认知了,在你感受某个功能很难困难,不知道怎么实现,你先行动起来,按照本身的思路一步一步推理,推理的过程就会思路打开了,会有多种方式来实现了。

最后

聊天服务开发了一个月,写文章写了一个周左右,写做不易,若是文章学到了,点个赞👍👍👍关注,支持一下!

关注公众号: 前端自学社区 , 加入自学交流群,一块儿成长学习!