本项目小程序端采用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; ... }