1

为什么需要实时推送

📡 制造业为什么离不开实时数据?

在制造业大屏中,数据不是"刷新一次看一次"那么简单。OEE(设备综合效率)每秒都在变化, 机器状态(运行/停机/报警)必须即时反映,产线产量要实时滚动。如果数据延迟 10 秒才更新, 操作员看到的就是"过去的故事",不是"现在的情况"。

核心机制

HTTP 轮询(定时拉取)
前端每隔 5 秒发一次 HTTP 请求:"有新数据吗?" 服务器说"没有"。5 秒后再问。 大部分请求都是浪费的 — 数据根本没变化!而且最多 5 秒延迟,不可能"毫秒级"感知。

WebSocket(双向长连接)
前端和服务器建立一条持久连接,不断开。服务器有新数据的那一刻,直接推给前端。 你零延迟收到消息,中间没有任何无用请求。

Server-Sent Events(服务器单向推送)
服务器主动给前端推送数据流。但这个推送是单向的 — 服务器推什么前端收什么,前端不能通过 SSE 向服务器发消息。

🏭 制造业实时场景

实时曲线 (Real-time Line Chart)

  • OEE 数值每秒刷新
  • 温度/湿度传感器数据流
  • 产量累计曲线实时增长
  • 设备功率波动监测

动态排序 (Dynamic Ranking)

  • 各产线产量排行榜实时变动
  • 设备 OEE 排名动态升降
  • 不良品率 Top 5 实时更新
  • 能耗排名柱状图动画

钻取与交互分析

  • 点击产线钻取到设备级
  • 点击设备查看历史趋势
  • 报警信息实时弹出详情
  • 联动过滤 — 选择车间联动所有图表

实时告警推送

  • 设备异常停机即时告警
  • 温度超标红色弹窗
  • 质量异常消息推送
  • 巡检提醒定时弹出
核心诉求: 数据变了 → 前端立刻知道。不是前端去问"你变了吗",而是后端说"我变了!"
2

HTTP 轮询

🔄 基本原理:setInterval + fetch

最简单粗暴的方式 — 前端设一个定时器,每隔 N 秒发一次 HTTP 请求问后端要数据。

前端轮询代码 JavaScript
// 最简单的轮询:每 2 秒请求一次 OEE 数据
let pollingTimer = null;

function startPolling() {
  pollingTimer = setInterval(async () => {
    try {
      const res = await fetch('/api/oee/realtime');
      const data = await res.json();
      updateDashboard(data);  // 更新大屏图表
    } catch (err) {
      console.error('轮询失败:', err);
    }
  }, 2000);  // 每 2 秒请求一次
}

function stopPolling() {
  clearInterval(pollingTimer);
}

// 页面加载时开始轮询
startPolling();

// 页面卸载时停止轮询(很重要!)
window.addEventListener('beforeunload', stopPolling);
后端接口(Express) Node.js
const express = require('express');
const app = express();

app.get('/api/oee/realtime', (req, res) => {
  // 每次请求都从数据库/缓存读取最新数据
  const oeeData = {
    timestamp: Date.now(),
    line1: { oee: 87.3, status: 'running' },
    line2: { oee: 92.1, status: 'running' },
    line3: { oee: 65.8, status: 'alarm' }
  };
  res.json(oeeData);
});

