有一个项目,前端vue,后端springboot。现在需要做一个功能:用户在使用系统的时候,管理员发布公告,则使用系统的用户可以看到该公告。
基于此,一个简单的方案:前端使用JS方法setInterval,重复调用后端公告获取接口。此方法有几点缺陷:
因此就想到了,如果后端主动向前端推送消息,这个问题就可以完美解决。
有两种方案可以实现后端向前端推送消息:
这里介绍SSE的方式(如果系统中对这种消息的准确性和可靠性有严格的要求,则使用websocket,websocket的使用相对复杂的多);
如果想了解SSE的详细基础知识,可以参考阮一峰老师的这篇文章:Server-Sent Events 教程
SpringMVC中,已经集成了该功能,所以无需额外引入jar包,直接上代码:
@RestController
@RequestMapping("/notice")
public class NoticeController {
@Autowired
private NoticeService noticeService;
@GetMapping(path = "createSseEmitter")
public SseEmitter createSseEmitter(String id) {
return noticeService.createSseEmitter(id);
}
@PostMapping(path = "sendMsg")
public boolean sendMsg(String id, String content) {
noticeService.sendMsg(id, content);
return true;
}
}
@Slf4j
@Service
public class NoticeServiceImpl implements NoticeService {
@Autowired
@Qualifier("sseEmitterCacheService")
private CacheService<SseEmitter> sseEmitterCacheService;
@Override
public SseEmitter createSseEmitter(String clientId) {
if (StringUtil.isBlank(clientId)) {
clientId = UUID.randomUUID().toString().replace("-", "");
}
SseEmitter sseEmitter = sseEmitterCacheService.getCache(clientId);
log.info("获取SSE,id={}", clientId);
final String id = clientId;
sseEmitter.onCompletion(() -> {
log.info("SSE已完成,关闭连接 id={}", id);
sseEmitterCacheService.deleteCache(id);
});
return sseEmitter;
}
@Override
public void sendMsg(String clientId, String content) {
if (sseEmitterCacheService.hasCache(clientId)) {
SseEmitter sseEmitter = sseEmitterCacheService.getCache(clientId);
try {
sseEmitter.send(content);
} catch (IOException e) {
log.error("发送消息失败:{}", e.getMessage(), e);
throw new BusinessRuntimeExcepption(CustomExcetionConstant.IO_ERR, "发送消息失败", e);
}
} else {
log.error("SSE对象不存在");
throw new BusinessRuntimeExcepption("SSE对象不存在");
}
}
}
这里,只列出了核心的代码,简而言之,需要做到两点即可:
为什么要这么做?看下文,后端代码一起来分析就明白了。
由于,我请求该接口,需要带上token,所以直接使用EventSource不行,另外这个IE也不支持。所以选择了一个工具:event-source-polyfill。
npm install event-source-polyfill
import { EventSourcePolyfill } from "event-source-polyfill";
created() {
let _this = this;
this.source = new EventSourcePolyfill(
"/" +
process.env.VUE_APP_MANAGER_PRE_API_URL +
"/notice/createSseEmitter?id=" +
uuid(),
{
headers: {
[process.env.VUE_APP_OAUTH_AUTHORIZATION]: store.getters.getToken,
},
//重连时间间隔,单位:毫秒,默认45000毫秒,这里设置为10分钟
heartbeatTimeout: 10 * 60 * 1000,
}
);
this.source.onopen = () => {
console.log("NOTICE建立连接");
};
this.source.onmessage = (e) => {
_this.scrollMessage = e.data;
console.log("NOTICE接收到消息");
};
this.source.onerror = (e) => {
if (e.readyState == EventSource.CLOSED) {
console.log("NOTICE连接关闭");
} else if (this.source.readyState == EventSource.CONNECTING) {
console.log("NOTICE正在重连");
//重新设置header
this.source.headers = {
[process.env.VUE_APP_OAUTH_AUTHORIZATION]: store.getters.getToken,
};
} else {
console.log(e);
}
};
},
有几点说明:
前端配置了代理,所以一直收不到后端发送的消息,尝试加入以下参数:
devServer: {
compress:false,
…………
}
之前在写后端的时候提到了两个问题:为什么要返回SseEmitter对象?为什么要缓存SseEmitter对象?
其实看过SSE的原理,都应该明白:这就是一个长连接,前端调用创建SseEmitter对象的接口,虽然接口返回了,但是并未结束(这就是为什么要返回SseEmitter对象,如果返回的是一个其他对象,就和普通的接口没两样了, 该接口就直接结束了),请看下截图:
发起请求之后,一直是待处理,并未结束,10分钟之后,该请求被取消(前端设置的重连),然后重新发起连接,重新发起的连接也是在等待中。只有接收到消息后,这个请求的状态码才是200,但是这个时候才连接已经建立好了。其中的细节,这里不做讲述。
所以,如果再使用SseEmitter对象发送消息,则前端就可以收到对象的消息了(即实现后端向前端发送消息)。这里使用的SseEmitter对象,就是createSseEmitter接口返回的对象(也就是使用哪个SseEmitter对象,就可以向哪个前端发送消息)。这也就是为什么要缓存SseEmitter对象的原因了。
通过调用发送消息接口,前端即可立即展示发送的消息: