共计 4701 个字符,预计需要花费 12 分钟才能阅读完成。
项目介绍
当代大学生上课缺少积极性,学习缺乏效率。同为大学生的我深有体会。所以特别开发出这样一款学习类的微信小程序帮助学生进行学习、巩固知识,同时增加对战 PK 模块来加强学生们的学习积极性。这是一个为学生提供在线学习课程、题库练习、考试答题、做题 PK、上课签到、资料查阅、成绩分析等功能的微信小程序
希望大佬们走过路过给个 star~
技术选型
前端:Taro + 微信小程序 + Echarts
后端:Node.js + MySql + websocket
其他:七牛云存储
项目功能
- 在线学习课程
- 专项题库练习
- 课程考试答题
- 知识趣味竞赛
- 上课签到系统
- 专业资料查阅
- 学生成绩分析
运行截图
1. 主页
2. 个人中心
3. 课程详情
4. 做题练习
5. 学习交流群
6. 聊天室
7. 课程列表
8. 习题列表
9. 排行榜
10. 论坛
项目分析
项目采用前后端分离的技术,前端采用了 Taro 微信小程序框架,因为本人比较喜欢 React,所以采用了 Taro 这款类 React 语法的框架,后端则采用了 Node.js,koa2 框架。聊天室页面采用 websocket 来进行连接
今天,我们首先来聊一聊聊天室使用的小技巧(并不)
首先我们的后端数据库采用的是 mysql,我们建了一个聊天记录的表(萌新勿喷~)
1. 后端部分
- 数据库部分
我们将所有的聊天记录存放到一张表上方便管理,因为我们有多个聊天群组,我们该如何区分这些不同的聊天群组呢?答案是,通过 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
,这样做是为了方便区分,自己的聊天消息和被人的聊天消息
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 的方法才能获取到,卑微的我暂时不知道如何解决这个问题 ……
具体后续请关注一下我的 github,将持续更新项目!
猛戳~