使用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,將持續更新項目!

猛戳~

相關文章
相關標籤/搜索