使用Taro小程序框架开发一个学习、作题、刷课、发帖、聊天交流的微信小程序

项目介绍

当代大学生上课缺乏积极性,学习缺少效率。同为大学生的我深有体会。因此特别开发出这样一款学习类的微信小程序帮助学生进行学习、巩固知识,同时增长对战PK模块来增强学生们的学习积极性。这是一个为学生提供在线学习课程、题库练习、考试答题、作题PK、上课签到、资料查阅、成绩分析等功能的微信小程序javascript

但愿大佬们走过路过给个star~css

技术选型

前端:Taro + 微信小程序 + Echarts前端

后端:Node.js + MySql + websocketjava

其余:七牛云存储mysql

项目功能

  1. 在线学习课程
  2. 专项题库练习
  3. 课程考试答题
  4. 知识趣味竞赛
  5. 上课签到系统
  6. 专业资料查阅
  7. 学生成绩分析

运行截图

1. 主页

2. 我的中心

3. 课程详情

4. 作题练习

5. 学习交流群

6. 聊天室

7. 课程列表

8. 习题列表

9. 排行榜

10. 论坛

项目分析

项目采用先后端分离的技术,前端采用了Taro微信小程序框架,由于本人比较喜欢React,因此采用了Taro这款类React语法的框架,后端则采用了Node.js,koa2框架。聊天室页面采用websocket来进行链接git

今天,咱们首先来聊一聊聊天室使用的小技巧(并不)github

首先咱们的后端数据库采用的是mysql,咱们建了一个聊天记录的表(萌新勿喷~)web

1. 后端部分

  • 数据库部分
    咱们将全部的聊天记录存放到一张表上方便管理,由于咱们有多个聊天群组,咱们该如何区分这些不一样的聊天群组呢?答案是,经过room_name来区分,获取聊天记录的时候就直接查询这个群组名便可,这样就不用开不少的表,将不一样的群聊记录存放到不一样的表中啦!

同时由于咱们的聊天记录内须要存储emoji等信息,因此,咱们须要将数据库的字符集调整为utf8mb4 -- UTF-8 Unicode,排序规则选择utf8mb4_unicode_ci,这个能够经过自行百度,或者navicat中设置。sql

而后咱们将数据表以及字段类型也设置为utf8mb4,便于存储emoji信息数据库

  • 后端处理聊天记录的方法。
router.get('/chatlog/:to', async (ctx) => {
  const to = ctx.params.to
  const response = []
  const res = await query(`SELECT * FROM chatlog WHERE room_name = '${to}' ORDER BY current_time DESC`);
  res.map((item, index) => {
    const { room_name, user_name, user_avatar, current_time, message } = item
    response[index] = {
      to: room_name,
      userName: user_name,
      userAvatar: user_avatar,
      currentTime: formatTime(current_time),
      message,
      messageId: `msg${current_time}${Math.ceil(Math.random() * 100)}`
    }
  })
  ctx.response.body = parse(response)
})
复制代码

这是获取指定群聊的后端接口,to表明的是群组名,使用get的方法便可获取到指定群聊的聊天记录啦!

继续聊聊咱们如何为全部链接到聊天室的网友们发送信息,这里咱们采用的是广播的方式,不一样于socket.io内已经封装好广播的方法,小程序规定只能使用websocket,因此我粗略的封装了一下广播(十分丑陋的代码)

let onlineUserSocket = {}
let onlineUserInfo = {}

const handleLogin = (ws, socketMessage) => {
  const { socketId, userName, userAvatar } = socketMessage
  onlineUserSocket[socketId] = ws
  onlineUserInfo[socketId] = { userName, userAvatar }
  ws.socketId = socketId
}

// 广播消息
const broadcast = (message) => {
  const { from, userName } = message
  Object.values(onlineUserSocket).forEach((socket) => {
    socket.send(JSON.stringify({
      ...message,
      isMyself: userName === onlineUserInfo[socket.socketId].userName
    }))
  })
}
复制代码

咱们再登陆的时候,就将前端传来的消息存入对象中,以及他的socket对象,而后广播的时候就能够遍历全部的socket对象,为全部在线用户广播消息,其中的isMyself表明的是否为本人,例如我发的消息,本身的socket对象接受广播的时候就是true。别人的就是false,这样作是为了方便区分,本身的聊天消息和被人的聊天消息


2. 前端部分

接下来聊聊前端的聊天室部分

