本項目小程序端採用Taro技術框架,可將React代碼編譯爲微信小程序、安卓APP、IOS程序、H5頁面等,管理端採用React Hook + TypeScript來進行開發
當代大學生上課缺乏積極性,學習缺少效率。同爲大學生的我深有體會。因此特別開發出這樣一款學習類的微信小程序幫助學生進行學習、鞏固知識,同時增長對戰PK模塊來增強學生們的學習積極性。這是一個爲學生提供在線學習課程、題庫練習、考試答題、作題PK、上課簽到、資料查閱、成績分析等功能的微信小程序javascript
但願大佬們走過路過給個star~
https://github.com/zhcxk1998/School-Partnerscss
目前因學業任務比較重,沒有好好的完善,目前小程序端比較完善的只有習題,課程,論壇,聊天室。管理端也開始進行開發了,如今完成了題庫管理,新增題庫,修改題庫以及登陸的功能
http://cdn.algbb.cn/School-Pa...
前端:Taro + 微信小程序 + Echarts前端
後端:Node.js + MySql + websocketjava
其餘:七牛雲存儲mysql
小程序端
管理端
項目採用先後端分離的技術,前端採用了Taro微信小程序框架,由於本人比較喜歡React,因此採用了Taro這款類React語法的框架,後端則採用了Node.js,koa2框架。聊天室頁面採用websocket來進行鏈接git
今天,咱們首先來聊一聊聊天室使用的小技巧(並不)github
首先咱們的後端數據庫採用的是mysql,咱們建了一個聊天記錄的表(萌新勿噴~)web
咱們將全部的聊天記錄存放到一張表上方便管理,由於咱們有多個聊天羣組,咱們該如何區分這些不一樣的聊天羣組呢?答案是,經過room_name來區分,獲取聊天記錄的時候就直接查詢這個羣組名便可,這樣就不用開不少的表,將不一樣的羣聊記錄存放到不一樣的表中啦!sql
同時由於咱們的聊天記錄內須要存儲emoji等信息,因此,咱們須要將數據庫的字符集調整爲utf8mb4 -- UTF-8 Unicode
,排序規則選擇utf8mb4_unicode_ci
,這個能夠經過自行百度,或者navicat中設置。數據庫
而後咱們將數據表以及字段類型也設置爲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
,這樣作是爲了方便區分,本身的聊天消息和被人的聊天消息
接下來聊聊前端的聊天室部分
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; ... }