简介:基于React与ECharts的数据可视化大屏项目,面向需要快速搭建展示系统的前端开发者,开箱即用,省去从零搭建的繁琐。项目将ECharts组件化封装进React生命周期,支持折线图、柱状图、饼图等常见图表,通过配置项驱动数据更新,并利用mock数据展示大屏交互逻辑,同时还包含工程化配置与目录拆分。压缩包共有63个文件,以js/jsx源码为主,包含路由、组件、样式、工具、模拟数据等模块,另有JSON配置、图标字体及说明文档,整体大小仅11.1MB,结构清晰便于二次开发。源码按大屏常见的三栏布局组织,左侧、中间、右侧数据模块各自独立,通过mock模拟实时数据流,便于分区域调试与替换真实接口。目前已有1531人学习,无论是学习React项目组织、ECharts集成方案,还是直接改造用于业务数据看板,都具有实用参考价值,是入门数据可视化大屏的不错选择。 “开箱即用”这四个字在数据可视化大屏领域,很多时候是被当成营销话术在用的。搜一下“react+echart数据可视化大屏”,能出来几百个仓库,个个都写“开箱即用”,但真拿过来跑,要么依赖装到一半报错,要么 Demo 只能跑固定分辨率,要么根本接不了实时数据。我去年帮一个智慧园区项目搭数据大屏,前前后后对比了十来个开源模板,最后干脆自己整理了一套 React + ECharts 的大屏基线方案,现在团队里新同学拿到手,改改接口和配置就能上线新的大屏页面。这篇就把这套方案的核心设计思路和完整实现过程拆给你看,里面包含适配方案、图表封装、数据接入和性能优化的完整链路,适合正在做大屏项目、或者准备接这类活的 React 前端工程师。
1. “开箱即用”的真实定义:大屏方案先想清楚这三层问题
先说清楚一个容易被忽略的点:大屏项目和普通后台管理页面的开发逻辑,根本就不是一回事。普通后台关注的是“信息密度”,恨不得一个页面塞下几十个字段;大屏关注的是“视觉表达和信息层级”,同一个指标,放在屏幕左上角和正中央,传达的优先级完全不同。所以一套真正好用的方案,第一步不是写代码,而是把“大屏”这个需求拆成三个层面:视觉层、交互层、数据层。
视觉层解决的是“长什么样”。大屏通常部署在 1080P 或 4K 分辨率的拼接屏、LED 屏上,设计稿一般是 1920x1080,但实际投放的屏幕比例可能是 16:9、16:10,甚至是 3x3 拼接屏。如果方案里没有一套成熟的缩放适配机制,还原度再高的设计稿在真机一拉开就变形。
交互层解决的是“怎么操作”。大屏的交互逻辑跟普通应用完全不同,使用场景大多是“放那展示”,没人会去点按钮。所以交互层要考虑的是:图表自动轮播、定时刷新、全局钻取、指标联动。这些东西如果靠每个页面临时写,成本极高。
数据层解决的是“数据从哪来”。大屏背后通常是一个或多个业务系统的数据聚合,有的需要轮询拉取,有的需要 WebSocket 实时推送,有的需要 REST API 一次性加载。一个“开箱即用”的方案必须把数据接入方式抽象出来,而不是在业务代码里到处写 fetch。
归纳一下,真正值得收藏的大屏模板,至少要满足三个条件:第一,能一键启动,环境变量配好。第二,核心能力被抽取成公共模块,比如图表封装、请求封装、适配方案,而不是散落在页面里。第三,业务变更时,改的是数据和配置,不用动布局和组件代码。
我见过不少团队,一开始图省事去网上找个炫酷模板,结果一个简单的图表数据更新需求,要追着模板里的代码翻半天。所以我的建议是,第一次做大屏,最好只参考别人的视觉设计,代码架构自己搭一遍,这样后续扩展才不被动。
2. 技术选型:为什么是 React + ECharts,而不是其它组合
选型这事儿,没有绝对的对错,只有合不合适。但我做了这么多年可视化项目,React + ECharts 这套组合在大屏场景下,确实有它独特的优势,这种优势来自“数据驱动渲染”这个核心理念的契合。
2.1 相比 Vue 和原生,React 在大屏场景赢在哪
先聊 React。大屏的核心是图表,图表的核心是“数据一变,视图就要跟着变”。React 的声明式 UI 写法和单向数据流,在处理这种场景时非常顺手:你只需要维护一份数据状态,剩下的交给组件去 diff 和更新。Vue 其实也能做到这件事,但它的模板指令体系在自由度上稍弱一些;原生 JS 就更不用说了,图表实例的创建、销毁、resize,全都得自己手动管理,代码量翻倍,出 bug 的概率也翻倍。
另外还有个现实原因:大屏项目的后续维护者,大概率是干 React 的。市面上 70% 以上的中后台系统都是 React 技术栈,大屏作为数据展示的“门面”,往往由后台团队一并维护。用同一套技术栈,人员流动时的交接成本最低。
2.2 ECharts 为什么比其它图表库更适合大屏
图表库的选型,我对比过 ECharts、AntV G2、D3.js、Highcharts 这几个主流方案,各有各的适用场景,但大屏场景下 ECharts 的赢面最大,理由有三点:
第一,配置项体系足够成熟。ECharts 的 option 配置几乎覆盖了所有常见的可视化场景,折线图、柱状图、饼图、地图、雷达图、热力图,全都有,而且在同一个图表实例里可以混搭多个 series,比如柱状图 + 折线图 + 面积图的组合,用 ECharts 写起来就是几行配置的事。
第二,Canvas 渲染的性能足够稳。这里提个容易混淆的概念:SVG 和 Canvas。AntV 的 G2 底层也支持 Canvas,但配置心智比 ECharts 重;D3.js 是直接操作 DOM 和数据绑定,灵活但开发效率低。ECharts 默认用 Canvas 渲染几万个数据点的散点图,帧率依然能保持流畅,这对大屏动效来说很关键。
第三,主题定制能力强。大屏最怕的就是“一眼假”——图表风格跟页面整体设计格格不入。ECharts 支持自定义主题,从背景色、字体、颜色到坐标轴、图例,几乎可以逐项覆盖。我通常会让设计师把大屏的视觉规范整理成一个 JSON 文件,直接喂给 ECharts 作为 theme,这样所有图表自动继承统一风格。
说到底,选型这事儿不用太纠结,React + ECharts 是当前综合成本最低、上限也足够高的一套组合。
3. 搭建一套真正可复用的基线模板:目录结构、适配方案、指令封装
选型定了,接下来就是搭骨架。我习惯把大屏项目拆成四个层次:入口层、组件层、配置层、工具层。入口层负责页面组装;组件层是图表和通用 UI 组件;配置层是图表 option 和主题文件;工具层是适配、请求、格式化等公共方法。这套分层结构能保证从单页大屏扩展到多页大屏时,依然不用改核心代码。
3.1 目录结构,一个可以直接落地的参考
下面是我目前一直在用的目录结构,不算复杂,但每个目录都有明确边界:
src/ ├── pages/ // 页面级组件,一个页面一个大屏 │ └── Dashboard/ │ ├── index.tsx // 页面布局组装 │ └── config.ts // 页面配置(标题、轮播时间等) ├── components/ │ ├── ScreenAdapter/ // 屏幕适配组件 │ ├── CommonChart/ // 图表统一封装 │ ├── BorderBox/ // 大屏边框装饰组件 │ └── DataCard/ // 指标卡 ├── charts/ // 图表配置集合 │ ├── theme.ts // ECharts 主题配置 │ ├── lineChart.ts // 折线/面积图 option 工厂 │ ├── barChart.ts // 柱状图 option 工厂 │ └── pieChart.ts // 饼图/环图 option 工厂 ├── hooks/ │ ├── useECharts.ts // 核心图表 Hook,管理实例生命周期 │ └── useResize.ts // 监听尺寸变化,防抖处理 ├── services/ │ ├── request.ts // 基于 axios/fetch 的请求封装 │ └── api.ts // 接口定义 └── utils/ ├── format.ts // 数字、日期格式化 └── scale.ts // 缩放计算你可能会问,为什么图表配置要单独放到 charts 目录,而不是写在每个组件里?这里的关键考量是:图表配置是“数据描述层”,它是可以和组件解耦的。同一个折线图配置,我可以放在大屏 A,也可以放在报表页 B,区别只是传入的数据不同。把配置做成工厂函数,传数据返回 option,这样图表的复用性和可维护性会高很多。
3.2 大屏适配方案,避开常见的“拉伸变形”坑
适配是大屏项目里最坑的一环,没有之一。很多模板用的是 CSS transform: scale(),把整个页面按设计稿比例缩放,这种做法有两个问题:第一,缩放后页面宽高发生变化,鼠标事件坐标错乱,涉及交互时很难处理;第二,高清屏下会被放大,导致模糊,文字和线条发虚。针对这个问题,我用的方案是“rem + 动态根字号 + 弹性布局”。
核心思路是:设计稿按 1920x1080 来,页面上所有尺寸都用 rem 单位,通过 JS 动态计算根字号,让 1rem 始终等于屏幕宽度除以 19.2。这样页面在不同分辨率下能等比缩放,而且因为用的是 rem 而不是 transform,文字渲染清晰,事件坐标也准。
// utils/scale.ts export function initScreenSize(maxWidth = 1920) { const docEl = document.documentElement const setRem = () => { const width = window.innerWidth // 如果屏幕超出设计稿宽度,按最大宽度锁定,避免内容被过度拉伸 const targetWidth = width > maxWidth ? maxWidth : width docEl.style.fontSize = `${targetWidth / 19.2}px` } setRem() window.addEventListener('resize', setRem) }登录页面只需要在入口调用一次initScreenSize(),然后你在样式里写width: 10rem,就等于是设计稿上的 100px。
在大屏容器布局上,我推荐用 CSS Grid 来做四分、六分、十二分的区块划分,每个区块内的图表再自适应容器。Grid 能很自然地表达“上中下三行、左中右三列”这类大屏经典布局,比 Flex 组合嵌套更清晰。
3.3 核心封装,useECharts 这个 Hook 承载了所有图表逻辑
图表组件的封装是整个方案的灵魂。市面上很多模板把 option 直接写在 useEffect 里,每次更新都重新 setOption,既不优雅又容易出性能问题。我封装了一个useEChartsHook,它负责三件事:实例创建、配置更新、实例销毁。
// hooks/useECharts.ts import * as echarts from 'echarts' import { useEffect, useRef, type RefObject } from 'react' export function useECharts( containerRef: RefObject<HTMLDivElement>, option: echarts.EChartsOption, theme?: string ) { const chartRef = useRef<echarts.ECharts>() const optionRef = useRef(option) // 实例仅创建一次 useEffect(() => { if (!containerRef.current) return const chart = echarts.init(containerRef.current, theme) chartRef.current = chart chart.setOption(optionRef.current) const resize = () => chart.resize() window.addEventListener('resize', resize) return () => { window.removeEventListener('resize', resize) chart.dispose() // 必须显式销毁,否则页面切换后内存会一直涨 chartRef.current = undefined } }, [containerRef]) // 注意依赖数组,不能让主题变化触发重建 // 配置更新时,使用 notMerge 模式替换 useEffect(() => { if (!chartRef.current) return optionRef.current = option chartRef.current.setOption(option, { notMerge: true }) }, [option]) }这里有几个细节值得展开说:
第一,实例创建和配置更新分成了两个 effect。创建只跑一次,更新单独监听 option 变化。如果合在一起,每次配置更新都会销毁再创建实例,视觉上会闪一下,交互事件也会丢失。
第二,setOption用了notMerge: true。默认情况下 setOption 是“合并”模式,也就是说,如果新配置里没有某个系列,旧系列会保留,容易产生“图表不更新”或者“多条旧数据残留”的诡异现象。大屏数据刷新场景里,组件往往会收到完全不同的数据结构,强制替换是更安全的做法。
第三,销毁逻辑必须写。React 18 StrictMode 在开发环境下会故意执行两次 effect,如果不做正确的销毁处理,控制台会报There is a chart instance already initialized on the dom,而且这种错误在高频切换大屏页面时会导致内存泄漏。
图表组件的封装就很简单了,一个壳子,内部调用 useECharts 即可:
// components/CommonChart/index.tsx import { useRef, memo } from 'react' import { useECharts } from '@/hooks/useECharts' const CommonChart = memo(function CommonChart(props: { option: echarts.EChartsOption }) { const containerRef = useRef<HTMLDivElement>(null) useECharts(containerRef, props.option) return <div ref={containerRef} style={{ width: '100%', height: '100%' }} /> }) export default CommonChart用memo包裹组件后,只有 option 引用变化时才触发重渲染,这对大屏这种多图表页面来说,能省下不少无意义的渲染开销。
4. 大屏组件的核心设计:图表配置怎么拆、怎么做联动、怎么加动效
图表配置的拆分,本质上是在“写死”和“过度抽象”之间找一个平衡点。我的经验是:把图表按照“业务语义”而不是“图表类型”来组织。
比如说,“近 7 日访问量趋势图”是一个折线图,“各区域销售额占比图”是一个环形图。我不会直接创建一个 LineChart 组件、一个 PieChart 组件,而是创建一个TrendChart、一个RegionPieChart,它们内部各自调用 CommonChart,再各自维护自己的 option 生成逻辑。这样组件的命名就是业务语言,后续接手的人看到TrendChart就知道它是什么,而不是去猜一个通用组件传了什么配置。
option 生成我用的工厂函数模式:
// charts/lineChart.ts export interface TrendLineConfig { xData: string[] seriesData: number[] title?: string color?: string } export function createTrendLineOption(config: TrendLineConfig): echarts.EChartsOption { const { xData, seriesData, title, color = '#36A9FF' } = config return { backgroundColor: 'transparent', title: { text: title, textStyle: { color: '#E0E8FF', fontSize: 16 } }, tooltip: { trigger: 'axis' }, grid: { left: '4%', right: '4%', top: '12%', bottom: '12%', containLabel: true }, xAxis: { type: 'category', data: xData, axisLine: { lineStyle: { color: 'rgba(255,255,255,0.3)' } }, axisLabel: { color: '#95A4C5' } }, yAxis: { type: 'value', splitLine: { lineStyle: { color: 'rgba(255,255,255,0.15)' } }, axisLabel: { color: '#95A4C5' } }, series: [{ data: seriesData, type: 'line', smooth: true, symbol: 'circle', symbolSize: 6, lineStyle: { width: 3, color }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: 'rgba(54, 169, 255, 0.35)' }, { offset: 1, color: 'rgba(54, 169, 255, 0)' } ]) } }] } }细看会发现,我把大屏常用的一些视觉参数已经内置:网格留白、坐标轴颜色、渐变面积图的上下透明度。这些参数是从几十个大屏项目里沉淀出来的“通用审美”,可以保证刚新建的图表也不会太丑。
关于联动,大屏里最常见的是“点击某个柱状图,联动刷新右边的趋势图”。ECharts 的chart.on('click')可以监听点击事件,但在 useECharts 里还没有暴露这个能力。我的方案是给 useECharts 增加一个事件注册参数,在实例创建后统一绑定:
export function useECharts( containerRef: RefObject<HTMLDivElement>, option: echarts.EChartsOption, config?: { theme?: string onClick?: (params: echarts.ECElementEvent) => void } ) { // 创建时绑定 useEffect(() => { if (config?.onClick) { chart.on('click', config.onClick) } }, []) // 只在创建时绑定一次 // 或者用 ref 保存最新回调,避免闭包陈旧问题 }联动状态统一放在父级页面组件里,用 useState 管理,点击事件触发 setState,其它图表组件收到新的 props 自动 setOption,整个链路就是 React 最擅长的单向数据流。
动效这块,ECharts 自带的 animation 默认是开启的,但大屏场景不建议每个图表都开首屏动画,十几个图表同时播动画的体验其实很乱。我的做法是:主视觉图表(例如中间的大屏地图、核心指标环形图)保留动画,次要图表关闭动画,用animation: false直接跳过,让首屏更快。数据刷新的动画可以用animationDurationUpdate: 300单独控制。
5. 数据接入的三种姿势:静态 Mock、REST 轮询、WebSocket 实时推送
图表配置只是骨架,数据接入是让大屏“活”起来的关键。在我的基线方案里,数据接入抽象成一层 service,页面组件只负责拿到数据后生成 option,至于数据是写死的、轮询来的还是 WebSocket 推来的,页面不关心。这就是依赖倒置的好处。
5.1 静态 Mock:开发调试的加速器
用 Mock 数据开发大屏,效率极高。我通常会在项目里内置一套跟接口字段结构完全一致的 mock 数据,放在mock/目录下,开发时直接 import,联调时只要把 mock 替换成真实接口调用就可以了。
// mock/trendData.ts export const trendData = { code: 0, data: { xData: ['01-01', '01-02', '01-03', '01-04', '01-05', '01-06', '01-07'], seriesData: [820, 932, 901, 934, 1290, 1330, 1320] } }Mock 数据设计有一个容易被忽略的细节:数据量级和真实数据必须接近。如果真实环境是小时级数据、成千上万条,用 Mock 的七天数据是测不出性能和渲染问题的。所以我的 Mock 数据通常包含两种模式:少量演示数据和大量压测数据,小组件开发用演示数据,整体性能验证用压测数据。
5.2 REST 轮询:大屏最常用的数据更新方式
大部分大屏不需要毫秒级实时推送,5 秒或 10 秒一次的轮询完全够用,实现起来也最简单。用setInterval配合 React 的 useEffect 就能实现:
import { useEffect, useState } from 'react' import { fetchTrendData } from '@/services/api' export function usePollingData<T>( fetcher: () => Promise<T>, intervalMs = 10000 ) { const [data, setData] = useState<T>() useEffect(() => { let mounted = true let timer: number const load = async () => { try { const res = await fetcher() if (mounted) setData(res) } catch (err) { // 这里要做错误处理,但不能中断轮询 console.error('[polling]', err) } } load() timer = window.setInterval(load, intervalMs) return () => { mounted = false window.clearInterval(timer) } }, [fetcher]) return data }用的时候只需要传一个返回 Promise 的接口函数,组件就会自动更新:
const trend = usePollingData(fetchTrendData, 5000) const option = useMemo( () => createTrendLineOption({ xData: trend?.data.xData ?? [], seriesData: trend?.data.seriesData ?? [] }), [trend] )有个实操细节:轮询接口返回的数据可能完全相同,比如后端还没更新,此时 useMemo 的依赖trend引用虽然变了,但值是一样的。为了减少无谓的重渲染,可以在 setData 前做一次浅比较,数据结构串行化后相同就不更新。这个优化在大屏图表数量多、轮询频率高的场景下,效果非常明显。
5.3 WebSocket:实时大屏的正确用法
实时展示场景,比如交易监控、运维告警、设备状态,就需要 WebSocket 了。用原生 WebSocket 在 React 里写,核心问题是连接管理和断线重连。我的做法是封装一个useWebSocketHook:
export function useWebSocket(url: string, onMessage: (data: unknown) => void) { const wsRef = useRef<WebSocket>() const onMessageRef = useRef(onMessage) onMessageRef.current = onMessage useEffect(() => { let closed = false let retryTimer: number let retryCount = 0 const connect = () => { if (closed) return const ws = new WebSocket(url) wsRef.current = ws ws.onopen = () => { retryCount = 0 } ws.onmessage = (e) => { try { const data = JSON.parse(e.data) onMessageRef.current(data) } catch (err) { // 解析失败不能导致连接中断 } } ws.onclose = () => { if (closed) return // 指数退避重连:1s, 2s, 4s, 8s... retryTimer = window.setTimeout(connect, Math.min(1000 * 2 ** retryCount, 30000)) retryCount++ } ws.onerror = (e) => { // 一般 onerror 后会触发 onclose,不用重复处理 } } connect() return () => { closed = true window.clearTimeout(retryTimer) wsRef.current?.close() } }, [url]) return wsRef }重连策略里的指数退避很重要,直接固定 3 秒重连的写法在告警风暴场景下,可能把后端连接池打爆。指数退避加上上限限制,兼顾了恢复速度和系统稳定性。
不过还是要说一句:WebSocket 不是所有大屏的标配,如果数据实时性要求在秒级以上,REST 轮询就够了。过度设计是另一种坑。
6. 性能优化与自检清单:第一帧快、动画不卡、内存不涨
大屏页面的性能问题,往往不是单点的,而是体系性的。我总结了大屏优化的三个维度,每个维度都有明确的检查动作和优化手段。
6.1 首屏加载:优先画出一帧
大屏的初始化体验,直接影响“开箱即用”这四个字的含金量。首屏优化最关键的是代码体积控制。ECharts 全量打包的体积在 1MB 以上,如果只用到几个图表类型,按需引入能减少一半以上:
// charts/theme.ts import * as echarts from 'echarts/core' import { LineChart, BarChart, PieChart } from 'echarts/charts' import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, DataZoomComponent } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' echarts.use([ LineChart, BarChart, PieChart, TitleComponent, TooltipComponent, GridComponent, LegendComponent, DataZoomComponent, CanvasRenderer ]) export { echarts }用 Vite 构建时再做一次路由级的代码分割,大屏页访问时才加载图表库。配合Suspense + lazy,首屏到“看到图表”的耗时通常能控制在 1 秒以内。
6.2 运行期渲染:减少无效 setOption
图表数据高频变化时,render 频率和setOption的频率直接决定页面卡不卡。这里有一个容易犯错的点:不要在组件 render 过程中直接调 setOption,因为 React 的 render 可能因为父组件不需要的更新而被触发。正确的做法是用 useMemo 或者 useRef 缓存 option,确保只有数据真正变化时才生成新的 option 引用。
如果图表数量很多,比如一屏超过 20 个图表,可以考虑只对当前可视区域(或重点区域)的图表做高频更新,其余图表降频更新。从产品角度看,角落里的图表慢半秒刷新,用户根本感知不到。
6.3 内存管理:图表实例和事件监听必须回收
这一节重点说大屏开发的“隐形杀手”——内存泄漏。大屏页面通常长时间保持打开,内存只涨不降,跑一天之后页面卡到没法看,这就是典型的泄漏。
最常出现的三个问题:
- ECharts 实例未销毁:组件卸载时没有调用
chart.dispose(),后边的 useECharts Hook 已经规避了这个问题。 - window 监听未移除:addEventListener 之后组件卸载忘了 remove,尤其是在大屏页切走再切回来时,重复注册 resize 监听,会导致图表越用越卡。
- 定时器和 WebSocket 未清理:轮询的 setInterval、WebSocket 连接,必须在 effect 清理函数里 clear 和 close。React 18 开发模式 StrictMode 下,effect 会执行两次再清理一次,如果清理逻辑没写对,开发环境就已经泄漏了。
写大屏代码时,我习惯在 Chrome devtools 的 Performance 面板里录一段 1 分钟的内存变化图,如果内存曲线是阶梯式上升而不是平台期,那一定是泄漏了,顺着组件树排查即可。
6.4 大屏性能自检清单,贴出来直接对照
每次大屏交付前,我都会走一遍下面的清单:
- [ ] 首屏图表首帧是否在 1 秒内出现
- [ ] 全屏缩放后图表文字是否发虚
- [ ] 数据刷新时是否出现旧数据残留
- [ ] 页面内是否超过 5 个图表同时播放入场动画
- [ ] 页面停留 1 小时后,内存曲线是否平滑
- [ ] 切换大屏页面再切回,控制台是否有 ECharts 实例重复告警
- [ ] 弱网环境下,数据请求失败是否有兜底展示
这个清单可以直接当验收标准用,每一条背后都是真实踩过的坑。
7. 大概率会踩的坑和排查链:从图表不更新到整屏白屏
最后分享几个高频问题,按照“现象 → 排查思路 → 根因 → 解决”的结构来,你看一遍之后就能少走弯路。
7.1 图表数据不更新
现象:轮询接口返回了新数据,页面上的图表纹丝不动。
排查链:先打开 React DevTools,看图表组件收到了新 props 吗。如果 props 是新的,再看 option 的引用是否变化了,useMemo 的依赖数组对不对。如果 option 也是新的,那问题大概率出在 useECharts 的 setOption 上——旧的配置对象可能被 ECharts 内部引用了,或者用了 merge 模式导致新旧 series 错乱。
根因:setOption的合并模式。如果新配置里 series 数组缺少了某个系列,merge 模式下旧系列并不会被删除,表现为“数据变了但图表没变”。
解决:setOption 时加{ notMerge: true },强制替换全部配置。
7.2 图表宽度对但高度一直塌陷
现象:页面上图表列出来了,宽度正常,高度却是 0。
排查链:先看容器,父元素有没有高度。然后看 ECharts 的 init 时机——如果图表在容器还在 display: none 时初始化,或者容器的高度由异步数据决定,初始化时拿到的容器高度就是 0,图表自然塌陷。
根因:ECharts 不会自动监听容器尺寸变化,init 时读不到有效高度,后续 setOption 也无法补全。
解决:在异步数据到达后重新调用chart.resize(),或者在组件挂载后、拿到容器真实宽高后再 init。我的 useECharts 在 containerRef 存在后 init,组件挂载完毕一般是安全的,但如果父组件用了条件渲染,需要保证图表容器先渲染出来。
7.3 地图组件一直白屏,控制台也不报错
现象:大屏布局和普通图表都正常,只有地图区域空白,浏览器控制台没有任何错误。
排查链:ECharts 地图的渲染跟普通图表不太一样。打开 Network 面板看地图的 JSON 数据文件是否加载成功——有时候文件跨域拉不下来,就会被静默挂掉。再看一下是否注册了地图数据,ECharts 5 里地图需要显式echarts.registerMap('mapName', geoJson),没注册的情况下地图组件会静默渲染成空白。
根因:绝大多数情况是地图 geoJSON 数据没有加载到位,或者坐标系类型写错。
解决:注册区县级地图时,优先使用简化后的 geoJSON,城市级以上的原版 JSON 文件体积动辄几 MB,首屏加载会被拖垮。地图 geoJSON 数据可以提前请求并缓存,在 createTrendLineOption 之前完成 registerMap 的调用。
7.4 大屏页面切走再切回来,页面整个白屏
现象:大屏和其他页面正常跳转,但切回大屏页时内容丢失。
排查链:先看是不是路由懒加载的组件被重新加载、然后内部某个全局单例没处理好。再检查页面卸载时 ECharts 的 dispose 是否正常调用,如果 dispose 抛错会导致 React 渲染中断,干脆白屏。
根因:最常出现在 useECharts 的 effect 清理函数里,如果 dispose 被重复调用(React StrictMode 下常见),控制台会报错,然后整个页面崩溃。
解决:dispose 前判断实例是否存在:
if (chartRef.current && !chartRef.current.isDisposed()) { chartRef.current.dispose() chartRef.current = undefined }7.5 大屏文字在投影上模糊、锯齿感明显
现象:开发机上看一切正常,部署到大屏电脑上文字边缘发虚。
排查链:这是大屏项目特有的“硬件坑”。先看投放电脑的分辨率和缩放设置,Windows 系统常常把缩放设置成 125% 或 150%,浏览器会按这个缩放渲染页面。再看显卡驱动是否把缩放交给屏幕,部分老式拼接屏处理器不支持浏览器内部的 CSS 缩放。
解决:推送优化方案的时候,我一般建议大屏电脑固定分辨率为 1920x1080、缩放比例为 100%,并且使用 Chrome/Edge 的 kiosk 模式全屏展示。代码层面,用第一章的 rem 适配方案能够消除大部分模糊问题,字体尽量用系统字体,加-webkit-font-smoothing: antialiased改善边缘渲染。
说到底,大屏开发和普通前端开发最大的差别在于“环境不确定性”——你不知道对面那块屏幕是拼接屏还是单屏、系统缩放是多少、显卡有没有做图像增强。所以模板里的适配逻辑一定要写得容错性强,宁可保守也别激进。
最后再分享一个小技巧
上面这套方案我前前后后迭代了半年多,最后沉淀下来一个习惯:把每个项目的公共代码抽到一个独立的 npm 包或者私有仓库里,只暴露页面配置和图表 option 工厂函数,业务方只需要改config.ts和charts/目录下的文件,就能拼出一个新的数据大屏。如果你现在正在做大屏项目,也可以按照这个思路,先搭一个最小的 React + Vite + ECharts 项目,然后一步步把适配、Hook、图表工厂加进去,跑通一个页面之后,再复制成第二个、第三个页面,这套“开箱即用”的基建就真正属于你自己了。
本文还有配套的精品资源,点击获取