handleSocketMessage(): void {
    const { socketTask } = this
    socketTask.onMessage(async ({ data }) => {
      const messageInfo: ReceiveMessageInfo = JSON.parse(data)
      const { to, messageId, isMyself, userName, userAvatar, currentTime, message } = messageInfo
      const time: string = formatTime(currentTime)

      this.messageList[to].push({
        ...messageInfo,
        currentTime: time
      })
      /* 设置群组最新消息 */
      this.contactsList.filter(contacts => contacts.contactsId === to)[0].latestMessage = {
        userName, message, currentTime: time
      }
      this.scrollViewId = isMyself ? messageId : ''
      await Taro.request({
        url: 'http://localhost:3000/chatlog',
        method: 'PUT',
        data: {
          to,
          userName,
          userAvatar,
          currentTime,
          message,
        }
      })
    })
  }
复制代码

咱们先接受消息,而后先更新指定群组名的聊天群组的聊天记录,而后再使用PUT的方式访问接口添加聊天记录到数据库中。

能够看到咱们的聊天记录是分为左边以及右边的,本身发的消息即为右边,咱们能够经过简单的flex布局来实现

// 这里是覆盖默认样式,显示本身消息的样式
.myself {
  justify-content: flex-end;

  .avatar {
    order: 1;
  }

  .info {
    display: flex;
    flex-direction: column;
    align-items: flex-end;

    .header {
      justify-content: flex-end;

      .username {
        order: 1;
        margin-right: 0 !important;
        margin-left: .5em;
      }
    }

    .content {
      color: #333 !important;
      border: #e7e7e7 1px solid;
      background: #fff !important;
      box-shadow: 0 8px 20px -8px #d7d7d7;
    }
  }
}

// 如下是默认样式,就是左边的样式
.message-wrap {
  display: flex;
  margin: 20px 0;

  .avatar {
    width: 14vw;
    height: 14vw;
    margin: 10px;
    border-radius: 50%;
    background-image: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%);
  }

  .info {

    .header {
      display: flex;
      align-items: center;
      max-width: 40vw;
      padding: 10px 0;
      color: #666;
      font-size: .8em;

      .username {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        max-width: 40vw;
        margin-right: .5em;
        color: #555;
        font-size: 1.2em;
        font-weight: bold;
      }
    }

    .content {
      display: inline-block;
      max-width: 60vw;
      padding: 10px 20px;
      color: #fff;
      word-break: break-all;
      border-radius: 20px;
      background: #66a6ff;
    }
  }
}
复制代码

最后咱们聊一下websocket的断线重连

handleSocketClose(): void {
    const { socketTask } = this
    socketTask.onClose((msg) => {
      this.socketTask = null
      this.socketReconnect()
      console.log('onClose: ', msg)
    })
  }

  handleSocketError(): void {
    const { socketTask } = this
    socketTask.onError(() => {
      this.socketTask = null
      this.socketReconnect()
      console.log('Error!')
    })
  }
复制代码

咱们这里先监听一下websocket关闭或者异常的状况,调用重连方法,以及清空socketTask的对象,接下来是重连的方法

socketConnect() {
    // 生成随机特有的socketId
    this.generateSocketId()

    /* 使用then的方法才能正确触发onOpen的方法,暂时不知道缘由 */
    Taro.connectSocket({
      url: 'ws://localhost:3000',
    }).then(task => {
      this.socketTask = task
      this.handleSocketOpen()
      this.handleSocketMessage()
      this.handleSocketClose()
      this.handleSocketError()
    })
  }

  socketReconnect(): void {
    this.isReconnected = true
    clearTimeout(this.timer)

    /* 3s延迟重连,减轻压力 */
    this.timer = setTimeout(() => {
      this.socketConnect()
    }, 3000)
  }
复制代码

咱们每三秒调用一遍socket链接的方法,从新再设置好socketId,以及socketTask,从新监听各类方法。这里有一个奇特的地方,就是Taro的connectSocket方法,不能使用async/await的方法来获取socketTask,也就是说不能这样const socketTask = await Taro.connectSocket({...})来获取socketTask,只能经过then的方法才能获取到,卑微的我暂时不知道如何解决这个问题......

聊天界面中有一个emoji表情的按钮,点击就会弹出emoji栏

实现起来比较简单,首先定义一个变量emojiOpened来判断用户是否点击emoji按钮,若点击则为输入栏新增一个类名来控制弹出的样式

<View className={`chat-input-container ${emojiOpened ? 'emoji-open' : ''}`}>
复制代码

同时再scss中设置弹出的样式

.emoji-open {
  transform: translateY(-30vh);
  transition: all .2s ease;
}

...

&-input-container {
    position: fixed;
    left: 0;
    bottom: -30vh;
    width: 100vw;
    height: 40vh;
    background: #fff;
    z-index: 1;
    transition: all .2s ease;
    ...
}
复制代码

具体后续请关注一下个人github,将持续更新项目!

猛戳~

相关文章
相关标签/搜索