Echarts图表动态更新从Ajax接口调用到页面实时刷新完整实战教程
先说说我踩过的坑
说实话,第一次搞Echarts动态更新的时候,我被坑得不轻。那时候代码写得跟流水账一样,图表要么不刷新,要么刷得页面卡成PPT,要么数据对不上。今天就把我踩过的坑、总结出来的经验,老老实实讲给你听。
咱们今天要做的事情是这样的:页面上放一个Echarts图表,然后定时从服务器拉取最新数据,图表自动更新。听起来简单,但里面的细节多了去了。
项目整体结构
先把目录结构摆出来,让你心里有数:
project/
├── server/
│ ├── index.js # Node.js 后端接口
│ └── package.json
├── client/
│ ├── index.html # 前端页面
│ ├── app.js # 前端逻辑
│ └── styles.css # 样式
└── README.md
后端接口:模拟实时数据源
没有数据源?咱们先造一个。用Node.js + Express搞一个模拟接口,每隔几秒返回不同的数据,这样我们测试的时候不用依赖真实数据库。
安装依赖:
cd server
npm init -y
npm install express cors
后端代码 (server/index.js):
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json());
// 模拟的实时数据生成器
// 假设我们要监控 5 个城市的实时温度
const cities = ['北京', '上海', '广州', '深圳', '杭州'];
// 生成随机温度,基础温度在 15-35 之间波动
function generateTemperature(city) {
const baseTemp = {
'北京': 20,
'上海': 25,
'广州': 30,
'深圳': 32,
'杭州': 24
};
const variation = (Math.random() - 0.5) * 8; // -4 到 +4 的随机波动
return Math.round((baseTemp[city] + variation) * 10) / 10;
}
// 初始数据
let temperatureData = cities.map(city => ({
city,
temperature: generateTemperature(city),
timestamp: Date.now()
}));
// 模拟数据每隔3秒自动更新
setInterval(() => {
temperatureData = temperatureData.map(item => ({
...item,
temperature: generateTemperature(item.city),
timestamp: Date.now()
}));
}, 3000);
// 接口1:获取最新温度数据
app.get('/api/temperature', (req, res) => {
res.json({
code: 200,
data: temperatureData,
updateTime: new Date().toLocaleTimeString('zh-CN')
});
});
// 接口2:获取指定城市的历史数据(用于折线图)
const history = {};
cities.forEach(city => {
history[city] = [];
// 预先生成过去60秒的历史数据
for (let i = 59; i >= 0; i--) {
history[city].push({
time: new Date(Date.now() - i * 1000).toLocaleTimeString('zh-CN'),
temp: generateTemperature(city)
});
}
});
// 定时追加历史数据
setInterval(() => {
cities.forEach(city => {
history[city].push({
time: new Date().toLocaleTimeString('zh-CN'),
temp: generateTemperature(city)
});
// 只保留最近60条
if (history[city].length > 60) {
history[city].shift();
}
});
}, 1000);
app.get('/api/history', (req, res) => {
const city = req.query.city;
if (!city || !history[city]) {
return res.status(400).json({ code: 400, message: '无效的城市参数' });
}
res.json({
code: 200,
data: history[city]
});
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`🚀 后端服务运行在 http://localhost:${PORT}`);
console.log(`📊 温度接口: GET http://localhost:${PORT}/api/temperature`);
console.log(`📈 历史接口: GET http://localhost:${PORT}/api/history?city=北京`);
});
启动服务:
node index.js
前端页面:搭建基础结构
HTML文件 (client/index.html):
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Echarts 实时数据看板</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<header>
<h1>🌡️ 城市实时温度监控</h1>
<div class="status-bar">
<span id="connectionStatus" class="status disconnected">⚫ 未连接</span>
<span id="lastUpdate">最后更新:--</span>
<button id="toggleBtn" onclick="toggleUpdate()">开始刷新</button>
</div>
</header>
<main>
<!-- 柱状图:各城市当前温度 -->
<section class="chart-card">
<h2>各城市当前温度对比</h2>
<div id="barChart" class="chart"></div>
</section>
<!-- 折线图:某个城市的历史温度趋势 -->
<section class="chart-card">
<h2>
温度趋势
<select id="citySelector" onchange="switchCity()">
<option value="北京">北京</option>
<option value="上海">上海</option>
<option value="广州">广州</option>
<option value="深圳">深圳</option>
<option value="杭州">杭州</option>
</select>
</h2>
<div id="lineChart" class="chart"></div>
</section>
<!-- 仪表盘:当前城市的温度详情 -->
<section class="chart-card">
<h2 id="gaugeTitle">北京温度详情</h2>
<div id="gaugeChart" class="chart"></div>
</section>
</main>
<footer>
<p>刷新间隔:<span id="intervalDisplay">3000</span>ms |
请求次数:<span id="requestCount">0</span> |
错误次数:<span id="errorCount">0</span>
</p>
</footer>
</div>
<script src="app.js"></script>
</body>
</html>
CSS样式 (client/styles.css):
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
color: #eee;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 30px;
}
header h1 {
font-size: 2.2rem;
margin-bottom: 15px;
background: linear-gradient(90deg, #00d4ff, #7c3aed);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.status-bar {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
flex-wrap: wrap;
font-size: 0.9rem;
color: #aaa;
}
.status {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85rem;
}
.status.connected {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.status.disconnected {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
button {
padding: 6px 18px;
border: none;
border-radius: 6px;
background: #7c3aed;
color: white;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
button:hover {
background: #6d28d9;
transform: translateY(-1px);
}
button.running {
background: #dc2626;
}
main {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
gap: 20px;
}
.chart-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 16px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
}
.chart-card h2 {
font-size: 1.1rem;
margin-bottom: 15px;
color: #ccc;
display: flex;
justify-content: space-between;
align-items: center;
}
.chart-card h2 select {
padding: 4px 10px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.1);
color: #eee;
font-size: 0.85rem;
cursor: pointer;
}
.chart {
width: 100%;
height: 320px;
}
footer {
text-align: center;
margin-top: 30px;
padding: 15px;
color: #666;
font-size: 0.85rem;
}
前端核心逻辑:这才是重点
现在到了最关键的部分。很多教程在这里就是贴一段代码然后说”很简单对吧”,但实际上这里面有太多的坑。我会把每一步都拆开来讲。
JavaScript代码 (client/app.js):
// ============================================================
// 全局状态管理
// ============================================================
const state = {
isRunning: false, // 是否正在自动刷新
updateTimer: null, // 定时器ID
requestCount: 0, // 请求计数
errorCount: 0, // 错误计数
selectedCity: '北京', // 当前选中的城市
barChart: null, // 柱状图实例
lineChart: null, // 折线图实例
gaugeChart: null, // 仪表盘实例
lineHistory: { // 各城市的历史数据缓存
'北京': [],
'上海': [],
'广州': [],
'深圳': [],
'杭州': []
}
};
// ============================================================
// 初始化图表
// ============================================================
function initCharts() {
// 柱状图
state.barChart = echarts.init(document.getElementById('barChart'));
// 折线图
state.lineChart = echarts.init(document.getElementById('lineChart'));
// 仪表盘
state.gaugeChart = echarts.init(document.getElementById('gaugeChart'));
// 响应式:窗口大小变化时重新调整图表大小
window.addEventListener('resize', () => {
state.barChart && state.barChart.resize();
state.lineChart && state.lineChart.resize();
state.gaugeChart && state.gaugeChart.resize();
});
console.log('✅ 图表初始化完成');
}
// ============================================================
// 接口调用封装(重点:错误处理 + 重试机制)
// ============================================================
async function fetchData(url, options = {}) {
const { retry = 3, delay = 1000 } = options;
for (let attempt = 1; attempt <= retry; attempt++) {
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.code !== 200) {
throw new Error(`业务错误: code=${data.code}, message=${data.message || '未知'}`);
}
state.requestCount++;
updateStats();
return data;
} catch (error) {
console.warn(`[fetch] 第${attempt}次尝试失败:`, error.message);
if (attempt === retry) {
state.errorCount++;
updateStats();
throw error; // 最后一次重试失败,向上抛出
}
// 等待后重试(指数退避:1s, 2s, 4s)
await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, attempt - 1)));
}
}
}
// ============================================================
// 获取并渲染柱状图数据
// ============================================================
async function updateBarChart() {
try {
const res = await fetchData('/api/temperature');
const { data, updateTime } = res;
// 更新最后更新时间
document.getElementById('lastUpdate').textContent = `最后更新:${updateTime}`;
// 状态指示灯
document.getElementById('connectionStatus')
.className = 'status connected';
document.getElementById('connectionStatus')
.textContent = '🟢 已连接';
// 准备柱状图数据
const cities = data.map(item => item.city);
const temps = data.map(item => item.temperature);
// 根据温度设置颜色(低温蓝色,高温红色)
const colors = temps.map(t => {
if (t < 20) return '#3b82f6'; // 蓝色
if (t < 26) return '#22c55e'; // 绿色
if (t < 30) return '#f59e0b'; // 橙色
return '#ef4444'; // 红色
});
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: '{b}: {c}°C'
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: cities,
axisLabel: { color: '#ccc', fontSize: 13 },
axisLine: { lineStyle: { color: '#444' } }
},
yAxis: {
type: 'value',
name: '温度(°C)',
nameTextStyle: { color: '#aaa' },
axisLabel: { color: '#ccc' },
axisLine: { lineStyle: { color: '#444' } },
splitLine: { lineStyle: { color: '#333' } }
},
series: [{
name: '温度',
type: 'bar',
data: temps.map((temp, i) => ({
value: temp,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: colors[i] },
{ offset: 1, color: colors[i] + '88' }
])
}
})),
barWidth: '50%',
label: {
show: true,
position: 'top',
color: '#eee',
formatter: '{c}°C',
fontSize: 12
}
}]
};
state.barChart.setOption(option, true); // true = notMerge,完全替换
} catch (error) {
console.error('柱状图更新失败:', error);
document.getElementById('connectionStatus')
.className = 'status disconnected';
document.getElementById('connectionStatus')
.textContent = '⚫ 连接失败';
}
}
// ============================================================
// 获取并渲染折线图数据
// ============================================================
async function updateLineChart() {
try {
const res = await fetchData(`/api/history?city=${encodeURIComponent(state.selectedCity)}`);
// 更新缓存
state.lineHistory[state.selectedCity] = res.data;
const history = res.data;
// 准备折线图数据
const times = history.map(item => item.time);
const temps = history.map(item => item.temp);
// 找到最高温和最低温,用于设置y轴范围
const maxTemp = Math.ceil(Math.max(...temps));
const minTemp = Math.floor(Math.min(...temps));
const option = {
tooltip: {
trigger: 'axis',
formatter: p => `${p[0].name}<br/>温度: <b>${p[0].value}°C</b>`
},
legend: {
data: [state.selectedCity],
textStyle: { color: '#ccc' },
top: 5
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: times,
axisLabel: {
color: '#ccc',
fontSize: 11,
rotate: 0 // 数据点多的时候可以旋转
},
axisLine: { lineStyle: { color: '#444' } }
},
yAxis: {
type: 'value',
name: '温度(°C)',
nameTextStyle: { color: '#aaa' },
axisLabel: { color: '#ccc' },
axisLine: { lineStyle: { color: '#444' } },
splitLine: { lineStyle: { color: '#333' } },
// 关键:固定y轴范围,避免剧烈跳动
min: Math.max(0, minTemp - 3),
max: maxTemp + 3
},
series: [{
name: state.selectedCity,
type: 'line',
smooth: true, // 平滑曲线
symbol: 'circle',
symbolSize: 6,
sampling: 'lttb', // 大数据采样,避免渲染卡顿
itemStyle: { color: '#7c3aed' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(124, 58, 237, 0.4)' },
{ offset: 1, color: 'rgba(124, 58, 237, 0.02)' }
])
},
data: temps
}]
};
state.lineChart.setOption(option, true);
} catch (error) {
console.error('折线图更新失败:', error);
}
}
// ============================================================
// 获取并渲染仪表盘
// ============================================================
async function updateGaugeChart() {
try {
const res = await fetchData('/api/temperature');
// 找到当前选中城市的温度
const cityData = res.data.find(item => item.city === state.selectedCity);
if (!cityData) return;
// 更新标题
document.getElementById('gaugeTitle').textContent = `${state.selectedCity}温度详情`;
const temp = cityData.temperature;
// 根据温度计算进度(0-50度映射到0-100%)
const percent = Math.max(0, Math.min(100, (temp / 50) * 100));
// 根据温度决定颜色
let gaugeColor;
if (temp < 15) gaugeColor = '#3b82f6'; // 蓝色 - 寒冷
else if (temp < 22) gaugeColor = '#22c55e'; // 绿色 - 舒适
else if (temp < 28) gaugeColor = '#f59e0b'; // 橙色 - 偏热
else gaugeColor = '#ef4444'; // 红色 - 炎热
const option = {
series: [{
type: 'gauge',
startAngle: 200,
endAngle: -20,
min: 0,
max: 50,
splitNumber: 5,
itemStyle: {
color: gaugeColor,
shadowColor: gaugeColor + '80',
shadowBlur: 10,
shadowOffsetX: 2,
shadowOffsetY: 2
},
progress: {
show: true,
width: 18
},
pointer: {
icon: 'path://M12.8,0.7l12,40.1H0.7L12.8,0.7z',
length: '12%',
width: 10,
offsetCenter: [0, '-60%'],
itemStyle: { color: 'auto' }
},
axisLine: {
lineStyle: {
width: 18,
color: [
[0.3, '#3b82f6'],
[0.6, '#f59e0b'],
[1, '#ef4444']
]
}
},
splitLine: {
length: 20,
lineStyle: { color: '#fff', width: 2 }
},
axisTick: {
length: 12,
lineStyle: { color: '#aaa', width: 1 }
},
axisLabel: {
color: '#ccc',
fontSize: 12,
distance: -40
},
title: {
offsetCenter: [0, '20%'],
fontSize: 14,
color: '#aaa'
},
detail: {
fontSize: 36,
fontWeight: 'bold',
offsetCenter: [0, '-5%'],
color: gaugeColor,
formatter: '{value}°C'
},
data: [{
value: temp,
name: state.selectedCity
}]
}]
};
state.gaugeChart.setOption(option, true);
} catch (error) {
console.error('仪表盘更新失败:', error);
}
}
// ============================================================
// 切换城市
// ============================================================
function switchCity() {
const selector = document.getElementById('citySelector');
state.selectedCity = selector.value;
updateGaugeChart();
updateLineChart();
}
// ============================================================
// 更新统计信息
// ============================================================
function updateStats() {
document.getElementById('requestCount').textContent = state.requestCount;
document.getElementById('errorCount').textContent = state.errorCount;
}
// ============================================================
// 核心刷新逻辑
// ============================================================
async function refreshAll() {
// 并行请求三个图表,速度更快
await Promise.all([
updateBarChart(),
updateLineChart(),
updateGaugeChart()
]);
}
// ============================================================
// 控制刷新开关
// ============================================================
function toggleUpdate() {
const btn = document.getElementById('toggleBtn');
if (state.isRunning) {
// 停止刷新
clearInterval(state.updateTimer);
state.isRunning = false;
btn.textContent = '开始刷新';
btn.className = '';
console.log('⏹️ 刷新已停止');
} else {
// 开始刷新
state.isRunning = true;
btn.textContent = '停止刷新';
btn.className = 'running';
// 立即执行一次
refreshAll();
// 设置定时器,每3秒刷新
state.updateTimer = setInterval(refreshAll, 3000);
console.log('▶️ 刷新已开始,间隔 3000ms');
}
}
// ============================================================
// 页面加载完成后初始化
// ============================================================
document.addEventListener('DOMContentLoaded', () => {
initCharts();
// 初始数据加载
refreshAll();
console.log('🎉 页面已就绪,点击"开始刷新"按钮启动实时更新');
});
关键细节解释
上面代码里有几个地方特别重要,我单独拎出来讲,因为这些都是我踩过的坑:
1. setOption 的 notMerge 参数
state.barChart.setOption(option, true); // true = notMerge
这个 true 很关键。Echarts 的 setOption 默认是合并模式(merge),意思是新配置会和旧配置叠加。对于动态更新的图表,每次数据完全变了,用合并模式会出现奇怪的问题——旧的数据点可能还会显示,或者样式混乱。传 true 表示完全替换,每次都是全新的图表。
2. Promise.all 并行请求
await Promise.all([
updateBarChart(),
updateLineChart(),
updateGaugeChart()
]);
三个图表的数据是独立的,没必要等第一个请求完再发第二个。用 Promise.all 可以同时发出三个请求,速度大概是串行的三倍。
3. 指数退避重试
await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, attempt - 1)));
网络不可能永远稳定。如果服务器抽风了,立刻重试可能只会让情况更糟(服务器压力更大)。指数退避的意思就是:第一次失败等1秒,第二次失败等2秒,第三次失败等4秒。这样既给了重试的机会,又不会把服务器打爆。
4. sampling: 'lttb' 大数据采样
sampling: 'lttb'
折线图如果数据点太多(比如60个点在30秒内),浏览器渲染会卡。lttb 是一种采样算法,它能自动筛选出最能代表数据趋势的点,去掉那些冗余的点,性能提升很明显。
5. y轴固定范围
min: Math.max(0, minTemp - 3),
max: maxTemp + 3,
如果不固定y轴范围,每次数据变化时,坐标轴的刻度都会跟着跳,图表看起来会”抖动”。提前算一下数据的范围,稍微留点余量,视觉体验会好很多。
运行效果
把前后端都跑起来之后,你会看到:
- 柱状图每3秒自动更新一次,五个城市的温度实时变化
- 折线图显示选定城市过去60秒的温度曲线
- 仪表盘显示当前城市的温度,颜色随温度变化
- 状态栏实时显示连接状态、请求次数和错误次数
打开浏览器控制台,你会看到类似这样的日志:
✅ 图表初始化完成
🎉 页面已就绪,点击"开始刷新"按钮启动实时更新
▶️ 刷新已开始,间隔 3000ms
如果某个请求失败了,你会看到:
[fetch] 第1次尝试失败: HTTP 503: Service Unavailable
[fetch] 第2次尝试失败: HTTP 503: Service Unavailable
柱状图更新失败: HTTP 503: Service Unavailable
进阶技巧
方案A:用 WebSocket 替代轮询
如果你的场景对实时性要求很高(比如秒级甚至毫秒级),轮询就不合适了——它总是有延迟,而且浪费带宽。这时候应该用 WebSocket:
// 前端:建立 WebSocket 连接
const ws = new WebSocket('ws://localhost:3000');
ws.onopen = () => {
console.log('WebSocket 已连接');
document.getElementById('connectionStatus')
.className = 'status connected';
document.getElementById('connectionStatus')
.textContent = '🟢 WebSocket已连接';
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// 直接更新图表,不需要主动请求
applyData(data);
};
ws.onerror = (error) => {
console.error('WebSocket 错误:', error);
};
ws.onclose = () => {
console.log('WebSocket 已断开,尝试重连...');
// 3秒后重连
setTimeout(() => {
location.reload();
}, 3000);
};
// 后端:推送数据
const WebSocket = require('ws');
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws) => {
console.log('新客户端连接');
// 连接后立即推送一次数据
ws.send(JSON.stringify({
type: 'temperature',
data: temperatureData
}));
// 每秒推送一次最新数据
const timer = setInterval(() => {
ws.send(JSON.stringify({
type: 'temperature',
data: temperatureData
}));
}, 1000);
ws.on('close', () => clearInterval(timer));
});
方案B:用 requestAnimationFrame 做更流畅的动画
let animationFrameId = null;
function animateTransition(currentData, newData) {
// 停止上一次的动画
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
const duration = 500; // 动画时长500ms
const start = performance.now();
function step(timestamp) {
const progress = Math.min((timestamp - start) / duration, 1);
const eased = easeInOutCubic(progress); // 缓动函数
// 在两个数据之间插值
const interpolated = currentData.map((val, i) =>
val + (newData[i] - val) * eased
);
// 更新图表
state.barChart.setOption({
series: [{ data: interpolated }]
});
if (progress < 1) {
animationFrameId = requestAnimationFrame(step);
}
}
animationFrameId = requestAnimationFrame(step);
}
function easeInOutCubic(t) {
return t < 0.5
? 4 * t * t * t
: 1 - Math.pow(-2 * t + 2, 3) / 2;
}
方案C:处理大数据量的节流
如果请求很频繁,但图表更新不需要那么快(比如每100ms请求一次,但图表1秒更新一次就够了):
let lastUpdate = 0;
const throttleMs = 1000; // 最少1秒更新一次
function throttledRefresh() {
const now = Date.now();
if (now - lastUpdate < throttleMs) return; // 还没到时间,跳过
lastUpdate = now;
refreshAll();
}
// 定时调用,但实际更新最多1秒一次
setInterval(throttledRefresh, 100);
常见错误排查
问题1:图表不更新
先检查三件事:
- 后端接口是否通?在浏览器直接打开
http://localhost:3000/api/temperature看看有没有数据返回 - 前端有没有报错?打开控制台看 Network 和 Console 面板
- 定时器有没有启动?检查
state.isRunning是否为true
问题2:图表闪烁/抖动
这通常是 y轴没有固定范围导致的。检查是不是漏了 min/max 配置,或者数据波动太大了。可以加个平滑处理:
// 用移动平均平滑数据
function smoothData(data, window = 3) {
return data.map((_, i) => {
const start = Math.max(0, i - Math.floor(window / 2));
const end = Math.min(data.length, i + Math.floor(window / 2) + 1);
return data.slice(start, end).reduce((a, b) => a + b, 0) / (end - start);
});
}
问题3:内存泄漏
如果页面长时间运行不刷新,图表实例没有销毁的话会有内存问题。加上这个:
// 组件卸载时清理
window.addEventListener('beforeunload', () => {
if (state.updateTimer) clearInterval(state.updateTimer);
state.barChart && state.barChart.dispose();
state.lineChart && state.lineChart.dispose();
state.gaugeChart && state.gaugeChart.dispose();
});
问题4:跨域问题
如果你前端和后端不是同一个域名(比如前端在 localhost:8080,后端在 localhost:3000),会报 CORS 错误。解决办法有两个:
- 后端加 CORS 中间件(前面代码里已经加了)
- 或者用 Vite/Webpack 的 proxy 配置转发请求
总结
这个教程涵盖了从后端接口到前端渲染的完整链路。核心要点就三个:正确的请求封装、合适的图表配置、良好的错误处理。
Echarts 的动态更新其实不难,难的是在真实项目中处理各种边界情况。网络会断、数据会异常、浏览器会卡顿,把这些问题都考虑进去,你的图表才能在生产环境里稳稳地跑起来。
有什么不清楚的地方,随时可以问。代码都写清楚了,跑起来看效果是最直观的学习方式。
