最近比较忙,一直没分享什么有意思的内容。刚好前两天在老项目中,遇到一个问题,今天分享一下解决方案:)
是一个业务统计汇总记录功能,由于使用年限长了,系统数据也比较多了,导致该页面功能打开特别卡(加载计算数据太久了)。
并且有大量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 类进行接收处理的,使用方式也很简单。
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; }
好了分享完毕~
可以作为一个参考,适合各种流式加载的业务处理方案。