上篇文章完成了博客评论区的样式和评论回复功能,接下来基于websocket实现评论的实时展示。
需求: 浏览博客的用户,可以看到评论区的实时评论
思路: 用户打开博客详情后,向后端发送打开的博客id和用户token,后端将这些信息保存到map中用于发送消息和关闭连接。
后端
- 引入依赖,pom.xml中添加
<!--websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
- 添加websocket文件夹,添加以下文件夹和文件
因为一篇文章可以同时被多个用户浏览,所以使用嵌套的map存储被浏览的文章id和真正浏览文章的用户token和对应会话,嵌套map结构如图所示,发送消息只发送给浏览同一篇文章的用户
WebSocketServer.java
/**
* 注意在websocket通信中只能传string
*/
@Component
@ServerEndpoint("/socket/{passageId}/{userToken}")
public class WebSocketServer {
// 存储被浏览的文章id和正在浏览文章的用户token和对应会话
public static final Map<String, Map<String,Session>> sessionMap = new ConcurrentHashMap<>();
public static CommentService commentService;
// 建立连接
/***
* 1.第一层map存储被浏览的文章id和浏览的用户及会话
* 2.第二层map存储用户token及会话
*/
@OnOpen
public void onOpen(Session session, @PathParam("passageId") String passageId,@PathParam("userToken") String userToken) {
if(sessionMap.containsKey(passageId)){
sessionMap.get(passageId).put(userToken,session);
}else {
Map<String,Session> map = new HashMap<>();
map.put(userToken,session);
sessionMap.put(passageId,map);
}
}
//关闭连接
/**
* 1.把登出的用户从sessionMap中剃除
* 2.发送给所有人当前登录人员信息
*/
@OnClose
public void onClose(@PathParam("passageId") String passageId,@PathParam("userToken") String userToken) {
//用户退出文章的浏览,则将该用户及其会话移除
Map<String,Session> passageMap = sessionMap.get(passageId);
passageMap.remove(userToken);
//没有用户浏览该文章,则将文章id从map中移除
if(passageMap.isEmpty()){
sessionMap.remove(passageId);
}
}
/**
* 接收处理客户端发来的数据
*/
@OnMessage
public void onMessage(String message) {
// 解析消息为java对象
Comment msg = JSON.parseObject(message, Comment.class);
if(msg != null&& msg.getCommentContent() != null &&!msg.getCommentContent().isEmpty()){
//将评论内容存入数据库
msg.setCommentTime(new Date());
commentService.insertSelective(msg);
//将评论内容发送给其他正在浏览该文章的用户
sendAllMessage(msg);
}else{
System.out.println("评论内容为空");
}
}
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
// 服务端发送消息给客户端
private void sendAllMessage(Comment message) {
try {
String passageId = Integer.toString(message.getPassageId()) ;
//根据评论发送文章的id,将该评论发送给正在浏览该文章的用户
Map<String,Session> passageMap = sessionMap.get(passageId);
for (Session session : passageMap.values()) {
session.getBasicRemote().sendText(JSON.toJSONString(message));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
CorsConfig .java
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路由
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许证书(cookies)
.allowCredentials(true)
// 设置允许的方法
.allowedMethods("*")
// 跨域允许时间
.maxAge(3600);
}
}
WebSocketConfig.java
@Configuration
public class WebSocketConfig {
/**
* 使用springboot内置tomcat需要该bean,打war包则注释掉该bean
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
//因为websocket中不能直接使用@Autowired注入Service,添加下面配置 在socket引入Service
@Autowired
public void socketUserService(CommentService commentService){
WebSocketServer.commentService = commentService;
}
}
前端
- 打开文章详情时初始化websocket
onMounted(()=>{
getPassageDetail();
getPassageComment();
if(userMsg.userToken){
init();
}
})
//websocket
const info = reactive(new CommentClass());
let socket:any;
const init = () =>{
// 如果sessionStorage中没有用户信息,则跳转登录页面
if (typeof (WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
} else {
console.log("您的浏览器支持WebSocket");
let socketUrl = "ws://localhost:8080/socket/" + passageId+"/"+userMsg.userToken;
if (socket != null) {
socket.close();
socket = null;
}
// 开启一个websocket服务
socket = new WebSocket(socketUrl);
//打开事件
socket.onopen = function () {
console.log("websocket已打开");
};
// 浏览器端收消息,获得从服务端发送过来的文本消息
socket.onmessage = function (msg:any) {
console.log("收到数据====" , msg.data)
let data = JSON.parse(msg.data)
if(!data.rootParentId){
commentList.value.push(data);
}else{
//给被评论的地方添加数据
for(let i=0;i<commentList.value.length;i++){
if(data.rootParentId == commentList.value[i].commentId){
if(commentList.value[i].child != null){
commentList.value[i].child[data.commentId] = data;
}else{
commentList.value[i].child={};
commentList.value[i].child[data.commentId] = data;
}
break;
}
}
}
};
//关闭事件
socket.onclose = function () {
console.log("websocket已关闭");
};
//发生了错误事件
socket.onerror = function () {
console.log("websocket发生了错误");
}
}
}
- 发布评论,分为在文章下发表评论和在评论下回复评论两种情况,都通过websocket向后端发送评论
//发布评论
const publishComment = (rootParentId?:number,parentId?:number)=>{
console.log( rootParentId == undefined && parentId == undefined)
//在文章下发表评论
if(rootParentId == undefined && parentId == undefined){
console.log(commentPublish.commentContent)
if(commentPublish.commentContent == '' ||commentPublish.commentContent == undefined){
ElMessage.warning('评论不能为空');
return
}
commentPublish.userId =userMsg.userId;
commentPublish.userName = userMsg.userName;
commentPublish.passageId = passageId;
if (typeof (WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
} else {
console.log("您的浏览器支持WebSocket");
socket.send(JSON.stringify(commentPublish));
Object.assign(commentPublish,commentEnpty);
}
}
//在评论下回复评论
else{
if(commentReply.commentContent == '' ||commentReply.commentContent == undefined){
ElMessage.warning('评论不能为空');
return
}
if(commentReply.commentContent == ''){
ElMessage.warning('评论不能为空');
return
}
if(rootParentId != undefined){
commentReply.rootParentId = rootParentId;
}
if(parentId != undefined){
commentReply.parentId = parentId;
}
commentReply.userId =userMsg.userId;
commentReply.userName = userMsg.userName;
commentReply.passageId = passageId;
if (typeof (WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
} else {
console.log("您的浏览器支持WebSocket");
socket.send(JSON.stringify(commentReply));
Object.assign(commentReply,commentEnpty);
replyInput.value = -1;
}
}
}
至此,评论区实时展示就完成了,实现效果如下
本文参考