实时监控大屏数据卡住不动?手把手教你用Echarts实现股票行情服务器状态仪表盘动态刷新完整教程
你是不是也遇到过这种情况——大屏上的数据突然”冻住”了,明明服务器还在跑,图表却像被人按了暂停键,怎么刷新都不动?别急,这个坑我踩过不止一次。今天就把我压箱底的实战经验拿出来,带你从零搭建一个能真正动态刷新的股票行情仪表盘。
一、先搞明白:数据”卡住”的根本原因
在写代码之前,我得先给你掰扯清楚一个问题:为什么你的Echarts图表会不动?
大多数情况下,问题出在这里——你把数据生成和渲染写死在了一次性请求里,或者setOption的时候没有处理好数据替换的逻辑。就像你每天去同一个柜台买包子,但老板每次都给你端来昨天剩下的凉包子,你当然会觉得”怎么跟昨天一样”。
真正的动态刷新,需要解决三个核心点:
- 数据源的时效性——你拿到的数据是不是最新的?
- 渲染的周期性——你有没有定期去拉取新数据?
- 更新策略的正确性——你是每次都清空重绘,还是精准更新?
下面我会带着你一步步把这三件事都搞定。
二、项目结构:先把架子搭好
先别急着写代码,我们先把目录理清楚。一个像样的监控大屏项目,目录结构应该是这样的:
stock-monitor/
├── index.html # 入口页面
├── css/
│ └── style.css # 样式文件
├── js/
│ ├── app.js # 主逻辑
│ ├── api.js # 数据请求封装
│ └── chart.js # Echarts配置
└── data/
└── mock.js # 模拟数据(开发时用)
为什么要这样分?你想想,如果所有代码都塞在一个文件里,过两天你自己都看不懂。模块化的好处是,api.js只管拿数据,chart.js只管画图,app.js负责串联——谁出事找谁,debug的时候不会崩溃。
三、HTML骨架:给Echarts一个”家”
先写index.html,这里有个很多人会踩的坑——很多人直接在body里写div,却忘了引入Echarts的CDN。我推荐用国内的镜像源,速度快很多:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>股票行情实时监控大屏</title>
<link rel="stylesheet" href="css/style.css">
<!-- 用腾讯CDN,国内访问飞快 -->
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.4.3/echarts.min.js"></script>
</head>
<body>
<div class="dashboard">
<header class="header">
<h1>📊 股票行情服务器状态监控</h1>
<span class="status-indicator" id="statusIndicator">● 连接中...</span>
<span class="update-time" id="updateTime">最后更新:--:--:--</span>
</header>
<div class="main-content">
<!-- 第一行:三个核心指标卡片 -->
<div class="metrics-row">
<div class="metric-card">
<div class="metric-title">活跃连接数</div>
<div class="metric-value" id="activeConnections">--</div>
<div class="metric-unit">个</div>
</div>
<div class="metric-card">
<div class="metric-title">消息吞吐率</div>
<div class="metric-value" id="throughput">--</div>
<div class="metric-unit">条/秒</div>
</div>
<div class="metric-card">
<div class="metric-title">平均延迟</div>
<div class="metric-value" id="avgLatency">--</div>
<div class="metric-unit">ms</div>
</div>
</div>
<!-- 第二行:K线图 + 仪表盘 -->
<div class="charts-row">
<div class="chart-container" id="klineChart"></div>
<div class="chart-container" id="gaugeChart"></div>
</div>
<!-- 第三行:实时折线图 + 服务器状态热力图 -->
<div class="charts-row">
<div class="chart-container" id="lineChart"></div>
<div class="chart-container" id="heatmapChart"></div>
</div>
</div>
</div>
<script src="js/api.js"></script>
<script src="js/chart.js"></script>
<script src="js/app.js"></script>
</body>
</html>
注意看header部分,我加了三个东西:标题、连接状态指示灯、最后更新时间。这三个是监控大屏的”面子工程”,但极其重要——用户第一眼看过去,就知道这个系统”活着”还是”死了”。
四、CSS样式:让大屏看起来像个正经监控台
监控大屏的样式有它的特殊性:深色背景、大字号、高对比度。直接看代码:
/* css/style.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0a0e27; /* 深空蓝,不刺眼 */
color: #e0e6ed;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
min-height: 100vh;
overflow-x: hidden;
}
.dashboard {
padding: 20px;
max-width: 1920px;
margin: 0 auto;
}
/* 顶部导航栏 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
background: linear-gradient(135deg, #1a1f3a 0%, #0d1229 100%);
border-radius: 12px;
margin-bottom: 20px;
border: 1px solid rgba(100, 180, 255, 0.15);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
.header h1 {
font-size: 24px;
font-weight: 600;
background: linear-gradient(90deg, #4facfe, #00f2fe);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: 2px;
}
/* 状态指示灯 */
.status-indicator {
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.status-indicator.online {
color: #00e676;
}
.status-indicator.offline {
color: #ff5252;
}
.status-indicator::before {
content: '';
width: 10px;
height: 10px;
border-radius: 50%;
background: currentColor;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.2); }
}
.update-time {
font-size: 13px;
color: #8892b0;
background: rgba(255,255,255,0.05);
padding: 4px 12px;
border-radius: 20px;
}
/* 指标卡片 */
.metrics-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 20px;
}
.metric-card {
background: linear-gradient(145deg, #151a30, #1a2040);
border-radius: 16px;
padding: 28px;
text-align: center;
border: 1px solid rgba(100, 180, 255, 0.1);
position: relative;
overflow: hidden;
transition: transform 0.3s, box-shadow 0.3s;
}
.metric-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 30px rgba(79, 172, 254, 0.2);
}
.metric-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #4facfe, #00f2fe);
}
.metric-title {
font-size: 14px;
color: #8892b0;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 1px;
}
.metric-value {
font-size: 48px;
font-weight: 700;
color: #ffffff;
line-height: 1;
}
.metric-unit {
font-size: 14px;
color: #4facfe;
margin-top: 8px;
}
/* 图表区域 */
.charts-row {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.chart-container {
background: linear-gradient(145deg, #151a30, #1a2040);
border-radius: 16px;
padding: 20px;
border: 1px solid rgba(100, 180, 254, 0.1);
height: 400px;
}
/* 响应式 */
@media (max-width: 1200px) {
.charts-row {
grid-template-columns: 1fr;
}
.metrics-row {
grid-template-columns: 1fr;
}
}
这段CSS有几个小心思:深色背景配合青色渐变,是监控大屏的”标准配色”;指标数字用48px大字,是为了在大屏上远距离也能看清;卡片hover时的上浮效果,让界面不那么死板。
五、数据请求层:别再写死接口了
很多人写监控大屏,直接把接口地址硬编码在app.js里,这样一旦接口换了就满地找牙。我们把请求逻辑抽出来:
// js/api.js
class StockApi {
constructor(baseUrl = 'https://api.stock-monitor.example.com') {
this.baseUrl = baseUrl;
this.retryCount = 3;
this.retryDelay = 2000;
}
/**
* 获取实时监控数据
* 返回结构:
* {
* activeConnections: 128,
* throughput: 3456,
* avgLatency: 12.5,
* klineData: { times: [...], opens: [...], closes: [...] },
* gaugeValue: 78,
* serverStatus: [[0,0,85],[0,1,92],...]
* }
*/
async fetchRealtimeData() {
const url = `${this.baseUrl}/api/v1/realtime`;
return this.request(url);
}
/**
* 核心请求方法:带重试 + 超时控制
*/
async request(url, options = {}) {
const timeout = options.timeout || 5000;
let lastError;
for (let attempt = 1; attempt <= this.retryCount; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.code !== 0 && data.code !== undefined) {
throw new Error(data.message || '接口返回异常');
}
return data.data || data;
} catch (error) {
lastError = error;
if (attempt === this.retryCount) break;
// 指数退避:2s → 4s → 8s
await this.sleep(this.retryDelay * Math.pow(2, attempt - 1));
}
}
throw lastError;
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// 全局单例
const stockApi = new StockApi();
这里有两个值得说的地方:
第一,AbortController的使用。 很多开发者不知道,fetch请求是可以取消的。如果用户在两次请求之间切换了页面,或者网络很慢,第二次请求还在路上,这时候用AbortController取消掉旧的请求,能避免一堆”过时数据覆盖新数据”的问题。
第二,指数退避重试。 别用固定间隔重试,那样服务器压力大而且没意义。第一次失败等2秒,第二次等4秒,第三次等8秒——这个策略既给了服务器喘息时间,又不会让用户等太久。
六、图表配置层:Echarts怎么初始化才不会卡
这是最关键的部分。很多人写Echarts配置,每次setOption都把整个配置对象重新传一遍,这样不仅性能差,还容易出现”动画闪烁”的问题。
正确做法是:初始化时传完整配置,后续更新时只传需要改动的部分。
// js/chart.js
class StockDashboard {
constructor() {
this.charts = {};
this.isInitialized = false;
this.init();
}
init() {
// 初始化所有图表实例
this.charts.kline = echarts.init(document.getElementById('klineChart'), 'dark');
this.charts.gauge = echarts.init(document.getElementById('gaugeChart'), 'dark');
this.charts.line = echarts.init(document.getElementById('lineChart'), 'dark');
this.charts.heatmap = echarts.init(document.getElementById('heatmapChart'), 'dark');
// 监听窗口大小变化,自动resize
window.addEventListener('resize', () => {
Object.values(this.charts).forEach(chart => chart.resize());
});
this.isInitialized = true;
console.log('✅ 图表初始化完成');
}
/**
* 设置K线图(带数据更新策略)
*/
setKlineOption(data) {
const option = {
backgroundColor: 'transparent',
title: {
text: '实时K线走势',
left: 'center',
textStyle: { color: '#8892b0', fontSize: 14 }
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
backgroundColor: 'rgba(21, 26, 48, 0.95)',
borderColor: '#4facfe',
textStyle: { color: '#e0e6ed' }
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: data.times,
axisLine: { lineStyle: { color: '#2a3050' } },
axisLabel: { color: '#8892b0', fontSize: 11 },
splitLine: { show: false }
},
yAxis: {
type: 'value',
scale: true,
axisLine: { lineStyle: { color: '#2a3050' } },
axisLabel: { color: '#8892b0', fontSize: 11 },
splitLine: { lineStyle: { color: '#1e2545' } }
},
dataZoom: [
{
type: 'inside',
start: 90,
end: 100
},
{
start: 90,
end: 100
}
],
series: [
{
type: 'candlestick',
data: data.closes.map((close, i) => [
data.opens[i], // 开盘
close > data.opens[i] ? data.closes[i] : data.opens[i], // 收盘(涨红跌绿)
Math.min(data.opens[i], data.closes[i]), // 最低
Math.max(data.opens[i], data.closes[i]) // 最高
]),
itemStyle: {
color: '#ef5350', // 阳线(涨)红色
color0: '#00e676', // 阴线(跌)绿色
borderColor: '#ef5350',
borderColor0: '#00e676'
}
}
]
};
// 关键:如果图表已经存在,用setOption更新;否则用init
if (this.charts.kline) {
this.charts.kline.setOption(option, true); // true表示不合并,直接替换
}
}
/**
* 设置仪表盘(增量更新,避免重绘)
*/
setGaugeOption(value) {
const option = {
series: [{
type: 'gauge',
startAngle: 180,
endAngle: 0,
min: 0,
max: 100,
splitNumber: 10,
itemStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 1, y2: 0,
colorStops: [
{ offset: 0, color: '#00e676' },
{ offset: 0.5, color: '#ffca28' },
{ offset: 1, color: '#ef5350' }
]
}
},
progress: {
show: true,
width: 18
},
pointer: {
length: '55%',
width: 4
},
axisLine: {
lineStyle: { width: 18 }
},
axisTick: {
distance: -25,
length: 8,
lineStyle: { color: '#8892b0', width: 1 }
},
splitLine: {
distance: -30,
length: 15,
lineStyle: { color: '#8892b0', width: 2 }
},
axisLabel: {
color: '#8892b0',
fontSize: 12,
distance: -20
},
title: {
show: true,
offsetCenter: [0, '20%'],
textStyle: { color: '#8892b0', fontSize: 14 }
},
detail: {
valueAnimation: true,
offsetCenter: [0, '-10%'],
textStyle: {
color: '#4facfe',
fontSize: 36,
fontWeight: 'bold'
},
formatter: '{value}%'
},
data: [{ value: value, name: 'CPU使用率' }]
}]
};
// 增量更新:只更新data,不重绘整个配置
this.charts.gauge.setOption({
series: [{ data: [{ value: value, name: 'CPU使用率' }] }]
});
}
/**
* 设置折线图(动态追加数据,限制最大长度)
*/
setLineOption(times, values) {
const option = {
backgroundColor: 'transparent',
title: {
text: '实时延迟监控',
left: 'center',
textStyle: { color: '#8892b0', fontSize: 14 }
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(21, 26, 48, 0.95)',
borderColor: '#4facfe',
textStyle: { color: '#e0e6ed' }
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
top: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: times,
axisLine: { lineStyle: { color: '#2a3050' } },
axisLabel: { color: '#8892b0', fontSize: 11, rotate: 30 },
splitLine: { show: false }
},
yAxis: {
type: 'value',
name: 'ms',
nameTextStyle: { color: '#8892b0' },
axisLine: { lineStyle: { color: '#2a3050' } },
axisLabel: { color: '#8892b0', fontSize: 11 },
splitLine: { lineStyle: { color: '#1e2545' } }
},
series: [{
name: '延迟',
type: 'line',
smooth: true,
symbol: 'none',
sampling: 'lttb', // 大量数据时智能采样,提升性能
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(79, 172, 254, 0.4)' },
{ offset: 1, color: 'rgba(79, 172, 254, 0.02)' }
])
},
lineStyle: { color: '#4facfe', width: 2 },
data: values
}]
};
if (this.charts.line) {
this.charts.line.setOption(option, true);
}
}
/**
* 设置热力图(服务器状态)
*/
setHeatmapOption(data) {
const { xAxis, yAxis, heatData } = data;
const option = {
backgroundColor: 'transparent',
title: {
text: '服务器节点状态热力图',
left: 'center',
textStyle: { color: '#8892b0', fontSize: 14 }
},
tooltip: {
position: 'top',
backgroundColor: 'rgba(21, 26, 48, 0.95)',
borderColor: '#4facfe',
textStyle: { color: '#e0e6ed' },
formatter: function(params) {
return `${yAxis[params.value[1]]} - ${xAxis[params.value[0]]}<br/>负载: ${params.value[2]}%`;
}
},
grid: {
height: '70%',
top: '20%',
left: '15%',
right: '8%'
},
xAxis: {
type: 'category',
data: xAxis,
splitArea: { show: true, areaStyle: { color: ['rgba(255,255,255,0.02)', 'rgba(0,0,0,0.02)'] } },
axisLabel: { color: '#8892b0', fontSize: 11 },
axisLine: { lineStyle: { color: '#2a3050' } }
},
yAxis: {
type: 'category',
data: yAxis,
splitArea: { show: true, areaStyle: { color: ['rgba(255,255,255,0.02)', 'rgba(0,0,0,0.02)'] } },
axisLabel: { color: '#8892b0', fontSize: 11 },
axisLine: { lineStyle: { color: '#2a3050' } }
},
visualMap: {
min: 0,
max: 100,
show: false,
calculable: true,
inRange: {
color: ['#00e676', '#ffca28', '#ef5350']
}
},
series: [{
type: 'heatmap',
data: heatData,
label: {
show: true,
color: '#fff',
fontSize: 11
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
};
if (this.charts.heatmap) {
this.charts.heatmap.setOption(option, true);
}
}
/**
* 更新指标卡片数值(带动画效果)
*/
updateMetric(id, value, suffix = '') {
const el = document.getElementById(id);
if (!el) return;
// 数字滚动动画
const start = parseInt(el.textContent) || 0;
const end = parseFloat(value);
const duration = 600;
const startTime = performance.now();
const animate = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// easeOutExpo缓动,让数字先快后慢
const easeProgress = progress === 1 ? 1 : 1 - Math.pow(2, -10 * progress);
const current = Math.round(start + (end - start) * easeProgress);
el.textContent = suffix ? `${current}${suffix}` : current;
if (progress < 1) {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
}
/**
* 更新最后刷新时间
*/
updateTimestamp() {
const now = new Date();
const timeStr = now.toLocaleTimeString('zh-CN', { hour12: false });
const dateStr = now.toLocaleDateString('zh-CN');
document.getElementById('updateTime').textContent =
`最后更新:${dateStr} ${timeStr}`;
}
/**
* 清理:组件卸载时调用
*/
dispose() {
Object.values(this.charts).forEach(chart => chart.dispose());
this.charts = {};
this.isInitialized = false;
}
}
// 导出单例
const dashboard = new StockDashboard();
export default dashboard;
这里我要重点说一下setOption的两个参数问题。很多人用setOption时只传一个参数,这样Echarts默认会”合并”新旧配置——好处是性能好,坏处是如果旧配置里有某些属性新配置没有,它们会残留。我在关键图表上用了setOption(option, true),第二个参数true表示”不合并,直接替换”,这样每次都是全新的干净状态。
但对于仪表盘,我用了增量更新——只传{ series: [{ data: [...] }] },这样Echarts只会更新data,不会重绘整个图表。这个技巧在高频率刷新(比如每秒一次)时,性能差距非常明显。
还有一个细节是折线图的sampling: 'lttb'。当数据点超过几百个时,Echarts会卡顿。LTTB(Largest-Triangle-Three-Buckets)采样算法可以智能地减少数据点,同时保持折线图的视觉形状。你试一下就知道了,数据量大的时候开不开这个参数,流畅度差很多。
七、主逻辑层:定时刷新 + 错误处理
现在有了数据层和图表层,最后把它们串起来:
// js/app.js
class StockMonitorApp {
constructor() {
this.refreshInterval = 3000; // 每3秒刷新一次
this.timer = null;
this.isConnected = false;
this.consecutiveErrors = 0;
this.maxConsecutiveErrors = 5;
this.init();
}
init() {
this.bindEvents();
this.startAutoRefresh();
this.updateConnectionStatus(true);
console.log('🚀 股票监控大屏已启动');
}
bindEvents() {
// 手动刷新按钮
document.addEventListener('click', (e) => {
if (e.target.dataset.action === 'refresh') {
this.refresh();
}
if (e.target.dataset.action === 'pause') {
this.togglePause();
}
});
}
/**
* 开始自动刷新
*/
startAutoRefresh() {
this.timer = setInterval(() => {
this.refresh();
}, this.refreshInterval);
console.log(`⏱️ 自动刷新已启动,间隔: ${this.refreshInterval}ms`);
}
/**
* 手动刷新
*/
async refresh() {
try {
const data = await stockApi.fetchRealtimeData();
this.consecutiveErrors = 0; // 重置错误计数
// 更新指标卡片
dashboard.updateMetric('activeConnections', data.activeConnections);
dashboard.updateMetric('throughput', data.throughput, '/s');
dashboard.updateMetric('avgLatency', data.avgLatency, 'ms');
// 更新图表
dashboard.setKlineOption(data.klineData);
dashboard.setGaugeOption(data.gaugeValue);
dashboard.setLineOption(data.lineData.times, data.lineData.values);
dashboard.setHeatmapOption(data.heatmapData);
// 更新时间戳
dashboard.updateTimestamp();
this.updateConnectionStatus(true);
} catch (error) {
this.consecutiveErrors++;
console.error(`❌ 数据获取失败 (${this.consecutiveErrors}/${this.maxConsecutiveErrors}):`, error.message);
this.updateConnectionStatus(false);
// 连续错误过多,暂停刷新并告警
if (this.consecutiveErrors >= this.maxConsecutiveErrors) {
this.stopAutoRefresh();
this.notifyUser();
}
}
}
/**
* 更新连接状态指示
*/
updateConnectionStatus(connected) {
this.isConnected = connected;
const indicator = document.getElementById('statusIndicator');
if (connected) {
indicator.textContent = '● 连接正常';
indicator.className = 'status-indicator online';
} else {
indicator.textContent = '● 连接异常';
indicator.className = 'status-indicator offline';
}
}
/**
* 切换暂停/恢复
*/
togglePause() {
if (this.timer) {
this.stopAutoRefresh();
} else {
this.startAutoRefresh();
}
}
stopAutoRefresh() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
console.log('⏸️ 自动刷新已暂停');
}
}
/**
* 用户通知(简单版,实际可用Toast/Modal)
*/
notifyUser() {
const msg = `⚠️ 已连续${this.consecutiveErrors}次获取数据失败,已暂停自动刷新。\n请检查网络连接或服务器状态。`;
alert(msg);
}
/**
* 页面卸载时清理
*/
destroy() {
this.stopAutoRefresh();
dashboard.dispose();
console.log('🛑 监控应用已关闭');
}
}
// 启动应用
const app = new StockMonitorApp();
// 页面关闭时自动清理
window.addEventListener('beforeunload', () => {
app.destroy();
});
这里有个设计决策:为什么刷新间隔我设置成3秒而不是1秒?因为股票数据本身不会有那么高的变化频率,3秒是一个在”及时”和”性能”之间的平衡点。如果你真的需要毫秒级的实时数据,应该用WebSocket而不是轮询——但那是另一个话题了。
另外注意consecutiveErrors这个设计。很多人做错误处理时,出错就出错,没有状态追踪。但实际上,一次网络抖动是正常的,连续出错才是问题。我设置了5次连续失败的阈值,触发后暂停刷新并告警——这个逻辑虽然简单,但在生产环境里非常实用。
八、模拟数据:本地开发怎么做
没有真实接口?没关系,我用一个模拟数据生成器让你先跑起来:
// data/mock.js - 开发时替换API调用
function generateMockData() {
const now = Date.now();
const times = [];
const opens = [];
const closes = [];
// 生成最近60个时间点
for (let i = 60; i >= 0; i--) {
const time = new Date(now - i * 3000);
times.push(time.toLocaleTimeString('zh-CN', { hour12: false }));
const basePrice = 3500 + Math.random() * 200;
opens.push(basePrice);
closes.push(basePrice + (Math.random() - 0.5) * 20);
}
return {
activeConnections: Math.floor(100 + Math.random() * 80),
throughput: Math.floor(2000 + Math.random() * 2000),
avgLatency: (8 + Math.random() * 15).toFixed(1),
gaugeValue: Math.floor(40 + Math.random() * 50),
klineData: { times, opens, closes },
lineData: {
times: times.slice(-30),
values: Array.from({ length: 30 }, () =>
10 + Math.random() * 20
)
},
heatmapData: {
xAxis: ['节点A', '节点B', '节点C', '节点D', '节点E'],
yAxis: ['北京', '上海', '广州', '深圳', '杭州', '成都'],
heatData: Array.from({ length: 30 }, () => [
Math.floor(Math.random() * 5),
Math.floor(Math.random() * 6),
Math.floor(Math.random() * 100)
])
}
};
}
// 在app.js中,将stockApi.fetchRealtimeData()替换为:
// return Promise.resolve(generateMockData());
这个模拟数据尽量贴近真实场景——价格有波动、延迟有随机性、服务器负载不均匀。先用这个把界面跑通,再接真实接口会顺利很多。
九、常见问题排查清单
最后,把我在生产环境踩过的坑汇总一下:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 图表完全不刷新 | setInterval没启动或timer被清掉了 |
检查startAutoRefresh是否被调用,console.log确认timer存在 |
| 刷新后图表闪烁 | setOption每次都完整重绘 |
仪表盘用增量更新,K线/折线用setOption(option, true) |
| 数据越来越多导致卡顿 | 没有限制数据点数量 | 折线图只保留最近100个点,K线只保留最近200根 |
| 跨域报错 | 接口域名与页面不同 | 配置CORS或使用反向代理 |
| 移动端布局错乱 | 没有响应式适配 | CSS加media query,图表加resize监听 |
| 页面关闭后还在请求 | 没有清理timer | beforeunload事件里调destroy() |
最容易被忽略的是数据量控制。很多人一开始只展示50个数据点,跑着跑着发现页面越来越卡——因为每次setOption都把历史数据也传进去了。正确做法是,后端只返回最新N条,前端不要自己累积数据。
十、进阶:从轮询升级到WebSocket
如果你的监控要求”真·实时”,3秒的轮询间隔确实太慢了。这时候需要升级到WebSocket:
class WebSocketStockMonitor {
constructor(url) {
this.ws = null;
this.reconnectTimer = null;
this.url = url;
this.listeners = [];
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('🔌 WebSocket已连接');
this.updateConnectionStatus(true);
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleData(data);
this.consecutiveErrors = 0;
} catch (e) {
console.error('数据解析失败:', e);
}
};
this.ws.onclose = () => {
console.warn('🔌 WebSocket已断开,3秒后重连...');
this.updateConnectionStatus(false);
this.reconnectTimer = setTimeout(() => this.connect(), 3000);
};
this.ws.onerror = (error) => {
console.error('WebSocket错误:', error);
};
}
handleData(data) {
// 收到数据后更新图表
dashboard.updateMetric('activeConnections', data.activeConnections);
dashboard.updateMetric('throughput', data.throughput, '/s');
dashboard.updateMetric('avgLatency', data.avgLatency, 'ms');
dashboard.setKlineOption(data.klineData);
dashboard.setGaugeOption(data.gaugeValue);
dashboard.setLineOption(data.lineData.times, data.lineData.values);
dashboard.setHeatmapOption(data.heatmapData);
dashboard.updateTimestamp();
}
disconnect() {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
if (this.ws) this.ws.close();
}
}
WebSocket的好处是服务器推数据,不用客户端主动问,延迟从3秒降到毫秒级。不过代价是服务端要维护长连接,成本也更高。根据实际场景选就好。
写在最后
这篇文章从”数据为什么卡住”这个问题出发,讲了HTML结构、CSS样式、数据请求、图表配置、主逻辑、模拟数据、问题排查,最后还提到了WebSocket升级方案。内容比较多,但每个部分都是我在实际项目里踩过坑之后总结出来的。
你要是照着这个写,大概率能跑起来。如果遇到具体的报错,把错误信息贴出来,我再帮你排查。监控大屏这东西,看着高大上,拆开看其实就是”定时请求 + 更新图表”两件小事——关键是细节做到位,别在setOption和数据结构上偷懒。
