✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🎯 你正在阅读「Java项目-轻聊」系列文章 🎯
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🔥 弹简特 个人主页
❄️ 个人专栏直通车:
✨ 靠热爱去书写自己,靠勇敢去书写生活!
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🌟 博主简介:

文章目录:
- 一、前言
- 二、准备
- 三、获取某会话中最后一条历史消息
- 四、获取会话历史消息
-
- 1、约定前后端交互接口
- 2、一张图理清楚业务逻辑
- 3、后端实现
-
- [3.1 持久层实现](#3.1 持久层实现)
- [3.2 服务层实现](#3.2 服务层实现)
- [3.3 控制层实现](#3.3 控制层实现)
- 4、前端实现
-
- [3.1 参数会话id在哪里](#3.1 参数会话id在哪里)
- [3.2 哪些地方需要获取历史会话信息](#3.2 哪些地方需要获取历史会话信息)
- [3.3 前端获取历史信息](#3.3 前端获取历史信息)
- [3.4 点击会话列表调用历史信息](#3.4 点击会话列表调用历史信息)
- [3.5 点击人员列表到会话列表调用历史信息](#3.5 点击人员列表到会话列表调用历史信息)
- 5、测试
一、前言
我们轻聊项目最核心的部分就是我们本期所介绍的消息管理模块了,我们的消息管理中会介绍:客户端获取历史消息,客户端发送消息,接受消息,服务器使用WebSocket推送消息。
而由于内容稍微偏多些,我们会慢慢一篇一篇的介绍对应的内容,本期我们就先介绍客户端获取历史消息
二、准备
1、消息怎么传输
所谓的消息传输,它的本质是张三把消息通过服务器转发给李四,它有一个中转的过程

🤔:那为什么张三不直接发给李四,为什么非要通过服务器去发呢?非要通过服务器中转呢?
答:这件事情的万恶之源在于ipv4的地址不太够用,无法做到给每个设备都分配一个唯一的IP。它能表示的数据最多就是42亿9,000万所以要给全世界每个设备都分配一个ip地址是做不到的。
那么此时解决这个问题的办法有三个:
- 第1个是动态ip的分配:就是上网的就分配,不上网就不分配
- 第2个是NAT 机制:我们把ip分成内网ip和外网ip,一个外网ip可以代表若干个内网ip
- 第3个是ipv6。
而在当前的一个背景下,我们使用的是第2种方法,使用nat机制,用一个比较稀缺的外网ip来代表一大波的内网ip。
所以此时很多个设备共用同一个外网ip,这样我们所消耗的ip数量就大大降低。
但是正是在这样的nat机制下,导致我们上述张三必须得通过服务器来中转发给李四,为啥张三不直接发给李四,因为张三根本就访问不了李四的设备。
张三李四都是普通的客户端,所以他们的设备大概率是没有外网ip的,只有内网ip。而如果没有外网ip,只有内网ip的话,则不能被直接访问到了,这种情况是由nat机制导致的,他就是这样。
除非张三和李四在同一个局域网里,否则他们两个无法直接进行通信。
它本质上就是我们的ipv4不够用,然后我们引入一个nat,而这个nat 却导致了:我们的内网和外网之间出现了一道鸿沟,即你内网可以直接访问外网,但是外网无法直接访问内网的设备。
所以你现在想要两个内网设备之间通信咋办?就得以一个大家都能够访问的,带有万网ip的设备作为桥梁,作为一个中转站。虽然张三不能直接访问的李四,但是张三可以访问服务器,李四不能直接访问张三,但是李四可以访问服务器。那么张三和服务器建立一个连接,李四和服务器也建立一个连接。那么服务器就可以同时和张三李四分别进行通信。此时张三有啥话要对李四说,张三就会通过服务器进行转发,转达给李四。
一方面:
之所以要通过服务器进行中转,就是因为ipv4不够用。然后我们又引入nat机制nat它本身就会导致这个问题:
使用服务器中转,最大原因就是nat背景下两个内网的设备无法直接进行通讯。[但是前提是不在同一个局域网内,在同一个局域网内的话就可以通信了,我们不用管。因为在同一个局域网内,连同一个路由器,就没必要那么麻烦。]
另一方面:
原因是通过服务器中转,是更容易在服务器这里记录历史消息,后续随时方便,我们来查询历史记录。
2、数据库设计
如果我们上述所说,我们两个人之间发消息,我们就得在服务器这里边使用数据库进行保存消息。那此时就得进行数据库设计,设计需要哪些表?
回顾一下,我们这里边的消息和我们前边的会话模块是相关的,在讲相关会话模块的时候,谈到了三个实体,用户实体,会话实体和消息实体。
我们的会话是一个中间的媒介,用户和会话是一个多对多的关系。
会话和消息是一个一对多的关系【一个会话里有多个消息,然后一个消息呢,只能从属于一个会话,是隶属关系】


然后咱们前面已经有了会话表message_session了。
所以呢,这里边我们的消息表只需要让他的每个记录带有会话表的ID,毕竟一对多的关系是在多的一方加入我们的隶属关系(外键加载多的一方),把隶属关系加入到多的一方中,代表我这个多隶属于哪个一的一方。
比如:
message_session表(1的一方)
| 主键 | 最后时间 |
|---|---|
| 1 | '2000-01-01 00:00:00' |
那么我们的消息表(多的一方)
message
| 主键 | 会话id |
|---|---|
| 10 | 1 |
| 11 | 1 |
那么我们就可以设计这个消息表中有哪些字段
| 名称 | 描述 |
|---|---|
| messageId | 消息id |
| formId | 就是消息是谁发的,那个用户是谁? |
| sessionid | 然后呢消息发给哪个会话就是这个消息隶属于哪个会话 |
| context | 消息的正文存储消息的内容 |
| posTime | 消息的发送时间,比如我们的消息列表是按照消息的时间顺序来排序的 |
注意这里的关系,不是说消息从一个用户发给另一个用户,而是消息从一个会话发给另一个会话。
其实一个会话里可以有多个用户,它不只是两个用户。那你谁在群里发一个消息,大家在群里都能看到。
我们不需要有一个目标ID这种,我们只需要关注这个消息是谁发的就行了,然后会话里面所有人就都能看到。
三、获取某会话中最后一条历史消息
还记得我们之前在学习获取会话列表的时候,每一个会话的最后一条消息都是一样的,我们自己构造的数据


那么现在我们有了消息表之后,会话中的最后一条数据我们就可以实时查询出来了:
持久层:

java
@Mapper
public interface MessageMapper {
/**
* 根据会话 ID 查询最后一条消息
* @param sessionId 会话id
* @return 返回最后一条消息
*/
String getLastMessageBySessionId(Integer sessionId);
}

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhongge.web_chatroom.dao.mapper.MessageMapper">
<select id="getLastMessageBySessionId" resultType="java.lang.String">
<!-- 原生SQL语句,MyBatis会解析后交给数据库执行 -->
select content
from message -- 操作message聊天消息表
where sessionId = #{sessionId} -- 条件:匹配会话ID
<!-- #{sessionId}:MyBatis占位符,预编译参数
底层生成PreparedStatement,使用?占位,自动防SQL注入,推荐使用;
参数名sessionId对应Mapper接口方法入参名称
-->
order by postTime desc -- 根据消息发送时间postTime倒序,最新消息排在最前面
limit 1 -- 只取第一条数据,也就是该会话最新的一条消息
</select>
</mapper>
服务层代码
java
@Override // 重写接口抽象方法
public List<MessageSession> getSessionList(int userId) {
// 定义最终要返回给前端的会话列表
List<MessageSession> messageSessionList = new ArrayList<>();
// 边界校验:用户ID不合法,直接返回空列表
if (userId <= 0) {
System.out.println("用户ID不合法,无法查询会话列表");
return messageSessionList;
}
// 1. 调用Mapper:查询当前用户关联的所有会话ID集合(按最后聊天时间倒序)
List<Integer> sessionIdList = messageSessionMapper.getSessionIdByUserId(userId);
/**
* 使用LinkedHashMap:
* 使用这个数据结构可以实现去重处理
* 1. 实现按【好友ID】去重:同一好友只保留一条会话
* 2. 保留插入顺序,维持数据库查询的时间排序
* key:好友ID value:对应会话实体
*/
Map<Integer, MessageSession> sessionByFriendId = new LinkedHashMap<>();
// 遍历每一个会话ID,逐个封装会话数据
for (int sessionId : sessionIdList) {
// 2. 根据会话ID + 当前用户ID,查询会话内的对方好友信息
// 根据会话id 将我们会话表中除了用户自己的好友id给查询出来
// 但是你放心,你查询出的结果一个会话只会有一个好友,因为你是单聊
List<Friend> friends = messageSessionMapper.getFriendsBySessionId(sessionId, userId);
// 容错:会话无有效聊天对象,跳过当前会话
if (friends == null || friends.isEmpty()) {
continue;
}
// 取第一个好友(你也只能取出第一个好友,因为你是单聊,那么你这个会话中就只有一个好友)
int friendId = friends.get(0).getFriendId();
// 组装会话实体对象
MessageSession messageSession = new MessageSession();
messageSession.setSessionId(sessionId); // 设置会话ID
messageSession.setFriends(friends); // 绑定好友信息(昵称、头像、ID)
// TODO 3. (已实现)查询该会话的最后一条消息内容(目前随机设置一个,到时候处理消息模块的时候就需要设置具体值)
// messageSession.setLastMessage("你好,晚上我们一起去吃火锅吧~");
// 3. 查询该会话的最后一条消息内容
String lastMessage = messageMapper.getLastMessageBySessionId(sessionId);
// 三目运算:无消息则赋值空字符串,避免前端null报错
messageSession.setLastMessage(lastMessage == null ? "" : lastMessage);
// 根据好友ID查找Map中是否已存在该好友的会话(这一步的作用就是用于去重的)
MessageSession existing = sessionByFriendId.get(friendId);
if (existing == null) {
// 该好友首次出现,直接存入Map
sessionByFriendId.put(friendId, messageSession);
} else {
// 如果这个好友已经存在在我们的列表中的话,那么我们就会使用最新的数据进行覆盖
// 他的场景就是,你创建出了多个会话id的时候,我们怎么办的问题(多考虑特殊情况)
// 该好友存在多条重复会话,调用方法筛选出最优会话进行覆盖
sessionByFriendId.put(friendId, pickBetterSession(existing, messageSession));
}
}
// 将去重、筛选完成后的会话集合转为List,作为最终结果
// sessionByFriendId.values() 我们会有多个会话,那么每个会话中都会有一个好友
// 所以此时我们sessionByFriendId集合中势必会有多个好友的信息,那么sessionByFriendId.values()就是将所有的key对应的会话信息返回(包括好友信息)
messageSessionList.addAll(sessionByFriendId.values());
return messageSessionList;
}
测试

由图可知,我们出现了报错

解决: 注入对象

结果: 此时运行结果正确

四、获取会话历史消息
1、约定前后端交互接口
| 项 | 内容 |
|---|---|
| URL | GET /message?sessionId={sessionId} |
| 是否需登录 | 否(前端在登录后调用) |
成功响应 (JSON 数组,时间升序,最多 100 条)
json
[
{
"messageId": 1,
"fromId": 1,
"fromName": "张三",
"sessionId": 1,
"content": "晚上吃什么",
"avatarPath": "/avatars/xxx.jpg"
}
]
注意:获取历史消息我们虽然说不需要先登录,但是其实他隐含的是先登录,为什么呢?
因为我们消息和会话之间的关系是:一个会话中,会有多个消息,一个消息只能隶属于一个会话,那么就可以根据会话id查询出这个会话中的所有消息,所以你的条件是有会话id,而这个会话id是在用户登录之后获取用户会话信息的时候获得的,此时就说明,要有会话id,就务必要先登录。
2、一张图理清楚业务逻辑

3、后端实现
3.1 持久层实现
Java和数据库的映射实体类
java
import lombok.Data; // Lombok
@Data // 聊天消息 message 表实体,兼历史消息接口返回体
public class Message {
private Integer messageId; // 消息主键
private Integer fromId; // 发送者 userId
private String fromName; // 发送者用户名(查询时 join user 表得到)
private Integer sessionId; // 所属会话 ID
private String content; // 消息正文
private String avatarPath; // 发送者头像路径,渲染气泡旁头像
}
此处我们持久层MessageMapper接口上述已经创建好了,这里我们就写接口和sql映射文件
java
/**
* 进入会话拉取历史(最多 100 条)
* @param sessionId 会话id
* @return 返回历史消息
*/
List<Message> getHistoryMessageBySessionId(Integer sessionId);
sql映射文件:
xml
<!--
查询指定会话的历史聊天消息
功能:根据会话ID,联查消息表+用户表,获取发送人昵称、头像,最多返回最新100条记录
id:对应Mapper接口中 getHistoryMessageBySessionId 方法名
resultType:返回数据封装到 Message 实体类 com.zhongge.web_chatroom.dao.dataobject.Message
-->
<select id="getHistoryMessageBySessionId" resultType="com.zhongge.web_chatroom.dao.dataobject.Message">
<!--
查询字段说明:
message.messageId 消息主键ID
message.fromId 发送者用户ID
user.username as fromName 发送人用户名,别名fromName映射实体属性
message.sessionId 所属聊天会话ID
message.content 消息文本内容
user.avatar_path as avatarPath 发送人头像路径,别名avatarPath映射实体属性
-->
select message.messageId, message.fromId, user.username as fromName,
message.sessionId, message.content, user.avatar_path as avatarPath
<!-- 关联两张表:消息表message、用户表user -->
from message, user
<!-- 关联条件:用户表userId = 消息发送者fromId,关联出该条消息发送人的信息 -->
where user.userId = message.fromId
<!-- 业务条件:只查询传入sessionId对应会话的消息 -->
and message.sessionId = #{sessionId}
<!-- #{sessionId} MyBatis预编译占位符,接收接口传入的会话ID参数,防SQL注入 -->
<!-- 根据消息发送时间倒序,最新消息排在前面 -->
order by postTime desc
<!-- 限制最多查询100条,避免一次性加载海量消息造成页面/数据库压力 -->
limit 100
</select>
3.2 服务层实现

接口:
java
public interface MessageService {
// 按会话 ID 获取历史消息(时间正序)
List<Message> getHistoryMessages(Integer sessionId);
}
实现类:

java
@Service // 注册为 Spring 管理的聊天消息业务服务 Bean
public class MessageServiceImpl implements MessageService { // 聊天消息业务服务实现类
private final MessageMapper messageMapper; // 消息数据访问层
// 构造注入消息 Mapper
public MessageServiceImpl(MessageMapper messageMapper) {
this.messageMapper = messageMapper; // 注入消息 Mapper
}
@Override // 实现获取历史消息
public List<Message> getHistoryMessages(Integer sessionId) {
if (sessionId == null) { // 会话 ID 为空
return new ArrayList<>(); // 返回空列表
}
List<Message> messages = messageMapper.getHistoryMessageBySessionId(sessionId); // 按会话 ID 查询消息(数据库倒序)
Collections.reverse(messages); // 反转为时间正序,便于前端按时间线展示
return messages; // 返回历史消息列表
}
}
3.3 控制层实现

java
@RestController // 消息模块 REST 入口
public class MessageController { // 提供某会话下的历史聊天记录
@Resource // 注入消息业务服务
private MessageService messageService; // 从数据库按 sessionId 查询消息列表
@GetMapping("/message") // 根据会话 ID 获取历史消息(进入聊天页时加载)
public Object getMessage(Integer sessionId) { // sessionId 为两人会话在库中的主键
return messageService.getHistoryMessages(sessionId); // 按时间排序返回消息 DTO 列表
}
}
4、前端实现
现在对于前端来说:什么时候去获取历史消息,以及怎么渲染的问题,对于渲染所需要的样式我们之前就已经借助AI完成对应UI的美化。
3.1 参数会话id在哪里
我们之前创建会话的时候,创建成功会返回有会话id,那么我们之前是将他存在标签属性中的

还有我们之前获取会话列表信息返回有会话id,我们将他存在了标签属性中

你要获取哪一个会话中的所有聊天消息,那么你就得提供会话id,而我们的会话id恰恰在之前我们就保存下来了,所以此处我们便可以直接获取会话了。
3.2 哪些地方需要获取历史会话信息
-
人员列表点击的时候,跳到会话列表,并自动打开消息框,加载聊天信息


-
会话列表中,点击对应的会话的时候,就会打开消息框,加载聊天信息

3.3 前端获取历史信息
js
// GET 历史消息并渲染右侧聊天气泡
/**
* 根据会话ID加载该会话全部历史聊天记录,渲染到右侧聊天窗口
* @param {number|string} sessionId 聊天会话唯一标识
*/
function getHistoryMessage(sessionId) {
// 1. 获取存放聊天记录的容器DOM(右侧消息展示区域)
let messageShowDiv = document.querySelector('.message-show');
// 清空容器原有消息,切换会话时清除上一轮聊天记录
messageShowDiv.innerHTML = '';
// 2. 获取右侧顶部对方昵称+头像标题栏DOM
let titleDiv = document.querySelector('.right .title');
// 先清空标题栏原有内容
titleDiv.innerHTML = '';
// 3. 获取左侧会话列表中当前被选中的会话li标签(带selected选中样式)
let selectedLi = document.querySelector('#session-list .selected');
// 判断是否存在选中会话(防止未选中会话时报错)
if (selectedLi) {
// 从选中li的自定义属性读取好友ID
let friendId = selectedLi.getAttribute('data-friend-id');
// 从选中li的自定义属性读取好友头像文件路径
let avatarPath = selectedLi.getAttribute('data-avatar-path');
// 调用工具方法,获取当前会话展示名称(单聊=好友名)
let name = getSessionDisplayName(selectedLi);
// 给标题栏追加样式类,用于显示头像布局
titleDiv.className = 'title title-with-avatar';
// 拼接标题栏HTML:头像标签 + 转义后的好友昵称
// avatarImgHtml:生成头像图片DOM片段
// escapeHtml:对昵称做HTML转义,防止XSS注入、特殊符号渲染错乱
titleDiv.innerHTML = avatarImgHtml(friendId, 'avatar-md', avatarPath)
+ '<span class="title-name">' + escapeHtml(name) + '</span>';
}
// 4. 发送AJAX GET请求,后端查询该会话所有历史消息
$.ajax({
// 请求方式 GET
method: 'get',
// 请求地址,拼接当前会话ID作为请求参数
url: '/message?sessionId=' + sessionId,
// 请求成功回调函数,body为后端返回的消息数组
success: function(body) {
// 遍历后端返回的每一条聊天消息对象
for (let message of body) {
// 调用封装方法,单条消息生成聊天气泡并追加到消息容器
addMessage(messageShowDiv, message);
}
// 渲染完所有消息后,自动滚动到容器最底部,展示最新一条消息
scrollBottom(messageShowDiv);
}
});
}
/**
* 在消息展示容器中新增一条聊天气泡,自动区分自己发送(右侧气泡)、他人发送(左侧气泡)
* @param {HTMLElement} messageShowDiv 存放所有聊天消息的父容器DOM
* @param {Object} message 单条消息数据对象,包含 fromId、fromName、content、avatarPath 等字段
*/
function addMessage(messageShowDiv, message) {
// 创建最外层消息块div,单条消息整体容器
let messageDiv = document.createElement('div');
// 获取当前登录用户的用户名(工具函数,全局保存的登录账号)
let selfUser = getCurrentUsername();
// 判断这条消息是不是当前登录用户自己发的:发送者用户名 == 登录用户名
let isSelf = selfUser === message.fromName;
// 动态设置样式类
// message-right:本人消息靠右展示
// message-left:别人消息靠左展示
messageDiv.className = isSelf ? 'message message-right' : 'message message-left';
// 拼接气泡内部HTML:包裹消息文本,使用escapeHtml转义防止XSS攻击
let bubbleHtml = '<div class="bubble"><p class="bubble-text">'
+ escapeHtml(message.content) + '</p></div>';
// 调用工具函数生成头像img标签,参数:发送人ID、小尺寸头像样式、头像图片地址
let avatarPart = avatarImgHtml(message.fromId, 'avatar-sm', message.avatarPath);
// 布局区分:
// 自己发的消息:气泡在前,头像在后(靠右布局,文字左头像右)
if (isSelf) {
messageDiv.innerHTML = bubbleHtml + avatarPart;
} else {
// 他人消息:头像在前,气泡在后(靠左布局,头像左文字右)
messageDiv.innerHTML = avatarPart + bubbleHtml;
}
// 将组装完成的单条消息DOM,追加到消息列表容器中渲染到页面
messageShowDiv.appendChild(messageDiv);
}
/**
* 让指定容器滚动条自动滑到最底部,聊天打开新消息时自动定位最新消息
* @param {HTMLElement} elem 需要滚动的消息容器DOM对象
*/
function scrollBottom(elem) {
// offsetHeight:元素可视区域高度(不含滚动隐藏部分,固定显示区域大小)
let clientHeight = elem.offsetHeight;
// scrollHeight:容器内全部内容的总高度,包含超出可视区域、需要滚动才能看到的内容
let scrollHeight = elem.scrollHeight;
// 滚动到垂直最底部:总内容高度 - 可视高度 = 最大可向下滚动距离
elem.scrollTo(0, scrollHeight - clientHeight);
}
/**
* 获取当前登录用户的用户名,用来区分消息是自己发的还是别人发的,控制气泡左右展示
* @returns {string} 当前登录用户名;找不到用户名称元素时返回空字符串
*/
function getCurrentUsername() {
// 选择器定位页面左侧个人信息区域的用户名DOM元素
// 层级:主容器.main → 左侧侧边栏.left → 用户模块.user → 用户名称标签.user-name
let nameEl = document.querySelector('.main .left .user .user-name');
// 三元判断:如果元素存在,取出内部纯文本作为用户名;不存在则返回空字符串
// 做判空防止nameEl为null时调用textContent抛出JS空指针报错
return nameEl ? nameEl.textContent : '';
}
逻辑:
text
选中会话
│
├─ 清空消息区 + 标题栏
├─ 若有选中项 → 渲染对方头像/昵称
├─ 请求 GET /message?sessionId
│ │
│ └─ 成功 → 逐条 addMessage
│ ├─ 取当前用户名
│ ├─ 判断左/右气泡
│ └─ 追加到页面
└─ scrollBottom → 滚到最新消息
那么这个历史消息由谁来调用呢?
我们说了有两个:
- 点击会话列表-->调用历史信息
- 点击人员列表-->自动选中会话列表-->调用历史信息
3.4 点击会话列表调用历史信息

找到我们之前的clickSession方法变为如下:
js
/**
* 点击左侧会话列表项的事件处理函数
* 功能:切换选中状态、展示聊天面板、清空未读消息、加载历史聊天记录
* @param {HTMLElement} currentLi 当前点击的会话li元素
*/
function clickSession(currentLi) {
// TODO:后续先处理你点击的会话是否是好友请求
// 这里需要判断当前点击的会话是否是好友请求类型
// 如果是好友请求,需要特殊处理显示不同的界面和逻辑
// TODO:后续展示右侧聊天面板(已实现)
// 需要根据点击的会话展示对应的聊天面板
// 可能包括聊天窗口、消息输入区域等功能组件
showChatPanel();
//筛选出所有普通会话项(排除好友请求条目)
let allLis = document.querySelectorAll('#session-list>li:not(.friend-request-item)');
// 统一处理会话选中样式:取消其他项选中、给当前项添加选中样式
activeSession(allLis, currentLi);
// 调用activeSession函数处理会话的选中状态
// 参数:所有普通会话项列表和当前点击的会话项
// 实现功能:取消其他会话的选中状态,同时给当前点击的会话添加选中样式
// TODO:获取当前会话唯一ID,后续用去请求会话中的历史消息(已实现)
let sessionId = currentLi.getAttribute("message-session-id");
// 需要从currentLi元素中提取会话的唯一标识符
// 这个ID将用于后续获取该会话的历史消息记录
// 用户主动点击会话-->未读清零-->角标消失
// TODO:后续实现点击出现角标的样式
// 需要实现点击会话时显示未读消息角标的功能
// 可能包括角标的显示/隐藏逻辑、位置计算等
// TODO:后续实现根据会话ID请求并加载历史聊天消息(已实现)
// 需要实现通过会话ID向后端API发送请求
// 获取并加载该会话的历史聊天消息到聊天面板中
// 可能包括消息的分页加载、滚动定位等功能
getHistoryMessage(sessionId);
}
3.5 点击人员列表到会话列表调用历史信息

找到我们之前的``代码完善如下:
java
/**
* 好友列表中点击好友触发事件
* 逻辑:打开已有会话 / 新建会话,会话自动置顶并切换到会话标签页
* @param {Object} friend 好友对象,包含 friendId、friendName、avatarPath 等信息
*/
// 好友 Tab 点击好友:打开已有会话或创建新会话
function clickFriend(friend) {
//TODO 后续在消息模块中:先展示消息的聊天面板(已实现)
showChatPanel();
// 先根据好友ID、好友名称,在会话列表中查找对应的会话li元素
// 优先按ID查找,找不到再按昵称匹配
let sessionLi = findSessionByFriendId(friend.friendId) || findSessionByName(friend.friendName);
// 获取整个会话列表容器
let sessionListUL = document.querySelector('#session-list');
// 分支1:该好友已有会话记录
if (sessionLi) {
// 将当前会话项移动到普通会话区域最顶部
// getFirstChatSessionLi:获取第一个普通会话节点(跳过顶部好友请求项)
sessionListUL.insertBefore(sessionLi, getFirstChatSessionLi(sessionListUL));
// 主动触发点击事件,执行选中会话、清空未读、加载历史消息逻辑
sessionLi.click();
}
// 分支2:暂无对应会话,需要新建会话
else {
// 创建空的会话li节点,作为临时占位项
sessionLi = document.createElement('li');
// 绑定好友ID,用于后续识别
sessionLi.setAttribute('data-friend-id', friend.friendId);
// 存在头像路径则绑定头像属性
if (friend.avatarPath) {
sessionLi.setAttribute('data-avatar-path', friend.avatarPath);
}
// 拼接会话项HTML,消息预览传空字符串(新建会话暂无消息)
sessionLi.innerHTML = buildSessionLiHtml(friend.friendId, friend.friendName, '', friend.avatarPath);
// 将新建的占位会话插入到普通会话最顶部
sessionListUL.insertBefore(sessionLi, getFirstChatSessionLi(sessionListUL));
// 新会话绑定点击事件
sessionLi.onclick = function() {
clickSession(sessionLi);
};
// TODO 后续实现 为先触发点击,打开聊天界面,提升交互体验(已实现)
sessionLi.click();
// 调用方法向后端请求创建会话,成功后回填会话ID
createSession(friend.friendId, sessionLi);
}
// 自动切换页面标签,从【好友】Tab 切回【会话】Tab
document.querySelector('.tab .tab-session').click();
}
5、测试
登录之后,点击会话列表,显示消息框,并加载历史消息渲染

点击好友列表-》选中好友-》点击好友李四-》跳到会话列表(置顶)-》自动选择和李四的会话-》打开消息聊天框并加载历史消息渲染


本期搞定【获取历史消息】🎉!🚀。
下期开始介绍消息推送用到的协议:WebSocket协议🖥️!
干货持续更新,记得点赞👍关注🌟收藏⭐,追更不迷路~