最近比较忙,一直没分享什么有意思的内容。刚好前两天在老项目中,遇到一个问题,今天分享一下解决方案:)
是一个业务统计汇总记录功能,由于使用年限长了,系统数据也比较多了,导致该页面功能打开特别卡(加载计算数据太久了)。
并且有大量java计算逻辑和计算使用的上下文数据在后台,并不能简单拆分为多个http请求。再加上老系统大改造的话工期也不划算。
于是就想到了使用SSE进行后台逐行推送。由于老项目JF也是老版本,里面并没有 JF 的 SseEmitter.java 类,好在该类是独立的,可以从JF高版本中拷贝过来直接用。
1、老项目的渲染方式是数据与模板在同一个Controller的Action方法中完成的,这样就导致数据在计算时,浏览器是空白加载状态。这个问题 就把代码拆分一下,增加一个event参数,先直接渲染html,把表格数据使用SSE二次推送过来。代码简例:
Controller{
myAction {
String event = get("event", "view");
if ("data".equals(event)) {
MyService.ME.data(new SseEmitter(getResponse()));
renderNull();
return;
}
render("xxx.html");
}
}2、Service代码中每处理一块数据时,就向前端发送一段数据, 并且可以增加进度条字段。代码简例:
MyService{
data(SseEmitter sse){
//查出需要处理的数据量
List<Record> recordList = Db.find(...);
HandlerMap handlerMap = xxx;//业务逻辑处理器
int count = 0;
int totalCount = recordList.size();
for (Record r : recordList) {
count ++;
r.set("COUNT", count);
//业务处理器
handlerFn(handlerMap, r);
//向浏览器发送数据(一行一渲染)
sse.sendMessage(Kv.by("event", "record").set("data", r)
.set("totalCount", count + "/" + totalCount));
}
//发送汇总数据
sse.sendMessage(Kv.by("event", "sum").set("data", handlerMap.sumData())
.set("totalCount", totalCount));
//结束
sse.complete();
}
}3、html之前是在后端渲染的数据,现在我改为了使用前端JS模板+返回json进行渲染(项目是layui)。当然也可以让后端渲染html片段进行推送到前端,进行充填也是可以的。代码简例:
html...
<查询项代码省略。。。>
<table>
<thead>
<tr>
<th>标题代码省略了</th>
。。。
</tr>
</thead>
<tbody id="table_body">
<tr><td colspan="N" onclick="event_data_fn()">点击查询按钮后显示列表数据、导出按钮</td></tr>
</tbody>
</table>
<script type="text/html" id="tpl_table_tr">
<tr>
<td>{{d.COUNT}}</td>
字段内容代码省略。。。
</tr>
</script>
<script>
function event_data_fn() {
layui.use('laytpl', function(){
var tpl_table_tr = layui.laytpl($('#tpl_table_tr').html());
var table_body = $("#table_body");
table_body.html("");
省略进度条和汇总等字段内容的清理代码...
// 重点看 SSE 接收数据的代码:
// 流式加载表格数据
var param = $("#searchForm").serialize();
const eventSource = new EventSource('/xxx/myAction?event=data&' + param);
eventSource.addEventListener('message', function (event) {
var kv = JSON.parse(event.data);
console.log('kv=', kv);
if(kv.event === 'record'){
table_body.append(tpl_table_tr.render(kv.data));
// 进度条
totalCount.text(kv.totalCount);
}else if(kv.event === 'sum'){
省略进度条汇总数据字段的展示处理。。。
// 主动关闭事件源
eventSource.close();
}
});
eventSource.addEventListener('error', function (event) {
eventSource.close();
});
});
}
</script>可以看到前端是使用 EventSource 类进行接收处理的,使用方式也很简单。
PS:也可以使用 @microsoft/fetch-event-source 是微软开发的 JavaScript 库,用于在浏览器和 Node.js 环境中实现 Server-Sent Events (SSE) 通信。它基于 Fetch API 构建,提供了比原生 EventSource更强大的功能,适用于需要自定义请求头、POST 方法、复杂错误处理等场景。(@bensharp 反馈)
4、线上系统是使用nginx的。所以路径需要配置一下设置为 http_version 1.1 以及关闭缓存功能。让java推一次,浏览器就立刻收到一次JS执行起来。不然nginx给缓存起来,攒一起后再给浏览器,就显得卡卡的失去了流式加载的效果。代码简例:
# 和自己项目功能路径对应上
location /xxx/myAction {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 60s;
proxy_buffering off;
proxy_cache off;
# 更换为对应容器端口
proxy_pass http://127.0.0.1:1011;
}好了分享完毕~

可以作为一个参考,适合各种流式加载的业务处理方案。