评论

云开发私人实时聊天室

一个云开发实时聊天室的构思与设计

云开发私人实时聊天室

说明

在最开始开发小程序时,本人和团队成员实现小程序的聊天室时遇到一些困难,查阅了一些资料,有些讲得太泛,有些讲的太难,在一个阶段克服了这个困难后,收获了很多,对整个流程也熟悉了很多,在这里记录自己的一个思路,希望也能对开发新手有帮助。

项目基本配置

1.项目创建及云开发配置:

官方文档:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/quick-start/miniprogram.html

PS:注意云函数目录是否为此样式:

若是普通目录样式记得在project.config.json中配置加入:

2.添加包colorui,用于样式使用,并在app.wxss中导入改包

3.在pages下新建文件夹index和新建page:index

聊天室静态页面

最终呈现的效果:

自己:

对方:

1. wxml

整体结构:

整一个页面说白了就是由一个scroll-view和一个回复框组成,scroll-view中由消息数组构成,消息的内容可以自己定义(时间,头像,消息内容等等)

具体源码:

<!-- scroll-view来实现页面拖动 -->
<scroll-view id='page' scroll-into-view="{{toView}}" upper-threshold="100"
  scroll-y="true" enable-back-to-top="true" class="message-list">
  <!-- 每一条消息 -->
  <view class="cu-chat" wx:for="{{3}}" wx:key="index" id="row_{{index}}">
    <!-- 自己发出的消息 -->
    <block wx:if="{{false}}">
      <block wx:if="{{true}}">
        <view class="datetime" style="width:100%">2021-11-16 18:10</view>
      </block>
      <view class="cu-item self"
        style="width: 750rpx; height: 120rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx">
        <view class="main">
          <view class="content bg-green shadow" style="position: relative; left: 0rpx; top: 22rpx;border-radius: 10rpx">
            <text style="font-size:33rpx">这是一条消息</text>
          </view>
        </view>
        <view class="cu-avatar radius center"
          style="background-image: url({{useravatar}}); width: 71rpx; height: 71rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx"
          bindtap="go_myinfo"></view>
      </view>
    </block>
     <!-- 对方发出的消息 -->
     <block wx:else>
      <block wx:if="{{true}}">
        <view class="datetime" style="width:100%">2021-11-16 19:10</view>
      </block>
      <view class="cu-item"
        style="width: 750rpx; height: 120rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx">
          <view class="cu-avatar radius center"
            style="background-image: url({{match_avatar}}); width: 71rpx; height: 71rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx">
          </view>
        <view class="main">
          <view class="content bg-white shadow" style="position: relative; left: 0rpx; top: 22rpx;border-radius: 10rpx">
            <text style="font-size:33rpx">这是对面的一条消息</text>
          </view> 
        </view>
      </view>
    </block>
  </view>
</scroll-view>


<!-- 回复框 -->
<view class="reply cu-bar">
  <!-- 输入框 -->
  <view class="opration-area">
    <input type="text" bindinput="getContent" value="{{textInputValue}}" maxlength="300" cursor-spacing="10"
      style="width: 544rpx; height: 64rpx; display: block; box-sizing: border-box;"></input>
  </view>
  <!-- 发送按钮 -->
  <button class="cu-btn bg-green shadow" bindtap='sendMsg'
    style="width: 150rpx; height: 64rpx; display: flex; box-sizing: border-box; left: -22rpx; top: 0rpx; position: relative">发送</button>
</view>

2. wxss

一些样式的配置,具体就不详细叙述了,见源码:

/*消息窗口*/
.message-list {
    margin-bottom: 54px;
}
​
/*文本输入或语音录入*/
.reply .opration-area {
    flex: 1;
    padding: 8px;
}
​
/*回复文本框*/
.reply input {
    background: rgb(252, 252, 252);
    height: 36px;
    border: 1px solid rgb(221, 221, 221);
    border-radius: 6px;
    padding-left: 3px;
}
​
/*回复框*/
.reply {
    display: flex;
    flex-direction: row;
    justify-content: flex-start;
    align-items: center;
    position: fixed;
    bottom: 0;
    width: 100%;
    height: 108rpx;
    border-top: 1px solid rgb(215, 215, 215);
    background: rgb(245, 245, 245);
}
​
/*日期*/
.datetime {
    font-size: 10px;
    padding: 10px 0;
    color: #999;
    text-align: center;
}

到此,静态的页面就已经做好啦,现在主要的难题也是数据部分,下面将先讲述数据库chatroom的设计及解释,最后进行js的代码编写。