app.listen(3000);
⏱️ 时序图与问题
客户端 (Browser) 服务端 (Server) │ │ │──── GET /api/oee/realtime ────────▶│ t=0s 请求1 │◀─── 200 OK {oee: 87.3} ───────────│ 响应1 │ │ │ (等待 2 秒...) │ │ │ │──── GET /api/oee/realtime ────────▶│ t=2s 请求2 │◀─── 200 OK {oee: 87.5} ───────────│ 响应2 │ │ │ (等待 2 秒...) │ │ │ │──── GET /api/oee/realtime ────────▶│ t=4s 请求3 │◀─── 200 OK {oee: 87.5} ───────────│ 响应3 ← 数据没变! │ │ │ (等待 2 秒...) │ │ │ │──── GET /api/oee/realtime ────────▶│ t=6s 请求4 │◀─── 200 OK {oee: 87.5} ───────────│ 响应4 ← 还是没变! │ │
致命问题: 假设你有 100 个大屏在车间同时运行,每个每秒请求 1 次:
100 × 1 req/sec = 100 请求/秒
其中 90% 的请求返回的数据和上次一模一样 — 纯属浪费
这就好比 100 个大屏每秒各发一次请求问"有新数据吗?",服务器每秒处理 100 次查询,其中 90% 返回的是相同的数据 — 纯属浪费服务器资源
问题 说明 严重程度
无谓请求 数据没变也要发请求,浪费带宽和服务器资源 严重
延迟不可控 2 秒轮询意味着最坏情况下数据延迟 2 秒 中等
服务器压力大 100 客户端 × 1 req/s = 100 QPS,且每次都要建立新 TCP 连接 严重
HTTP 开销 每次请求都带完整 Header(Cookie、User-Agent 等),头部可能比数据还大 中等
什么时候用轮询就够了? 数据更新频率低(分钟级)、客户端数量少(个位数)、对实时性要求不高的内部工具。 大屏场景不推荐
3

Server-Sent Events (SSE)

📤 SSE 原理:服务端单向推送

SSE 基于 HTTP 协议,服务端可以持续向客户端发送数据。客户端用 EventSource 接收。 和轮询不同,SSE 只建立一次连接,服务端有数据就推,没有就安静等着。

核心机制

SSE = 服务器单向推送数据流
服务器有新数据时主动推给前端,比如设备状态变化:"正常" → "预警" → "故障" → "已修复"。 前端只需要监听这个流,数据会自动到达。但注意:前端不能通过 SSE 向服务器发数据, 数据流是单向的(服务端 → 客户端)。

对比 WebSocket = 双向通信:前端和服务器可以互相发消息。

客户端 (Browser) 服务端 (Server) │ │ │──── GET /api/sse/oee ────────────▶│ 建立连接(只一次!) │ Accept: text/event-stream │ │ │ │◀──── data: {oee: 87.3} ───────────│ t=0s 服务端主动推 │◀──── data: {oee: 87.5} ───────────│ t=3s 服务端主动推 │ │ t=5s 没变化,不推!(省流量) │◀──── data: {oee: 86.1} ───────────│ t=8s 变了才推 │◀──── data: {oee: 86.3} ───────────│ t=10s 持续推送... │ │ │ ✅ 只在有数据变化时推送 │ │ ✅ 只建立一次连接 │ │ ❌ 客户端无法向服务端发消息 │
💻 完整代码示例
服务端 — Node.js SSE 端点 Node.js
const express = require('express');
const app = express();

app.get('/api/sse/oee', (req, res) => {
  // 1. 设置 SSE 必需的响应头
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.flushHeaders();  // 立即发送响应头

  // 2. 每 2 秒推送一次 OEE 数据
  const interval = setInterval(() => {
    const data = {
      timestamp: Date.now(),
      line1: { oee: (80 + Math.random() * 15).toFixed(1) },
      line2: { oee: (85 + Math.random() * 10).toFixed(1) },
      line3: { oee: (60 + Math.random() * 20).toFixed(1) }
    };
    // SSE 格式: "data: ...\n\n"
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  }, 2000);

  // 3. 客户端断开时清理
  req.on('close', () => {
    clearInterval(interval);
    res.end();
  });
});

app.listen(3000);
客户端 — 浏览器 EventSource JavaScript
// 1. 创建 EventSource 连接(浏览器原生 API!)
const eventSource = new EventSource('/api/sse/oee');

// 2. 监听消息事件
eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('收到OEE数据:', data);
  updateDashboard(data);  // 更新大屏
};

// 3. 监听连接打开
eventSource.onopen = () => {
  console.log('SSE 连接已建立');
};

