在老旧的工控机上绘制实时数据波形图,常常会遇到浏览器卡顿、画面撕裂、内存不断上涨等问题。工控场景里的浏览器版本往往偏低,机器性能也无法和普通开发机相比,因此选择一个轻量、兼容的方案至关重要。jQuery配合Canvas,正好处于性能和开发效率的平衡点上:jQuery负责简化DOM操作与事件绑定,Canvas负责图形的底层绘制,两者结合完全可以满足多数设备的数据可视化需求。

老旧工控机的硬件与浏览器约束
工控机并不等同于普通办公电脑,很多现场设备使用的是多年前的嵌入式主板,CPU主频只有1GHz左右,内存可能仅有2GB,甚至还在使用机械硬盘。操作系统往往是Windows XP或Windows 7精简版,浏览器则停留在IE8、IE9或IE11兼容模式。这类环境有两个直接影响绘图方案的特点:一是Canvas API在高版本IE中才被完整支持,因此代码不能依赖太新的JavaScript语法;二是CPU算力有限,如果每一帧都做大量重复计算或频繁操作DOM,画面刷新率会明显下降。
针对这种场景,开发时应该尽量避免使用ES6以上的语法,以及诸如箭头函数、Promise、fetch等运行时支持不完整的新特性。jQuery 1.x系列在旧版IE中的兼容性最好,选择它作为基础库比使用Vue、React等框架要稳妥得多。Canvas绘图本身是命令式操作,不依赖框架的响应式机制,因此在jQuery初始化完成后,可以直接把绘制逻辑绑定到画布上。
另外一个容易被忽略的因素是显示分辨率。工控机常常连接着老旧VGA显示器,分辨率不高,但缩放比例异常,导致Canvas在高DPI屏幕上显示模糊。如果为了追求清晰而启用设备像素比缩放,反而会增加绘制负担。在性能吃紧的设备上,建议放弃高清适配,直接按CSS像素设置Canvas尺寸,换取更稳定的帧率。
Canvas绘图基础与jQuery的配合方式
Canvas本身只是一个位图画板,所有图形都需要通过JavaScript调用绘图上下文来完成。先看一个最基础的HTML结构,画布宽度设置为800像素,高度设置为300像素:
<canvas id="waveCanvas" width="800" height="300">
当前浏览器不支持Canvas
</canvas>
在jQuery中获取Canvas的原生DOM对象,需要用到数组下标访问,因为jQuery对象是类数组结构,$("#waveCanvas")[0]返回的就是原生节点。获取绘图上下文的方法是调用getContext("2d"),之后所有绘制操作都基于这个上下文对象。以下代码展示了初始化网格线的过程:
$(function() {
var canvas = $("#waveCanvas")[0];
if (!canvas || !canvas.getContext) {
return;
}
var ctx = canvas.getContext("2d");
// 绘制网格线
ctx.strokeStyle = "#d0d0d0";
ctx.lineWidth = 1;
ctx.beginPath();
for (var i = 0; i < canvas.width; i += 40) {
ctx.moveTo(i, 0);
ctx.lineTo(i, canvas.height);
}
for (var j = 0; j < canvas.height; j += 40) {
ctx.moveTo(0, j);
ctx.lineTo(canvas.width, j);
}
ctx.stroke();
});
这段代码通过循环画出垂直和水平方向的网格线,形成一种坐标系参考。这里有一个值得注意的细节:多次调用moveTo和lineTo之后,只调用一次stroke就能把所有线条绘制出来。这种将路径合并的思路在波形图绘制中同样适用,可以大幅减少绘制状态切换带来的性能损耗。
实时波形绘制的核心实现
波形图本质上是一条不断更新的折线。实时数据从串口、PLC或传感器采集模块中不断传入网页,前端需要把这些数据点保存下来,再以固定频率重绘画布。如果每一帧都重新保存全部数据,内存占用会持续增长,最终导致浏览器崩溃。最常用的办法是采用循环缓冲结构,数据量到达上限后,新的数据覆盖最旧的数据,保证缓冲区大小恒定。
下面是一个完整的实时波形绘制代码示例。它使用jQuery初始化,内部维护一个长度为240的数组作为数据缓冲,每帧模拟产生一个新数据点,随后清空画布、绘制网格、再绘制波形曲线。为了兼容旧浏览器,动画循环使用requestAnimationFrame,并提供了setTimeout降级方案:
$(function() {
var canvas = $("#waveCanvas")[0];
if (!canvas || !canvas.getContext) {
return;
}
var ctx = canvas.getContext("2d");
var dataBuffer = [];
var maxPoints = 240;
// 兼容旧浏览器的requestAnimationFrame
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function(callback) {
return setTimeout(function() {
callback(Date.now());
}, 1000 / 60);
};
}
// 向缓冲区加入一个数据点
function pushData(value) {
if (dataBuffer.length >= maxPoints) {
dataBuffer.shift();
}
dataBuffer.push(value);
}
// 绘制网格
function drawGrid() {
ctx.strokeStyle = "#e0e0e0";
ctx.lineWidth = 1;
ctx.beginPath();
for (var i = 0; i < canvas.width; i += 40) {
ctx.moveTo(i, 0);
ctx.lineTo(i, canvas.height);
}
for (var j = 0; j < canvas.height; j += 40) {
ctx.moveTo(0, j);
ctx.lineTo(canvas.width, j);
}
ctx.stroke();
}
// 绘制波形
function drawWave() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawGrid();
if (dataBuffer.length < 2) {
return;
}
ctx.strokeStyle = "#0066cc";
ctx.lineWidth = 2;
ctx.lineJoin = "round";
ctx.beginPath();
var step = canvas.width / (maxPoints - 1);
for (var index = 0; index < dataBuffer.length; index++) {
var value = dataBuffer[index];
var x = index * step;
var y = canvas.height - (value / 100) * canvas.height;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
}
// 模拟产生实时数据
function getSensorValue() {
var base = 50;
var noise = Math.random() * 40 - 20;
return Math.max(0, Math.min(100, base + noise));
}
// 动画刷新循环
function tick() {
pushData(getSensorValue());
drawWave();
window.requestAnimationFrame(tick);
}
tick();
});
这段代码的绘制策略是每帧清空整个画布再重绘,虽然简单直接,但在数据量小、画面元素少时效率尚可。波形曲线不会出现拖影,网格线也始终保持清晰。数据缓冲区使用shift方法移除最早的数据点,虽然数组移位在数据量大时会有开销,但对于240个点来说完全不是问题,而且这种方法比使用链表更简单直观。
实际接入真实数据源时,只需要把getSensorValue函数替换为读取全局变量或调用WebSocket回调即可。例如,在WebSocket的onmessage事件里调用pushData,而动画循环只负责绘制,两者解耦后代码更容易维护。
性能优化思路:离屏Canvas与绘制状态合并
上面这个版本在性能偏弱的工控机上可能只能跑出30帧左右,原因是网格和波形每帧都重新绘制,包含大量重复计算。针对这种情况,最有效的优化手段是使用离屏Canvas缓存静态内容。网格、边框、坐标文字这类不常变化的元素,只需要绘制一次,然后每帧通过drawImage快速将整个位图拷贝到主画布上。这种方式把每帧的绘制工作量从几十条路径线段减少为一次图像拷贝。
var offCanvas = document.createElement("canvas");
offCanvas.width = canvas.width;
offCanvas.height = canvas.height;
var offCtx = offCanvas.getContext("2d");
function buildStaticLayer() {
offCtx.strokeStyle = "#e0e0e0";
offCtx.lineWidth = 1;
offCtx.beginPath();
for (var i = 0; i < offCanvas.width; i += 40) {
offCtx.moveTo(i, 0);
offCtx.lineTo(i, offCanvas.height);
}
for (var j = 0; j < offCanvas.height; j += 40) {
offCtx.moveTo(0, j);
offCtx.lineTo(offCanvas.width, j);
}
offCtx.stroke();
}
初始化时调用buildStaticLayer生成静态网格层,之后每帧只需要两行关键代码:先调用ctx.drawImage(offCanvas, 0, 0)贴出网格,再绘制波形曲线。这样一来,原先重复执行的循环只执行一次,CPU占用率随之下降。数据显示这个优化在低端设备上通常能把帧率提升到50帧以上。
另一个容易忽略的优化是减少绘图状态的切换。Canvas内部是一个状态机,每次修改strokeStyle、lineWidth、fillStyle都可能触发底层重新编译着色器或刷新状态缓存。因此,相同样式的线段应该尽量合并到同一个路径中。例如,绘制多条波形时,可以先把所有曲线的路径都画完,再统一调用一次stroke,而不是每画一条曲线就切换一次颜色。此外,如果曲线颜色不变,可以在初始化时设置好样式,绘制循环里完全不再赋值。
IE等旧浏览器环境下的兼容性注意
老旧的工控机浏览器往往不支持Canvas的某些高级特性,例如路径2D、阴影、图像平滑控制等。开发时最好只使用最基础的API,包括beginPath、moveTo、lineTo、stroke、fill、clearRect、drawImage。这些方法在IE9及以上的浏览器中都能正常工作。IE8及以下版本则完全不支持Canvas,需要额外引入excanvas模拟层,但性能较差,只能作为最后手段。
JavaScript语法方面也要格外小心。老旧工控机上运行的IE11对const、let、箭头函数的支持不完整,一旦出现语法错误,整个脚本都会停止运行。建议全部使用var声明变量,函数使用普通function关键字定义,避免使用模板字符串、对象解构等新语法。如果代码中存在第三方库的依赖,也应该优先选择压缩后体积小、依赖少的版本。
最后,jQuery本身的操作也需要注意性能。在绘图循环中不要使用jQuery选择器去反复查找DOM元素,而应该在初始化时把需要的元素缓存到变量中。例如var canvas = $("#waveCanvas")[0]只执行一次,后续绘图完全使用原生变量。实时刷新画面时更要避免用jQuery的.html()或.append()来更新页面中的其他元素,因为这些操作会触发浏览器重排,和Canvas高频重绘叠加在一起,容易造成严重的卡顿。
通过这些方法,一套基于jQuery与Canvas的波形绘制方案足以在老旧工控机上稳定运行。它没有复杂的框架依赖,没有艰深的算法,只需要把握住数据缓冲、路径合并、离屏缓存这几个关键点,就能在资源受限的环境中获得令人满意的实时展示效果。