数据库创建及设计

1.数据库表创建:

在编辑器打开云开发控制台,点击数据库,再点击集合名称右边加号,创建一个集合名称为chatroom的表。

2.chatroom设计

具体页面如图:

其中,

_id为记录创建时自动创建的标识属性,即主键

_openid和match_openid代表了自身和对方

records为一个对象数组,每个对象的属性分别是:

msgText:消息属性(此案例中只有text属性,即文本,可自扩展为图片、音频等)

openid:发送人的标识

sendTime:消息创建时间

sendTimeTS:消息创建时的时间戳(用于做时间比较,判断时间显示)

showTime:消息是否显示时间

textContent:具体文本内容

其中,

records:array类型,

records中的记录:object类型

records中的sendTimeTS:number类型

records中的showTime:boolean类型

其余全为string类型

PS:

1、openid和match_openid可标识一个聊天室,是唯一不变的;

2、用户本身的openid是有可能在记录中的match_openid位置上的,谁发起了这个聊天室,openid这个位置就是那个发起用户的openid,所以在开发中,想要获取自己和所有其他人的聊天室,要查每条记录中的openid或者match_openid与自身openid是否匹配。

3.权限设置

因为该表中的记录,非记录创建者也可以进行读写,这里的权限记得设置,不然会出问题:

具体功能实现(JS写法)

1.先配置Page.data:6个属性,如有需要可自行扩展

chats存储数据库表中的records的所有信息;

textInputValue是输入框内容。

2.绑定数据库表onChange函数:

这里的onChange输出e是这样的:

type=init,获取了数据库表中该记录的所有内容,在这里将js中的chats进行赋值即可;

另外,当该记录内容变化时,type是update类型

3.wxml修改,wx-for将chats显示,以及一些判断和内容显示的设置:

到此,显示效果就有啦

接下来,就是信息的添加了,下面将显示如何添加新信息到数据库

4.发送信息

先获取输入框内容:

发送函数:

增加一条信息,就是在records数组中加一条记录,所以在函数内部要对新纪录的属性进行一些赋值和判断等。

对showTime的处理:

消息空白处理:

对消息内的所有属性进行一个打包处理:

存储记录,并滑动页面:

最后,清空消息框内容

发送一条消息,最终效果如图:

js源码

const app = getApp()
const db = wx.cloud.database()
const _ = db.command
const chatroomCollection = db.collection("chatroom")
var util = require('../../utils/util.js');
​
Page({
​
  data: {
    //这里的openid和match_openid应该是在上一级页面传进来的属性,这里由于只有聊天室所以暂时设置为一些固定值,用于测试
    openid:'',
    match_openid:'',
    //这里的avatar是头像,具体传参方式自己设定,这里暂时设置为固定值,用于测试
    useravatar:'',                       
    match_avatar:'',
    chats:[],
    textInputValue:''
  },
​
  onReady() {
    var that = this
    //查询openid和match_openid所标识的唯一聊天室
    chatroomCollection.where({
        _openid: _.or(_.eq(that.data.openid), _.eq(that.data.match_openid)),
        match_openid: _.or(_.eq(that.data.openid), _.eq(that.data.match_openid))
      })
      //绑定onChange,直观而言即表中该记录发生变动时,调用该函数
      .watch({
        onChange: this.onChange.bind(this),
        onError(err) {
          console.log(err)
        }
      })
  },
​
​
  //数据库表onchange绑定函数
  onChange(e) {
    let that = this
    //type="init"的情况:初始化聊天窗口信息
    if (e.type == "init") {
      that.initchats(e.docs[0].records)
    }
    //type="update"的情况:records中增加了一条记录
    else {
      //在chats数组中增加该新消息
      let i = that.data.chats.length
      const new_chats = [...that.data.chats]
      if (e.docs.length)
        new_chats.push(e.docs[0].records[i])
      this.setData({
        chats: new_chats
      })
    }
  },
​
  initchats(records) {
    this.setData({
      chats: records
    })
    //跳转到页面底部
    this.goBottom()
  },
​
  //获取输入文本
  getContent(e) {
    this.data.textInputValue = e.detail.value
  },
​
  sendMsg(){
    let that = this
    //show代表了数据库表中的showTime属性,是否显示消息时间
    var show = false
    //无记录时,true
    if (this.data.chats.length == 0)
      show = true
    //判断上下两条消息的时间差决定是否显示时间,这里设置了2分钟:120000毫秒,可自行修改
    else {
      if (Date.now() - this.data.chats[this.data.chats.length - 1].sendTimeTS > 120000)
        show = true
    }
​
    const _ = db.command
    //消息空白处理
    if (!that.data.textInputValue) {
      wx.showToast({
        title: '不能发送空白信息',
        icon: 'none',
      })
      return
    }
​
    //消息内容赋值
    const doc = {
      openid: that.data.openid,
      msgText: "text",
      textContent: that.data.textInputValue,
      sendTime: util.formatTime(new Date()),
      sendTimeTS: Date.now(),
      showTime: show,
    }
​
    //添加数据库表中该记录的records数组,并跳转页面到底部
    chatroomCollection.where({
        _openid: _.or(_.eq(that.data.openid), _.eq(that.data.match_openid)),
        match_openid: _.or(_.eq(that.data.openid), _.eq(that.data.match_openid))
      })
      .update({
        data: {
          records: _.push(doc)
        }
      })
      .then(res => {
        that.goBottom()
      })
​
    //消息设空
    that.setData({
      textInputValue: ""
    })
  },
​
  goBottom() {
    wx.createSelectorQuery().select('#page').boundingClientRect(function (rect) {
      if (rect) {
        // 使页面滚动到底部
        wx.pageScrollTo({
          scrollTop: rect.height + 4
        })
      }
    }).exec()
  },
})

