智析:我是怎么把大屏点亮的
大四的上学期过得比想象中要快,广商的图书馆到了晚上依然很安静。这几天刚把毕业设计的开题报告交上去,电脑屏幕上还挂着IDE的深色界面。回想这几年的大学生活,拿了两次国奖,GPA刷到了4.02,也顺手拿了几个国二的竞赛奖项。很多人问我怎么做到的,其实哪有什么捷径,无非就是坐在电脑前,把一个又一个报错解决掉,把一个又一个项目硬生生给折腾出来。
今天想记录一下之前做的一个项目——“智析”通用可视化大屏系统。现在回过头看,这算是我大学阶段折腾得最久、踩坑最多,但也最有成就感的一个东西。
做这个项目的初衷其实挺简单的。之前打比赛或者接外包的时候,发现很多甲方或者非技术背景的同学,对“数据大屏”有一种近乎执念的偏爱。但他们完全不懂代码,每次要改个图表颜色、换个数据源,都得找程序员重新发版。我就想,能不能搞个零代码的配置平台,像搭积木一样,拖拖拽拽就能让不懂技术的人搭出一个好看的大屏?
技术选型上我没怎么纠结,直接上了最稳妥的组合:后端Spring Boot,前端Vue3加上ECharts。不搞花里胡哨的新框架,因为我知道这个项目的核心难点不在于用了多前沿的技术,而在于繁杂的业务逻辑和细节处理。
刚开始写代码的时候,我以为最难的是后端的动态多数据源接入。毕竟要同时支持MySQL、PostgreSQL甚至Excel文件的解析,还得动态生成SQL去抽数据。但写了几天发现,后端这些东西虽然繁琐,但逻辑是线性的,只要把设计模式套好,接口一层层写下来,无非是体力活。
真正让我头疼的,是前端的渲染,尤其是ECharts这个让人又爱又恨的库。
“零代码”听起来很美好,对用户来说是零代码,意味着对开发者来说是“全代码”。ECharts的配置项实在太多了,简直像一本厚厚的字典。为了让用户能在界面上调整图表的每一个细节,我必须在前端写一套极其庞大的表单,然后把表单数据实时映射到ECharts的option对象里。
折腾柱状图、折线图这些基础图表还好,但遇到散点图、雷达图或者带地图坐标的图表时,数据结构的转换就非常恶心了。不仅要考虑图表本身的颜色、字体、图例位置,还要考虑不同分辨率下这些元素的相对大小。有时候用户在配置面板里把标题字号设成了24px,在预览区看着挺好,结果全屏一放,字小得像蚂蚁;或者坐标轴的标签稍微长一点,就会被容器直接截断。那段时间,我每天都在跟grid.left、grid.containLabel这些参数死磕,一点点调优默认配置,试图找出一个在大多数情况下都不至于“穿模”的参数组合。
但调参只是开胃菜,真正的大坑是屏幕适配。
我平时写代码用的是一台14寸的笔记本,分辨率是常规的比例。但大屏系统最终的宿命,往往是挂在甲方会议室那块1920*1080甚至拼接出来的带鱼屏上。
一开始我用了最传统的rem方案,监听屏幕宽度,动态计算根节点的font-size。结果发现这招对普通的DOM元素管用,对ECharts却很吃力。因为ECharts底层是Canvas绘制的,很多尺寸依赖于容器的绝对像素,虽然可以调用chart.resize(),但图表内部的间距、边框粗细在缩放时经常会失真,整个排版会有种说不出的廉价感。
后来我在网上翻了大量的资料,也看了几个开源大屏的源码,最终决定推翻重来,采用CSS vw/vh 配合 transform: scale 的缩放方案。
具体的做法是这样的:我把大屏的画布设计尺寸固定死,比如就是1920*1080。在开发和预览的时候,外层套一个容器,利用当前浏览器窗口的实际宽高,除以设计稿的宽高,得出一个缩放比例scaleRatio。然后直接用CSS的transform: scale(scaleRatio)把整个画布等比例缩放。为了解决缩放后可能出现的白边问题,外层容器再辅以vw和vh进行居中或者铺满处理。
这个方案写出来也就几十行代码,但在它跑通的那一刻,我真的长舒了一口气。因为不管用户把浏览器窗口拉成什么奇葩形状,里面的图表布局、字体大小、甚至ECharts的Canvas渲染,都像是一张被整体缩放的高清图片,再也没有出现过错位和挤压的问题。这种用最底层的CSS属性优雅解决复杂渲染问题的感觉,真的很让人上瘾。
前端的壳子搭好了,接下来就是让数据“活”起来。大屏系统如果没有实时数据,那充其量就是个PPT。
为了实现数据的实时推送,我引入了WebSocket。用户可以在配置项里勾选“开启实时刷新”,后端就会定时把新数据推过来。由于是通用系统,后端的定时任务和连接管理我写得比较小心,用了一个并发安全的Map来维护所有活跃的Session。
本以为这就完事了,结果在本地模拟弱网环境测试的时候,出了一个非常隐蔽的Bug。
当我拔掉网线再插上,模拟网络断开重连的过程时,前端页面的折线图突然出现了“时光倒流”的诡异现象——折线图的后几个数据点突然掉到了前面,整个图形乱作一团。
我盯着浏览器的Network面板排查了一下午,才弄明白是怎么回事。原来在网络不稳定的情况下,WebSocket虽然有断线重连机制,但底层TCP协议在处理拥塞或者重传时,可能会导致消息到达前端的时间和后端发出的时间不一致。更要命的是,后端在重连瞬间,可能会把积压在消息队列里的旧数据和刚产生的新数据一股脑儿推过来。前端如果只是傻傻地按接收顺序去push数据,就会导致乱序。
为了解决这个问题,我给后端的每一条推送消息都加上了一个timestamp和一个递增的sequence_id。前端在接收到消息时,不再直接渲染,而是先扔进一个优先级队列里做一个极短时间的缓冲,并且记录下当前已经渲染到的最大序列号。如果新来的消息序列号小于当前序列号,说明是迟到的旧数据,直接丢弃;如果是正常顺序,再推进图表的数据集里。
加上这层防抖和保序逻辑后,图表终于稳定了。无论网络怎么抖动,折线图都只会稳稳地向右延伸。
整个项目断断续续写了快两个月。记得把所有功能串联起来,进行最终测试的那天晚上,已经是凌晨一点多了。
我把笔记本接在实验室的一块闲置显示器上,在系统里拖拽了几个组件:中间放了一个带有3D效果的全国地图,两边配上柱状图、雷达图和几个滚动的数据列表。配置好本地数据库的连接,写了几句简单的SQL,然后勾选了WebSocket实时推送。
保存,点击“大屏预览”。
按下F11全屏的那一瞬间,整个显示器暗了下来,紧接着,深蓝色的科技风背景铺满屏幕。地图上的光标开始闪烁,两边的数据面板像呼吸一样,随着后端的推送一帧一帧地平滑跳动。没有任何错位,没有任何报错,一切都在按照我写下的逻辑安静地运转着。
当时实验室里只有机箱风扇的嗡嗡声。我靠在椅背上,看着屏幕上流动的数据,心里并没有那种想要跳起来欢呼的冲动,反而是一种非常平静的满足感。
就像是一块一块亲手打磨的齿轮,历经各种卡壳、返工,最后终于严丝合缝地扣在了一起,随着主发条的转动,发出了均匀而精准的滴答声。
大学这几年,为了保持绩点,为了拿国奖,确实花了很多时间在理论课和应试上。但在我心里,真正让我觉得自己是个“软件工程”学生的,其实就是这些在深夜里折腾项目的时刻。从面对未知技术的迷茫,到被一个个Bug折磨得头昏脑涨,再到最后找出解决方案,看着程序完美运行。
这个过程很累,但当你看到屏幕点亮的那一刻,你会觉得,之前所有的折腾,都值了。