基于HTML5的WebSCADA报表
基于 html5 的 web scada 报表
背景
最近在一个 scada 项目中遇到了在 web 页面中展示设备报表的需求。一个完整的报表,一般包含了筛选操作区、表格、chart、展板等多种元素,而其中的数据表格是最常用的控件。在以往的工业项目中,所有的表格看起来千篇一律,就是通过数字和简单的背景颜色变化来展示相关信息。但是现在通过各种移动 app 和 web 应用的熏陶,人们的审美和要求都在不断提高,尤其是在 web 项目中,还采用老式的数字表格确实也有点落伍了。
如何选择一个合适的 html 前端表格控件?此处可以省略一万字。哈哈。jquery、angular、react 等阵营中的控件库中都有不少成熟案例,但是这些基于 dom 的控件也有不足,一个是效率问题:如果在数据量很大表格的中采用自定义的单元格控件,对的负担实在太重,尤其是移动端。另一个问题是开发效率,上述的控件库中各自的封装程度、接口形式都有所不同,但整体上还是要求开发者对 css、js 都有较深的了解。还有控件的复用、嵌入、发布、移植,也都是问题。
基于上面的考虑,最后采用了基于 canvas 的 ht。通过 ht 表格控件的自定义渲染接口,以及web worker 的多线程数据模拟,实现的表格控件效果如下:
开始
首先我们要做的就是结合业务逻辑,对表格中不同列的数据,进行不同的渲染。例如设备历史信息中的运行时间、停机时间等,比较适合用饼图来汇总展示,用户就可以很直观的从列表上对比出设备的历史状况。
我们来看看这一步是怎样实现的。
ht 有自己的 datamodel 数据模型,省略了我们对数据状态管理、时间派发、ui刷新的开发工作。datamodel 容器中的子元素 data,即是 ht 中最基础的数据结构,可以映射到不同的ui控件上。在画布上,data 可以展示成矢量、图片或者文字等,在树形控件上,data 展示为树的一个节点。在表格当中每个 data 对应着表格中的一行 row。
也就是表格控件自身包含一个 datamodel,在绘制时,将这个 model 中的每个 data 都绘制成一行。
不同的列,展示的是该 data 中的不同属性。例如我们可以把设备的停机时间,保存到 data 的 stopping 属性。
在配置表格的列 column 信息时,我们可以指定该列的表头描述“停机时间”,其数据单元格对应 data 的 stopping 属性,以及自定义绘制格式:
{ name: 'stopping', //对应的data属性 accesstype: 'attr', align: 'center', color: '#e2e2e2', //文字颜色 displayname: '停机', //表头描述 drawcell: pagetable.getdrawlegend('stopping','#e2e2e2') },
自定义渲染
在单元格的基本显示格式中,已经默认提供了文本、数组、颜色等类型,可以自动的对数据格式化,并展示为文字或背景颜色等,但是还未满足我们的个性需求,因此就要将column中 的 drawcell 重载为自定义的渲染函数。
drawcell 的参数:function (g, data, selected, column, x, y, w, h, view),
其中 g 是 canvas 的环境信息,data 是该行的数据体,我们根据这些信息,再利用 html5 原生的 canvas api 就可以画出想要的效果。
懒得亲自直接用 html5 的原生接口? ht 提供了对 canvas api 的封装接口,包括各种矢量类型以及一些简单的 chart。利用该功能,可以轻松组合出复杂的效果。
先创建一个对象,该 image 矢量对象负责包含对组合矢量的描述信息,然后将该 image 对象以及 drawcell 的上下文信息,作为参数传入 ht.default.drawstretchimage 函数,即可实现自定义绘制。
//drawcell function (g, data, selected, column, x, y, w, h, tableview) { var value = data.a(attr); var image = { width: 60, height: 30, comps: [ { type: 'rect', rect: [11,11,8,8], borderwidth: 1, bordercolor: '#34495e', background: legendcolor, depth: 3 }, { type: 'text', text: value, rect:[30, 0, 30, 30], align: 'left', color: '#eee', font: 'bold 12px arial' } ]}; ht.default.drawstretchimage(g, image, 'centeruniform', x, y, w, h); }
因为有多个 legend 图例显示的列,所以我们可以简单包装一下,用了一个 getdrawlegend 函数,参数是该列图例的颜色及 data 属性名称,返回值是 drawcell 函数。
getdrawlegend: function(attr,legendcolor){return drawcell}
至此,我们就完成了启停时间这几列的自定义绘制:
“统计”列的饼图,实际上更简单。还是利用 ht 的矢量接口,把上述几项时间数据传入饼图矢量结构即可。
var values = [data.a('running'),data.a('stopping'),data.a('overhauling')]; var image = { width: 200, height: 200, comps: [ { type: 'piechart', rect: [20,20, 150, 150], hollow: false, label: false, labelcolor: 'white', shadow: true, shadowcolor: 'rgba(0, 0, 0, 0.8)', values: values, startangle: math.pi, colors: piecolors } ] };
其他列的渲染过程大同小异。在“风速”列中,我们可以根据风速大小计算一个颜色透明值,来实现同一色系的映射变换,比原来那种非红即绿的报警表,看起来更舒服一些。在“可用率”列,用 rect 的不同长度变化,来模拟进度条的效果。在功率曲线中稍微有点不同,因为想实现曲线覆盖区域的颜色渐变,在 ht 的 linechart 中没有找到相关接口,所以直接采用了 canvas 绘制。
为了运行效率考虑,在表格的单元格中绘制 chart,应该追求简洁大方,一目了然。这几个 legend 图例小矩形,其实是应该画在表头的。我为了偷懒,就画在了单元格,导致画面显得有点乱。
web worker
众所周知,浏览器的 js 环境是基于单进程的,在页面元素较多,而且有很大运算需求的情况下,会导致无法兼顾渲染任务和计算任务,造成页面卡顿或失去响应。在这种情况,可以考虑使用web worker 的多线程,来分担一些计算任务。
web worker 是 html5 的多线程 api,和我们原来传统概念中的多线程开发有所不同。web worker 的线程之间,没有内存共享的概念,所有信息交互都采用 message 的异步传递。这样多线程之间无法访问对方的上下文,也无法访问对方的成员变量及函数,也不存在互斥锁等概念。在消息中传递的数据,也是通过值传递,而不是地址传递。
在 demo 中,我们利用 web worker 作为模拟后端,产生虚拟数据。并采用前端分页的方式,从 worker 获取当前页显示条目的相关数据。
在主线程中,创建 web worker注册消息监听函数。
worker = new worker("worker.js"); worker.addeventlistener('message', function(e) { //收到worker的消息后,刷新表格 pagetable.update(e.data); }); pagetable.request = function(){ //向worker发送分页数据请求 worker.postmessage({ pageindex: pagetable.getpageindex(), pagerowsize: pagetable.getpagerowsize() }); }; pagetable.request();
本处的new worker创建,对于主线程来说是异步的,等加载完 worker.js,并完成初始化后,该 worker 才是真正可用状态。我们不需要考虑 worker 的可用状态,可以在创建语句后直接发送消息。在完成初始化之前向其发送的请求,都会自动保存在主线程的临时消息队列中,等 worker 创建完成,这些信息会转移到 worker 的正式消息队列。
在 worker 中,创造虚拟随机数据,监听主线程消息,并返回其指定的数据。
self.addeventlistener('message', function(e) { var pageinfo = getpageinfo(e.data.pageindex, e.data.pagerowsize); self.postmessage(pageinfo); }, false);
由于前面提到的无法内存共享,web worker 无法操作 dom,也不适用于与主线程进行大数据量频繁的交互。那么在生产环境中,web worker 能发挥什么作用?在我们这种应用场景,web worker 适合在后台进行数据清洗,可以对从后端取到的设备历史数据进行插值计算、格式转换等操作,再配合上 ht 的前端分页,就能实现大量数据的无压力展示。
分页
传统上有后端分页和前端分页,我们可以根据实际项目的数据量、网速、等因素综合考虑。
采用后端分页的话,可以简化前端架构。缺点是换页时会有延迟,用户体验不好。而且在高并发的情况下,频繁的历史数据查询会对后端数据库造成很大压力。
采用前端分页,需要担心的是数据量。整表的数据量太大,会造成第一次获取时的加载太慢,前端资源占用过多。
在本项目中,得益于给力的 golden 实时数据库,我们可以放心的采用前端分页。历史数据插值、统计等操作可以在数据库层完成,传递到前端的是初步精简后的数据。在数千台设备的历史查询中,得到的数据量完全可以一次发送,再由前端分页展示。
在某些应用场景,我们会在表格中显示一些实时数据,这些数据是必须是动态获取的。类似在 demo 中的趋势刷新效果,我们可以在创建表格时批量获取所有历史数据,然后再动态向数据库获取当前页所需的实时数据。如果网速实在不理想,也可以先只获取第一页的历史数据,随后在后台线程慢慢接收完整数据。
这样的架构实现了海量数据的快速加载,换页操作毫无延迟,当前页面元素实时动态刷新的最终效果。
还有一些传统客户,喜欢在一张完整的大表上进行数据筛选、排序等操作。
我们可以把 demo 中的数据总量改成一万条,单页数量也是一万条,进行测试:
出乎意料的是,ht 面对上万数据量的复杂表格,轻松经受住了考验。页面的滚动、点击等交互毫无影响,动态刷新没有延迟,表格加载、排序等操作时,会有小的卡顿,在可接受的程度之内。当然也跟客户端的机器配置有关。可以想象,几万个 chart的展示以及动态刷新,对于基于dom的控件几乎是件无法完成的任务。
后记
如前文所述,我们基于 ht 的表格实现了海量数据的可定制展现,并取得了令人满意的效果。以下是一些还可以改进的地方。
在 demo 中,通过对 ht 表格控件的 drawcell 进行重载,实现了自定义渲染,然后把这些 drawcell 放到了 pagetable 的原型函数中,以供 column 调用。实际上,更好的办法应该是把这些常见的 chart、图例封装到 column 的基本类型中,那样在配置表格 column 列时,可以指定 type 为 piechart 或 linechart 即可,不需再自行绘制相关矢量。
对于这些表格中的 chart,也可以增加一些交互接口,例如可以增加单元格 tooltip 的自定义渲染功能,在鼠标停留时浮出一个信息量更大的 chart,可以对指定设备进行更深入的了解。
界面美观优化。对 ht 的控件进行颜色定制,可以通过相关接口进行配置:
var tableheader = pagetable._tablepane.gettableheader(); tableheader.getview().style.backgroundcolor = 'rgba(51,51,51,1)'; tableheader.setcolumnlinecolor('#777'); var tableview = pagetable._tablepane.gettableview(); tableview.setselectbackground('#3d5d73'); tableview.setrowlinecolor('#222941'); tableview.setcolumnlinevisible(false); tableview.setrowheight(30);
今后也可以对htconfig进行全局配置,在单独文件中进行样式的整体管理,实现外观样式与功能的分离,有助于工程管理。