其中,util.js内容如下:

const formatTime = date => {
  const year = date.getFullYear()
  const month = date.getMonth() + 1
  const day = date.getDate()
  const hour = date.getHours()
  const minute = date.getMinutes()
  const second = date.getSeconds()
​
  return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second].map(formatNumber).join(':')}`
}
​
const formatNumber = n => {
  n = n.toString()
  return n[1] ? n : `0${n}`
}
​
module.exports = {
  formatTime
}
​

一个简单的demo就完成了,大家有什么问题欢迎随时q我。

-----------完结撒花----------

最后一次编辑于  2021-11-18  
点赞 10
收藏
评论

10 个评论

  • Smooth
    Smooth
    2021-11-18

    过程很详细,只不过我想提个问题,就是这个聊天室demo是实时聊天的吗?

    看过程感觉实时聊天每次发消息后,对方都要退出实时聊天页面,获取到文章中说的“要查每条记录中的openid或者match_openid与自身openid是否匹配”,再重新点击对应的对话框进到实时聊天界面,而不能一直呆在我跟他人的对话框页面进行实时对话?

    2021-11-18
    赞同 2
    回复 5
    • ren
      ren
      2021-11-19
      是实时的,因为在客户端这边的程序中,假设你在聊天窗口中,对方发了一条信息,这个时候程序会侦测到数据库表的变化(onChange),就会在chats数组中加一条记录,就能实现实时聊天了。
      2021-11-19
      3
      回复
    • Smooth
      Smooth
      2021-11-19回复ren
      好!那确实棒!
      2021-11-19
      1
      回复
    • ren
      ren
      2021-11-19回复Smooth
      加油加油!
      2021-11-19
      2
      回复
    • 俊
      2022-06-04
      这个可以实时页面刷新吗,我也感觉不行啊
      2022-06-04
      回复
    • ren
      ren
      2022-09-19回复
      如果你一直在这个页面,数据库一更新js那个存聊天信息的数组就会同时更新,所以你一直在这个聊天室里面也可以接收到的;如果你是出了聊天室,一进来是要获取这个数组的那就更可以了
      2022-09-19
      回复
  • Y&K
    Y&K
    2021-11-18

    很不错喔,点个大大的赞

    2021-11-18
    赞同 2
    回复
  • 攻城狮
    攻城狮
    2022-03-27

    请问 小程序切换到后台。请问此时 别人发的消息 我会收到新消息提醒吗(声音提醒)?目前看小程序切换到后台,没法通过声音提醒用户有新消息。

    即使采用给公众号发 模版消息,公众号也只有红点提醒,没有声音提醒

    2022-03-27
    赞同 1
    回复 1
    • ren
      ren
      2022-09-19
      小程序的通知只能通过订阅号或者公众号来通知,如果想要有声音提醒的话,需要使用订阅号进行通知,其中长期订阅比较难申请,短期订阅需要用户每次同意通知才会通知一次,比较不符合实际情况,这就得你实际考虑业务情况了
      2022-09-19
      回复
  • 擎天柱
    擎天柱
    2022-11-25


    重新研究过后发现了一个问题,我想请教您一下:请问e.type是在何处更新的呢?什么时机修改为init,什么时机修改为update呢?谢谢您🌹

    2022-11-25
    赞同
    回复 1
    • ren
      ren
      2023-02-15
      您好,e.type的值是对应这种情况的
      init:在刚进这个页面获取到整个聊天室的信息时,也就是第一次获取聊天的信息,type==init
      update:在这条记录变化时,比如records中新增了一条记录,程序会主动调用onChange函数,从而更新我们的页面的records数据,从而保持数据库的聊天数据和当前页面的聊天数据一致
      2023-02-15
      回复
  • 擎天柱
    擎天柱
    2022-11-25

    谢谢您的解答。还想请教您一下,如果我想扩展发送消息的类型,应该修改哪些部分呢?

    比方说,除了发送文字,我还想支持发送图片,那么可以直接基于这个框架进行修改吗?

    谢谢您,打扰您了🌹

    2022-11-25
    赞同
    回复 1
    • ren
      ren
      2023-02-15
      图片也可以的,每一条聊天数据你可以定义一个type,type为文字就显示成文字<text>,type为图片你就显示图片<image>,其他同理,你可以扩展到语音甚至视频等。
      存储方面,如果type为文字你就保存文字的信息,type为图片时你就需要先存储图片到指定位置(比如各平台的对象存储,能提供文件的存储,并生成url访问地址)然后将这个url存到对应的聊天数据中就可以了
      具体的难点主要是前端的样式问题,通过判断type的不同显示不同的view就可以了,逻辑上这套框架是可以支持的
      2023-02-15
      回复
  • 擎天柱
    擎天柱
    2022-11-19

    您好,我在进入聊天页面过程中,触发以下问题,请问是什么原因呢?如何处理呢?谢谢您,打扰您了🌹

    2022-11-19
    赞同
    回复 2
    • ren
      ren
      2022-11-22
      数据库没有该聊天室的记录吧?所以记录里面没有records字段,就报错了
      2022-11-22
      1
      回复
    • 擎天柱
      擎天柱
      2022-11-25回复ren
      谢谢您,已经完善了records初始化操作,可是在测试对话时,又遇到了以下问题:(请问是什么原因呢?应该如何解决?)
      2022-11-25
      回复
  • Galaxy
    Galaxy
    2022-11-13

    请问下,个人开发者开发这种聊天室小程序提交审核能通过吗

    2022-11-13
    赞同
    回复 1
    • ren
      ren
      2022-11-22
      不能噢,会触及到社交类目,个人类小程序提交通过不了的
      2022-11-22
      回复
  • 蓝莓山妖
    蓝莓山妖
    2022-05-30

    你好,在实践的过程中发现第一次初始化即"init"时候,得到的e.docs[]其实是空的,是不是应该先创建这条记录呢

    2022-05-30
    赞同
    回复 2
    • ren
      ren
      2022-09-19
      你可以多增加一个判断如果e.docs是空的再做相应的处理就好了
      2022-09-19
      回复
    • 擎天柱
      擎天柱
      2022-11-19
      请问您是怎样解决的呢?(不太明白如何初始化e.docs)谢谢
      2022-11-19
      回复
  • 左左酱
    左左酱
    2022-03-28

    你好,这个参与聊天室的人员,可以是直接微信的账号吗?还是需要授权/注册然后生成自己系统的账号?

    我是想,微信账号可以直接参与聊天,不用再注册新的小程序聊天系统的账号。你这个实现是支持的吗?

    2022-03-28
    赞同
    回复 1
    • ren
      ren
      2022-09-19
      不太理解你的意思,直接微信账户应该是不行的,同一个用户每个小程序里面的账号可以说都是唯一的(同一个用户每个小程序的openid不同),但是小程序里面的注册是不需要很复杂的操作的,只需要允许授权就可以了。
      2022-09-19
      回复
  • Lucky dog~
    Lucky dog~
    2022-03-12

    你好 我想问一下 你这做了下拉加载历史消息吗 类似微信的

    2022-03-12
    赞同
    回复 1
    • ren
      ren
      2022-09-19
      这个可以扩展代码实现,我这篇文章里实现的是一下子加载所有聊天信息(消息多了可能会卡顿),你可以先获取聊天的最后20条,然后每次用户上拉到顶部再获取之前的信息以达到下拉加载历史信息的功能
      2022-09-19
      1
      回复
登录 后发表内容