// 4. 监听错误(自动重连是浏览器内置的!)
eventSource.onerror = (event) => {
  if (event.target.readyState === EventSource.CLOSED) {
    console.log('SSE 连接已关闭');
  } else {
    console.log('SSE 连接中断,浏览器将自动重连...');
  }
};

// 5. 手动关闭连接
// eventSource.close();
SSE 优势: ① 浏览器原生支持 EventSource,无需引入任何库 ② 断线后浏览器自动重连 ③ 比 WebSocket 更简单,适合只需服务端推送的场景(大屏展示就是典型!)
SSE 局限: ① 单向通信 — 客户端不能通过 SSE 给服务端发消息 ② 默认最多 6 个 SSE 连接(浏览器 HTTP/1.1 限制) ③ 只支持文本数据,不支持二进制
4

WebSocket — 全双工实时通信

🔌 WebSocket 原理

WebSocket 是一个独立的网络协议(ws://),通过 HTTP 握手升级建立连接后, 客户端和服务端可以随时互发消息,不再受 HTTP 请求-响应模式限制。

类比理解

HTTP(对讲机模式)
你按住按钮说一句"给我今天的 OEE",松开后等对方回复。说完一句,对话就结束了。 想再说?重新按按钮,重新问。

WebSocket(电话会议模式)
你拨通电话后,双方都不挂断。 任何一方随时可以说(发送数据),另一方立刻听到(接收数据)。 不需要"你说完了我才能说",可以同时双向通话。

在制造业大屏中:服务端随时推"OEE 变了!设备 3 报警了!", 客户端也可以随时发"我要订阅产线 A 的数据"。

WebSocket 握手升级流程
客户端发起 HTTP
Upgrade: websocket
服务端同意升级
101 Switching Protocol
WebSocket 连接建立成功!
双方自由互发消息(全双工)
🖥️ 服务端实现 — Node.js + ws 库
server.js — WebSocket 服务端 Node.js
const { WebSocketServer } = require('ws');

// 创建 WebSocket 服务器,监听 8080 端口
const wss = new WebSocketServer({ port: 8080 });

// 保存所有已连接的客户端
const clients = new Set();

wss.on('connection', (ws) => {
  console.log('新客户端连接!当前连接数:', wss.clients.size);
  clients.add(ws);

  // 收到客户端消息
  ws.on('message', (data) => {
    const msg = JSON.parse(data);
    console.log('收到消息:', msg);

    // 示例:客户端请求订阅特定产线
    if (msg.type === 'subscribe') {
      ws.lineId = msg.lineId;  // 标记该客户端关注的产线
      ws.send(JSON.stringify({
        type: 'subscribed',
        lineId: msg.lineId
      }));
    }
  });

  // 客户端断开
  ws.on('close', () => {
    clients.delete(ws);
    console.log('客户端断开,剩余:', clients.size);
  });
});

// 模拟 OEE 数据 — 每 2 秒向所有客户端广播
setInterval(() => {
  const oeeData = {
    type: 'oee_update',
    timestamp: Date.now(),
    lines: {
      line1: { oee: (80 + Math.random() * 15).toFixed(1), status: 'running' },
      line2: { oee: (85 + Math.random() * 10).toFixed(1), status: 'running' },
      line3: { oee: (60 + Math.random() * 25).toFixed(1), status: 'alarm' }
    }
  };

  // 向所有已连接的客户端广播
  for (const client of clients) {
    if (client.readyState === 1) {  // WebSocket.OPEN
      client.send(JSON.stringify(oeeData));
    }
  }
}, 2000);

console.log('WebSocket 服务器运行在 ws://localhost:8080');
安装依赖 Bash
npm init -y
npm install ws express
🌐 客户端实现 — 浏览器 WebSocket API
浏览器端 WebSocket 代码 JavaScript
// 1. 创建 WebSocket 连接
const ws = new WebSocket('ws://localhost:8080');

// 2. 连接成功
ws.onopen = () => {
  console.log('✅ WebSocket 连接成功');

  // 可以立即向服务端发送消息!
  ws.send(JSON.stringify({
    type: 'subscribe',
    lineId: 'line1'
  }));
};

// 3. 收到服务端消息
ws.onmessage = (event) => {
  const data = JSON.parse(event.data);

  if (data.type === 'oee_update') {
    updateOeeChart(data.lines);     // 更新 OEE 实时曲线
    updateStatusCards(data.lines);   // 更新设备状态卡片
  }
};

// 4. 连接关闭
ws.onclose = (event) => {
  console.log('连接关闭:', event.code, event.reason);
  // 注意:WebSocket 不会自动重连!需要自己实现
  setTimeout(() => {
    console.log('尝试重连...');
    connectWebSocket();  // 重新创建连接
  }, 3000);
};

// 5. 连接错误
ws.onerror = (error) => {
  console.error('WebSocket 错误:', error);
};
注意: 浏览器原生 WebSocket 不会自动重连!需要在 onclose 中手动实现重连逻辑。 这也是为什么生产环境推荐用 Socket.IO 的原因之一。
📊 消息流图解 — 制造业大屏场景
WebSocket 消息流 — OEE 实时推送 数据采集层 WebSocket 服务端 大屏 1 (车间A) 大屏 2 (办公室) ┌─────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ PLC/SCADA│ │ Node.js ws │ │ 浏览器 WS │ │ 浏览器 WS │ │ 传感器 │ │ :8080 │ │ 大屏展示 │ │ 管理看板 │ └────┬─────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ OEE 数据(每2s) │ │ │ │────────────────────────▶│ │ │ │ │ ◀── 握手升级 ──────────│ │ │ │ ◀── 握手升级 ──────────────────────────────│ │ │ │ │ │ │── {oee:87.3} ─────────▶│ │ │ │── {oee:87.3} ─────────────────────────────▶│ │ │ │ 更新曲线 │ │ │ │ │ │ OEE 数据(2s后) │ │ │ │────────────────────────▶│ │ │ │ │── {oee:88.1} ─────────▶│ │ │ │── {oee:88.1} ─────────────────────────────▶│ │ │ │ 动画更新 │ │ │ │ │ │ │◀── subscribe:line1 ────│ 客户端也能发消息! │ │ │ │ │ │ 设备3报警! │ │ │ │────────────────────────▶│ │ │ │ │── {alarm:设备3} ──────▶│ 🔴 红色弹窗! │ │ │── {alarm:设备3} ──────────────────────────▶│
5

Socket.IO — 生产级 WebSocket 方案

🚀 为什么生产环境推荐 Socket.IO?
类比理解

原生 WebSocket = 手动挡汽车:能开,但离合、换挡、熄火重启都要你自己来。 断线了你要自己写重连逻辑,消息丢了你要自己处理。

Socket.IO = 自动挡 + 自动驾驶:所有麻烦事都帮你搞定了 — 自动重连(断线自动恢复)、 降级兜底(WebSocket 不可用时自动降级到轮询)、 房间机制(只给特定组的人广播)。

特性 原生 WebSocket Socket.IO
自动重连 需手动实现 内置
降级兜底 不支持 自动降级到轮询
房间(Rooms) 需手动实现 内置
消息广播 遍历所有连接 一行代码
命名空间 不支持 支持
二进制数据 原生支持 支持
💻 完整代码示例 — Room 广播 OEE
服务端 — Socket.IO + Room 广播 Node.js
const { Server } = require('socket.io');
const express = require('express');

const app = express();
const httpServer = app.listen(3000);

// 创建 Socket.IO 服务器
const io = new Server(httpServer, {
  cors: { origin: '*' }  // 开发环境允许所有来源
});

io.on('connection', (socket) => {
  console.log('客户端连接:', socket.id);

  // 客户端加入 "dashboard" 房间
  socket.on('join-dashboard', () => {
    socket.join('dashboard');
    console.log(socket.id, '加入 dashboard 房间');
  });

  // 客户端请求钻取某个设备详情
  socket.on('drill-down', (deviceId) => {
    // 只给请求者返回数据(不是广播)
    const history = getDeviceHistory(deviceId);
    socket.emit('device-detail', history);
  });

  socket.on('disconnect', () => {
    console.log('客户端断开:', socket.id);
  });
});

// 每 2 秒向 "dashboard" 房间广播 OEE 数据
setInterval(() => {
  const oeeData = {
    timestamp: Date.now(),
    lines: {
      line1: { oee: (80 + Math.random() * 15).toFixed(1) },
      line2: { oee: (85 + Math.random() * 10).toFixed(1) },
      line3: { oee: (60 + Math.random() * 20).toFixed(1) }
    }
  };

  // 🔥 关键:只推送给 dashboard 房间的成员
  io.to('dashboard').emit('oee-update', oeeData);
}, 2000);
客户端 — 连接、监听、自动重连 JavaScript
// 引入 Socket.IO 客户端(CDN 或 npm)
// <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>

const socket = io('http://localhost:3000', {
  // 自动重连配置(Socket.IO 内置!)
  reconnection: true,
  reconnectionAttempts: Infinity,   // 无限重试
  reconnectionDelay: 1000,          // 首次重连延迟 1 秒
  reconnectionDelayMax: 5000,       // 最大延迟 5 秒
});

// 连接成功
socket.on('connect', () => {
  console.log('连接成功,ID:', socket.id);
  socket.emit('join-dashboard');  // 加入房间
});

// 监听 OEE 更新
socket.on('oee-update', (data) => {
  updateOeeChart(data.lines);
  updateStatusCards(data.lines);
});

// 监听断线(Socket.IO 会自动重连)
socket.on('disconnect', (reason) => {
  console.log('断线原因:', reason);
  // 不需要手动重连!Socket.IO 自动处理
});

// 监听重连成功
socket.on('reconnect', (attempt) => {
  console.log('重连成功,尝试次数:', attempt);
});

// 钻取设备详情(双向通信!)
function drillDownDevice(deviceId) {
  socket.emit('drill-down', deviceId);
  socket.on('device-detail', (data) => {
    showDeviceModal(data);  // 弹出设备详情弹窗
  });
}
Socket.IO Room 机制图解
大屏 A
join('dashboard')
Room: "dashboard"
[大屏A, 大屏B, 管理看板]
io.to('dashboard').emit()
OEE 数据源
只有加入 "dashboard" 房间的客户端才会收到 OEE 广播,其他连接不受影响
🔄 自动重连机制详解
Socket.IO 自动重连时间线 t=0s 连接正常 ← → 正常收发数据 t=5s 网络波动,连接断开! ↓ t=6s 第1次重连尝试 (delay: 1s) ... 失败 t=8s 第2次重连尝试 (delay: 2s) ... 失败 t=11s 第3次重连尝试 (delay: 3s) ... 失败 t=15s 第4次重连尝试 (delay: 4s) ... 失败 t=20s 第5次重连尝试 (delay: 5s) ... 失败 t=25s 第6次重连尝试 (delay: 5s, 已达上限) ... 成功! ✅ ↓ 自动恢复,无需手动干预 断线期间的数据可以选择补发(需要后端配合)
断线期间的数据怎么办? Socket.IO 支持 volatile 消息(丢掉也无所谓)和普通消息。 对于 OEE 实时曲线,断线期间的数据可以不要(只关心最新值)。 对于报警消息,需要在重连后请求补发。
6

前端实时图表更新

📈 ECharts 实时曲线 — 追加新数据,移除旧数据

实时曲线的核心逻辑:收到新数据 → 追加到数组尾部 → 超过 N 条则移除头部 → 调用 setOption 更新。 不需要重新创建图表,只更新数据即可,ECharts 会自动做过渡动画。

类比理解

实时曲线 = 走纸式心电监护仪
医院里的心电图仪,纸带上不停画着波浪线 — 新数据从右边出来,旧数据从左边"卷走"。 我们的前端曲线也是一样:新数据追加到右边,超过 50 个点就把最左边的移除, 屏幕上始终只显示最近 50 个点的"窗口"。

实时曲线核心逻辑 JavaScript
// 假设已经创建好了 ECharts 实例
const MAX_POINTS = 50;  // 保留最近 50 个数据点

// X 轴时间数据
let timeData = [];
// Y 轴各产线 OEE 数据
let line1Data = [];
let line2Data = [];
let line3Data = [];

// 收到新数据时调用
function appendData(newData) {
  const now = new Date(newData.timestamp);
  const timeStr = now.toLocaleTimeString();

  // 1. 追加新数据
  timeData.push(timeStr);
  line1Data.push(parseFloat(newData.lines.line1.oee));
  line2Data.push(parseFloat(newData.lines.line2.oee));
  line3Data.push(parseFloat(newData.lines.line3.oee));

  // 2. 超过最大点数,移除最旧的数据(队列滑动窗口)
  if (timeData.length > MAX_POINTS) {
    timeData.shift();    // 移除头部最旧的
    line1Data.shift();
    line2Data.shift();
    line3Data.shift();
  }

  // 3. 更新图表(只更新 series 数据,不需要重建图表)
  chart.setOption({
    xAxis: { data: timeData },
    series: [
      { data: line1Data },
      { data: line2Data },
      { data: line3Data }
    ]
  });
}
性能要点:shift() 移除旧数据时,MAX_POINTS 不宜超过 200。 如果数据量更大,建议用环形缓冲区(Circular Buffer)代替数组。
🎯 仪表盘动态更新 — 带动画的 Gauge
仪表盘实时更新 JavaScript
// Gauge(仪表盘)配置
const gaugeOption = {
  series: [{
    type: 'gauge',
    detail: { formatter: '{value}%' },
    data: [{ value: 87.3, name: 'OEE' }],
    // 动画配置
    animationDurationUpdate: 800,  // 过渡动画时长 800ms
    animationEasingUpdate: 'cubicInOut'  // 缓动函数
  }]
};

// 收到新 OEE 值时更新
function updateGauge(newOee) {
  gaugeChart.setOption({
    series: [{
      data: [{ value: newOee, name: 'OEE' }]
    }]
  });
  // ECharts 会自动从旧值 → 新值做 800ms 的平滑过渡动画
}
🔧 完整代码示例 — Socket.IO + ECharts 实时大屏
完整 HTML — 可直接运行 HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>实时OEE看板</title>
  <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
  <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
  <style>
    body { background: #0a0e27; color: #e0e6ff; font-family: sans-serif; display: flex; gap: 20px; padding: 20px; }
    .chart-box { background: #131a3a; border: 1px solid #7ec8f0; border-radius: 12px; padding: 16px; }
    #lineChart { width: 600px; height: 350px; }
    #gaugeChart { width: 300px; height: 350px; }
    #status { position: fixed; top: 10px; right: 20px; padding: 6px 14px; border-radius: 6px; font-size: 13px; }
    .connected { background: rgba(34,197,94,0.2); color: #22c55e; }
    .disconnected { background: rgba(255,107,107,0.2); color: #ff6b6b; }
  </style>
</head>
<body>
  <div id="status" class="disconnected">未连接</div>

  <div class="chart-box">
    <h3>OEE 实时曲线</h3>
    <div id="lineChart"></div>
  </div>

  <div class="chart-box">
    <h3>产线1 OEE</h3>
    <div id="gaugeChart"></div>
  </div>

  <script>
    // ===== 1. 初始化图表 =====
    const lineChart = echarts.init(document.getElementById('lineChart'));
    const gaugeChart = echarts.init(document.getElementById('gaugeChart'));

    const MAX = 50;
    let times = [], l1 = [], l2 = [], l3 = [];

    // 折线图初始配置
    lineChart.setOption({
      backgroundColor: 'transparent',
      tooltip: { trigger: 'axis' },
      legend: { data: ['产线1', '产线2', '产线3'], textStyle: { color: '#8892b0' } },
      xAxis: { type: 'category', data: [], axisLabel: { color: '#8892b0' } },
      yAxis: { type: 'value', min: 50, max: 100, axisLabel: { color: '#8892b0' },
               splitLine: { lineStyle: { color: '#7ec8f0' } } },
      series: [
        { name: '产线1', type: 'line', smooth: true, data: [], lineStyle: { color: '#4facfe' } },
        { name: '产线2', type: 'line', smooth: true, data: [], lineStyle: { color: '#22c55e' } },
        { name: '产线3', type: 'line', smooth: true, data: [], lineStyle: { color: '#ff6b6b' } }
      ]
    });

    // 仪表盘初始配置
    gaugeChart.setOption({
      backgroundColor: 'transparent',
      series: [{
        type: 'gauge',
        detail: { formatter: '{value}%', color: '#e0e6ff', fontSize: 28 },
        data: [{ value: 0, name: 'OEE' }],
        axisLine: { lineStyle: { color: [[0.6,'#ff6b6b'],[0.8,'#ffd93d'],[1,'#22c55e']] } },
        axisLabel: { color: '#8892b0' },
        animationDurationUpdate: 800,
        animationEasingUpdate: 'cubicInOut'
      }]
    });

    // ===== 2. 连接 Socket.IO =====
    const socket = io('http://localhost:3000');
    const statusEl = document.getElementById('status');

    socket.on('connect', () => {
      statusEl.textContent = '已连接 ' + socket.id;
      statusEl.className = 'connected';
      socket.emit('join-dashboard');
    });

    socket.on('disconnect', () => {
      statusEl.textContent = '连接断开,自动重连中...';
      statusEl.className = 'disconnected';
    });

    // ===== 3. 收到实时数据,更新图表 =====
    socket.on('oee-update', (data) => {
      const time = new Date(data.timestamp).toLocaleTimeString();

      times.push(time);
      l1.push(+data.lines.line1.oee);
      l2.push(+data.lines.line2.oee);
      l3.push(+data.lines.line3.oee);

      if (times.length > MAX) { times.shift(); l1.shift(); l2.shift(); l3.shift(); }

      // 更新曲线
      lineChart.setOption({
        xAxis: { data: times },
        series: [{ data: l1 }, { data: l2 }, { data: l3 }]
      });

      // 更新仪表盘
      gaugeChart.setOption({
        series: [{ data: [{ value: l1[l1.length-1], name: 'OEE' }] }]
      });
    });
  </script>
</body>
</html>
7

三种方案对比

⚖️ Polling vs SSE vs WebSocket vs Socket.IO
对比维度 HTTP 轮询 SSE WebSocket Socket.IO
通信方向 客户端 → 服务端
(请求-响应)
服务端 → 客户端
(单向推送)
双向全双工
(⇄ 自由互发)
双向全双工
(⇄ 自由互发)
协议 HTTP HTTP WS (独立协议) WS / HTTP 降级
复杂度 极低 中等 中等
浏览器支持 100% 98% 98% 100%
自动重连 浏览器内置 需手动实现 内置
降级兜底 不支持 不支持 自动降级到轮询
连接开销 高(每次新建连接) 低(保持一条连接) 低(保持一条连接) 低(保持一条连接)
服务端推送 不支持 原生支持 原生支持 原生支持
二进制数据 支持 不支持 支持 支持
典型场景 低频数据获取
简单后台管理
服务端单向推送
新闻/通知/日志
聊天/游戏/协同
需要双向通信
生产环境实时系统
大屏/IoT/协同
大屏推荐度 ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
选型建议:
制造业大屏(生产环境) → Socket.IO(最推荐,功能全,兜底好)
简单大屏(只展示不交互) → SSE(够用,最简单)
内部小工具(低频更新) → HTTP 轮询(最省事)
极致性能需求(IoT 海量连接) → 原生 WebSocket(无额外开销)
8

面试考点

1 WebSocket 和 HTTP 的区别是什么?

核心区别:

  • 通信模式:HTTP 是请求-响应模式(客户端问、服务端答);WebSocket 建立后双方可随时互发消息
  • 连接生命周期:HTTP 每次请求建立连接、响应后断开(HTTP/1.0);WebSocket 一次握手、持久连接
  • 开销:HTTP 每次请求带完整 Header(几百字节);WebSocket 帧头最小 2 字节
  • 实时性:HTTP 无法主动推送,只能靠轮询模拟;WebSocket 服务端可主动推送
  • 协议:HTTP 是独立协议;WebSocket 通过 HTTP Upgrade 握手升级建立
2 WebSocket 和 SSE 的区别?各适合什么场景?

方向不同:WebSocket 全双工(双向),SSE 半双工(服务端→客户端单向)。

  • SSE 优势:基于 HTTP、浏览器原生 EventSource、自动重连、实现简单
  • SSE 劣势:只能服务端推、不支持二进制、HTTP/1.1 最多 6 个连接
  • WebSocket 优势:双向通信、支持二进制、无连接数限制
  • 选型原则:只需服务端推送(大屏展示、消息通知)→ SSE;需要双向交互(聊天、协同编辑、IoT 控制)→ WebSocket
3 如何处理 WebSocket 断线重连?

原生 WebSocket 需要手动实现重连逻辑:

  • onclose 回调中启动重连(setTimeout + 递增延迟)
  • 使用指数退避策略:1s → 2s → 4s → 8s → 最大 30s
  • 设置最大重试次数,避免无限重试
  • 重连成功后重新发送订阅请求
  • 重连期间在前端显示"连接中断"状态提示

推荐方案:使用 Socket.IO,内置完善的自动重连机制,无需手动实现。

4 实时数据量太大怎么办?

分层治理策略:

  • 服务端聚合:不推原始数据,推聚合后的结果(如 1 秒内 1000 条聚合为 1 条均值)
  • 按需推送:客户端订阅特定设备/产线,只推送关注的数据(Room 机制)
  • 降频推送:OEE 每秒变但人眼分辨不了,2-3 秒推一次足够
  • 压缩传输:WebSocket 支持二进制帧,用 MessagePack / Protobuf 代替 JSON
  • 消息分级:报警消息立即推送,OEE 数据定时推送,历史数据拉取走 HTTP
  • 后端缓冲:用 Redis Pub/Sub 做消息中间件,WebSocket 服务端只做推送
5 前端如何节流处理高频实时数据?

三种策略:

  • 节流(Throttle):不管数据来多快,图表更新频率固定(如每 500ms 更新一次),中间的数据缓存起来批量处理
  • 防抖(Debounce):连续收到数据时不更新,停止收到数据后才更新 — 适合搜索框,不适合实时大屏
  • requestAnimationFrame:把图表更新放在 requestAnimationFrame 里,和浏览器刷新率同步(60fps → 16.7ms 一次)

制造业大屏推荐:requestAnimationFrame + 数据缓冲队列。 数据来了先存队列,每帧取队列中所有数据做一次批量更新。

6 Socket.IO 相比原生 WebSocket 有什么优势?

六大核心优势:

  • 自动重连:内置指数退避重连策略,断线自动恢复,无需手动实现
  • 降级兜底:WebSocket 不可用时自动降级到 HTTP 长轮询,保证可用性
  • 房间机制:原生支持 Rooms,可以分组广播(如只给"dashboard"房间推数据)
  • 命名空间:一个端口支持多个命名空间(/oee/alarm),逻辑隔离
  • 事件机制:自定义事件名(socket.emit('oee-update', data)),比原生 WebSocket 的字符串更语义化
  • 广播便捷:io.to('room').emit() 一行搞定定向广播,原生需要遍历所有连接

代价:多了一个库的依赖(~30KB gzip),协议不是标准 WebSocket(不能和非 Socket.IO 客户端互通)。

实时数据推送技术 — 制造业可视化开发工程师必备知识
HTTP 轮询 → SSE → WebSocket → Socket.IO → ECharts 实时更新