大家好,又见面了,我是你们的朋友全栈君
本项目小程序端采用Taro技术框架,可将React代码编译为微信小程序、安卓APP、IOS程序、H5页面等,管理端采用React Hook + TypeScript来进行开发
当代大学生上课缺少积极性,学习缺乏效率。同为大学生的我深有体会。所以特别开发出这样一款学习类的微信小程序帮助学生进行学习、巩固知识,同时增加对战PK模块来加强学生们的学习积极性。这是一个为学生提供在线学习课程、题库练习、考试答题、做题PK、上课签到、资料查阅、成绩分析等功能的微信小程序!
目前因学业任务比较重,没有好好的完善,目前小程序端比较完善的只有习题,课程,论坛,聊天室。管理端也开始进行开发了,现在完成了题库管理,新增题库,修改题库以及登录的功能
希望大佬们走过路过可以给个star鼓励一下感激不尽
https://github.com/zhcxk1998/School-Partners
这个是小程序后台管理端的介绍文章 后台管理端介绍文章,使劲戳!
前端:Taro + 微信小程序 + Echarts
后端:Node.js + MySql + websocket
其他:七牛云存储
管理端
项目采用前后端分离的技术,前端采用了Taro微信小程序框架,因为本人比较喜欢React,所以采用了Taro这款类React语法的框架,后端则采用了Node.js,koa2框架。聊天室页面采用websocket来进行连接
今天,我们首先来聊一聊聊天室使用的小技巧(并不)
首先我们的后端数据库采用的是mysql,我们建了一个聊天记录的表(萌新勿喷~)
我们将所有的聊天记录存放到一张表上方便管理,因为我们有多个聊天群组,我们该如何区分这些不同的聊天群组呢?答案是,通过room_name来区分,获取聊天记录的时候就直接查询这个群组名即可,这样就不用开很多的表,将不同的群聊记录存放到不同的表中啦!
同时因为我们的聊天记录内需要存储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;
...
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。