ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue 3 项目 ECharts 实战:从安装到组件封装,搞定柱状图与折线图

Vue 3 项目 ECharts 实战:从安装到组件封装,搞定柱状图与折线图 最近后台管理系统和前台上页面的图表需求真的特别多尤其是柱状图和折线图这对“老搭档”。群里好几个朋友都在问 Vue 项目里到底怎么把 ECharts 用好有人卡在图表不显示有人不知道怎么动态更新数据还有人被打包体积整得头大。正好我之前在项目里从头到尾搞过一遍从最基础的环境搭建到一个组件里同时塞下柱状图和折线图、再做到能复用能扩展踩了不少坑也总结了一些实用经验。这篇文章我就把整个过程掰开揉碎了写出来从安装依赖到组件封装再到常见问题的排查全部给你捋清楚。不管你是刚入门前端想抄作业还是已经在项目里集成过但想优化一下这篇内容应该都能给你一些可以参考的东西。1. 整体设计先在脑子里把图表拆开想清楚再动手1.1 为什么是 ECharts以及 Vue 和 ECharts 怎么配合先回答一个最基础的问题做数据可视化图表为什么首选 ECharts。市面上的图表库不少有 Chart.js、AntV G2、Highcharts还有国外的 D3。ECharts 的优势在于中文文档完善、社区活跃、开箱即用组件丰富而且它对数据的处理逻辑很符合中国人的使用习惯——直接丢一个 options 对象进去它就能自己渲染出一整套图表来不需要像 D3 那样从底层一步步构建。在 Vue 项目里用 ECharts核心思路其实不复杂HTML 里放一个容器 divJS 里调用 ECharts 的 init 方法在这个 div 上初始化实例然后调用 setOption 传入配置项来渲染图表。听起来很简单但如果直接把 ECharts 的初始化逻辑写在组件里用一次写一遍项目一复杂就会变成一团乱麻。所以我的习惯是在项目里封装一个通用的图表组件把数据、配置项、实例生命周期这些都管理好。这样做的好处是清晰、可维护性强后期图表多了也可以统一维护公共配置。1.2 选型思路全量引入还是按需引入安装 ECharts 之后第一个要考虑的问题就是你以什么方式引入它。ECharts 支持全量引入和按需引入两种方式它们的区别主要体现在打包体积上。全量引入的方式特别简单在 main.js 里import * as echarts from echarts然后全局挂载到 Vue 原型上或者在需要的组件里直接引入使用。这种方式适合中小型项目因为不用关心模块依赖随便用哪种图表都没问题。缺点是打包体积会大不少ECharts 完整包差不多有 1MB 左右gzip 之后也有 300 多 KB对性能要求严格的项目来说不算友好。按需引入则需要从 ECharts 的模块里手动引入需要的图表和组件比如import * as echarts from echarts/core; import { BarChart, LineChart } from echarts/charts; import { GridComponent, TooltipComponent, LegendComponent } from echarts/components; import { CanvasRenderer } from echarts/renderers;然后通过echarts.use([...])注册。这种方式的打包体积会小很多但每次要用新图表类型时要记得手动加注册模块容易漏。我的建议是如果是个人项目或小团队内部系统全量引入能省很多心如果是大型项目、对首屏加载有要求务必用按需引入。1.3 组件封装思路从“能用”到“好用”上面说了我倾向于封装一个通用图表组件。但这个组件到底怎么设计才叫“好用”我的标准是父组件只要传配置和数据不用管 ECharts 实例怎么创建和销毁组件内部自动处理窗口 resize、组件卸载时的销毁支持不同图表类型之间的切换数据的更新能自动反映到图表上。说白了就是要把 ECharts 的初始化逻辑、生命周期管理、数据监听这些脏活累活全部封装起来让使用者只关心配置项里的业务数据。这篇文章后面会用柱状图和折线图两个实例来展示怎么实现这个组件的设计思路。2. 从零到一环境准备、安装依赖与项目初始化2.1 Vue 项目初始化与 ECharts 安装我这里用 Vue 3 Vite 来演示因为这是目前新项目的主流组合比我早期用 Vue CLI 要轻快很多。如果你还在用 Vue 2 也没有关系代码思路是通用的后面我也会提到两个版本之间的微小差异。创建项目npm create vitelatest chart-demo -- --template vue cd chart-demo npm install然后安装 EChartsnpm install echarts --save安装完成后确认一下 package.json 里有echarts: ^5.x.x这里注意一下ECharts 5 的 API 跟 4 基本兼容本文示例代码在 4 和 5 上都可以跑。装完之后先不要急着写代码先理清楚 ECharts 在 Vue 里的生命周期该怎么管这是最关键的否则后面容易出现图表不显示、错乱、内存泄漏这些问题。2.2 ECharts 实例生命周期init、setOption、resize、disposeECharts 实例的生命周期对应到 Vue 组件的生命周期其实就四个关键节点init在组件挂载mounted 或 onMounted后拿到渲染容器 DOM 节点调用echarts.init(dom)创建实例setOption创建实例后调用传入配置项渲染图表。之后数据变化时随时可以再次调用 setOption 来更新resize当浏览器窗口大小改变时需要调用chart.resize()让图表自适应dispose在组件卸载beforeUnmount 或 onBeforeUnmount时调用chart.dispose()销毁实例释放内存。这几个节点里最容易被新手忽略的是最后一步 dispose。如果组件里创建了图表实例但组件销毁时没有释放页面里频繁切换路由就会出现内存泄漏时间一长页面会越来越卡。我自己早期就吃过这个亏在后台管理系统里疯狂切换菜单最后浏览器直接卡死。2.3 一个最小可运行的基础示例先来一个最简单的示例帮助你验证环境是否正常。在项目里新建一个组件SimpleChart.vuetemplate div refchartRef stylewidth: 100%; height: 400px;/div /template script setup import { ref, onMounted, onBeforeUnmount } from vue; import * as echarts from echarts; const chartRef ref(null); let chartInstance null; onMounted(() { chartInstance echarts.init(chartRef.value); chartInstance.setOption({ title: { text: 最简单的柱状图 }, xAxis: { type: category, data: [Mon, Tue, Wed, Thu, Fri, Sat, Sun] }, yAxis: { type: value }, series: [ { name: 销量, type: bar, data: [120, 200, 150, 80, 70, 110, 130] } ] }); }); onBeforeUnmount(() { if (chartInstance) { chartInstance.dispose(); } }); /script这里有几个关键注意点。容器 div 必须要有明确的宽度和高度。很多新手第一次配 ECharts 时设置了width: 100%但没给高度结果页面上什么都看不到。因为 ECharts 初始化时无法拿到容器尺寸就会提示Cant get DOM width or height。我习惯给容器一个固定高度比如上面代码里的height: 400px或者通过 CSS 设置高度。echarts.init必须在 DOM 挂载之后调用所以放在onMounted里面而不是setup函数体里。如果你在setup同步代码里调用 init此时chartRef.value还是 null拿到不 DOM 节点。3. 核心图表实现用真实场景做柱状图和折线图3.1 柱状图实战展示每门课程男女选修人数这里我直接用之前网上看到的“使用柱状图展示每门课程男女选修人数”这个题目来做一个真实案例很有代表性因为这种带图例和分组柱状图的场景在教务系统、考勤系统里特别常见。假设我们有数据课程有体育、音乐、美术、编程每门课程有男生和女生人数。柱状图要展示出来需要把 series 数组里定义两个柱子系列一个男生一个女生并通过 xAxis 的 data 来指定分类名称。template div refchartRef stylewidth: 100%; height: 400px;/div /template script setup import { ref, onMounted, onBeforeUnmount } from vue; import * as echarts from echarts; const chartRef ref(null); let chartInstance null; const courseData { courses: [体育, 音乐, 美术, 编程], male: [48, 15, 12, 60], female: [20, 40, 55, 18] }; onMounted(() { chartInstance echarts.init(chartRef.value); chartInstance.setOption({ title: { text: 每门课程男女选修人数, left: center }, tooltip: { trigger: axis, axisPointer: { type: shadow } }, legend: { data: [男生人数, 女生人数], bottom: 10 }, xAxis: { type: category, data: courseData.courses }, yAxis: { type: value, name: 人数 }, series: [ { name: 男生人数, type: bar, data: courseData.male, itemStyle: { color: #5470c6 } }, { name: 女生人数, type: bar, data: courseData.female, itemStyle: { color: #ee6666 } } ] }); }); onBeforeUnmount(() { chartInstance chartInstance.dispose(); }); /script这里对比一下单系列柱状图和多系列分组柱状图的差别。单系列柱状图 series 里只需要一个对象多系列就是再添加一个对象同时在 legend 里配置数据来源。ECharts 会根据 series 的 name 自动匹配 legend 的 data所以图例里要显示的每一项都要和 series 里的 name 对应上否则图例可能不显示或者显示不全。你可能已经注意到了我这里给柱状图柱子加了固定的颜色。实际上 ECharts 有一套默认的颜色板如果不手动设置色彩也能自动分配好看的颜色。但很多时候业务上对颜色有统一要求比如男生蓝色、女生红色这种场景就需要用 itemStyle.color 来控制了。还有一个经常被问到的需求是“柱状图的柱子能不能用渐变色”。答案是可以用echarts.graphic.LinearGradient。只需要把颜色配置改成itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: #83bff6 }, { offset: 1, color: #2f8cf7 } ]) }这个渐变方向参数的意思是从上到下x10, y10, x20, y21颜色从浅蓝过渡到深蓝。想要左右渐变就改前两个参数。这个在很多管理后台的首页大屏里用得很频繁视觉上比纯色高级不少。3.2 折线图实战带数据标签和面积效果的折线图说完柱状图紧接着看折线图。折线图最常见的业务场景是趋势展示例如近七天用户访问量、近一年销售额趋势、温度变化等。我还记得网上热搜词里有“echarts折线图x轴刻度”这个需求非常多人在折线图 x 轴显示上踩坑所以我下面重点说一下刻度相关的配置。先来一个常见的折线图配置展示某商店一周的销售额趋势template div reflineChartRef stylewidth: 100%; height: 400px;/div /template script setup import { ref, onMounted, onBeforeUnmount } from vue; import * as echarts from echarts; const lineChartRef ref(null); let lineInstance null; onMounted(() { lineInstance echarts.init(lineChartRef.value); lineInstance.setOption({ title: { text: 一周销售额趋势, left: center }, tooltip: { trigger: axis }, legend: { data: [销售额], bottom: 10 }, grid: { top: 60, left: 60, right: 30, bottom: 50 }, xAxis: { type: category, data: [周一, 周二, 周三, 周四, 周五, 周六, 周日], boundaryGap: false }, yAxis: { type: value, name: 金额元 }, series: [ { name: 销售额, type: line, data: [1200, 1800, 1500, 2200, 2800, 3500, 4000], smooth: true, symbol: circle, symbolSize: 8, lineStyle: { width: 3 }, areaStyle: { opacity: 0.2 }, label: { show: true, position: top } } ] }); }); onBeforeUnmount(() { lineInstance lineInstance.dispose(); }); /script这个配置我已经带上了不少常用选项逐一解释smooth: true让折线变成平滑曲线视觉上更柔和如果不设置或者为 false就是标准的折线在一些对数据精度要求较高的场景其实标准折线更好平滑曲线会有一定的误导性。我一般看业务要求纯展示趋势用 smooth财务数据尽量别用。symbol: circle和symbolSize: 8控制数据点标记的样式。如果不想要数据点可以把 showSymbol 设置为 false默认情况下数据点密度大时 ECharts 会自动隐藏部分标记这个细节官方文档里有说明。label.show: true是在每个数据点上方显示数值。数据点多的时候不建议开启图形上会显得很挤数据点只有 7 个我经常开启信息传递更直接。areaStyle是给折线下面添加面积填充效果适合展示“量级”的趋势感。这里opacity: 0.2表示半透明填充不会遮挡网格线。关于折线图 x 轴刻度的常见问题有时候数据点少x 轴却显示得很稀疏或者数据点多了刻度重叠。这时候需要配置 xAxis 的axisLabel属性axisLabel: { interval: 0, // 强制显示所有刻度 rotate: 30 // 旋转 30 度防止重叠 }interval: 0表示强制展示所有刻度标签。它的默认值是auto当标签数量过多时会自动跳过一部分这会导致有些分类在 x 轴上不显示。rotate则是让文字斜一点显示适合分类名称比较长的场景比如“2024年第一季度”这种。顺带提一下热搜里出现的“python每隔一段时间画折线图”和“graphpad 画双 y 轴图柱形图和折线图”这些其实是不同工具里的需求但在 ECharts 里也有对应的双 Y 轴实现。我后面用一整节专门扩展讲一下柱状图和折线图混搭的场景因为这种组合图在管理后台特别实用。4. 组件化进阶封装一个可复用的图表组件4.1 设计思路把配置和逻辑分离前面两个例子都是把 echarts.init 和 setOption 直接写在业务组件里代码简单直观但有一个问题如果页面里有多张图表或者同一个图表组件在多个页面复用重复代码就非常多了。我更推荐的做法是封装一个通用的图表组件让它同时支持柱状图和折线图甚至支持后续扩展其他图表类型。设计要点是不管要渲染什么类型的图表父组件只需要传一个完整的option配置对象进来子组件负责初始化、更新和销毁。如果父组件有特殊的 resize 行为或事件绑定再通过 props 暴露开关。这种设计说白了就是把“跟图表相关的脏活”全部收进来让业务组件里只保留“这个图表长什么样、数据是什么”。后期如果要从柱状图换成折线图只需要改父组件传进来的 option业务组件无需变动。4.2 通用图表组件的完整代码实现下面是一个我目前在项目里用的通用图表组件我做了一些简化保留核心逻辑template div refchartRef :style{ width: width, height: height }/div /template script setup import { ref, onMounted, onBeforeUnmount, watch, onUnmounted } from vue; import * as echarts from echarts; const props defineProps({ option: { type: Object, required: true }, width: { type: String, default: 100% }, height: { type: String, default: 400px }, theme: { type: String, default: }, watchResize: { type: Boolean, default: true } }); const emit defineEmits([chart-click]); const chartRef ref(null); let chartInstance null; let resizeHandler null; // 初始化图表 function initChart() { if (!chartRef.value) return; chartInstance echarts.init(chartRef.value, props.theme); chartInstance.setOption(props.option); // 图表点击事件暴露出去 chartInstance.on(click, (params) { emit(chart-click, params); }); } // 更新图表 function updateChart() { if (!chartInstance) return; chartInstance.setOption(props.option); } onMounted(() { initChart(); if (props.watchResize) { resizeHandler () chartInstance chartInstance.resize(); window.addEventListener(resize, resizeHandler); } }); // 监听配置变化 watch(() props.option, updateChart, { deep: true }); onBeforeUnmount(() { if (resizeHandler) { window.removeEventListener(resize, resizeHandler); } if (chartInstance) { chartInstance.dispose(); chartInstance null; } }); defineExpose({ getInstance: () chartInstance, resize: () chartInstance chartInstance.resize() }); /script这个组件有几个细节值得展开说。onMounted里调用了initChart这是必须的因为要在 DOM 渲染完成之后拿到容器节点。window.addEventListener(resize, resizeHandler)是为了让图表在浏览器窗口大小变化时自适应。watch里的{ deep: true }是关键。因为父组件传进来的 option 对象很可能内部某个数组的值发生变化而不是整个对象被替换。如果不开启深度监听ECharts 就不会感知到数据变化。我见过很多人在这里踩坑数据变了图表就是不刷新排查了半天最后发现是 watch 的深度监听没开。defineExpose是为了让父组件能通过 ref 拿到图表实例实现一些高级操作比如myChartRef.value.resize()或者myChartRef.value.getInstance()来调用原生 API。4.3 使用这个通用组件来渲染柱状图和折线图上面封装好组件后业务里使用它就很清爽了。比如我们要渲染一门课程选修人数的柱状图template div BaseChart :optionbarOption height420px chart-clickhandleChartClick / /div /template script setup import { computed } from vue; import BaseChart from /components/BaseChart.vue; const courseData { courses: [体育, 音乐, 美术, 编程], male: [48, 15, 12, 60], female: [20, 40, 55, 18] }; const barOption computed(() ({ title: { text: 课程男女选修人数, left: center }, tooltip: { trigger: axis }, legend: { data: [男生, 女生], bottom: 10 }, xAxis: { type: category, data: courseData.courses }, yAxis: { type: value }, series: [ { name: 男生, type: bar, data: courseData.male, itemStyle: { color: #5470c6 } }, { name: 女生, type: bar, data: courseData.female, itemStyle: { color: #ee6666 } } ] })); function handleChartClick(params) { console.log(点击了图表, params.name, params.value); } /script渲染折线图也一样只要换掉 option 里 series 的类型和 xAxis data 就行。比较关键的一点是我用了computed来生成 option这样当courseData的数据从接口返回后computed 会重新计算触发子组件的 watch从而执行 setOption 更新图表。这是 Vue 3 Composition API 的一个经典使用模式数据和配置分离配置随着数据变化自动更新。如果你用的是 Vue 2这个设计的思路同样成立。Vue 2 里用计算属性或者直接 watch option然后调用子组件的方法更新图表。区别在于 Vue 2 的 template 里更习惯用:optionbarOption传 prop子组件里用 watch 监听逻辑其实是一致的。5. 组合图实战双 Y 轴柱状图和折线图混搭5.1 业务场景与配置思路很多后台报表页面都要展示“数量和金额”这种量纲不同的数据。比如统计某商城每个月的订单量和销售额订单量可能是几千上万销售额可能是几十万上百万数值差了好几个数量级。如果放在同一个 Y 轴上订单量的柱子会被销售额的折线“压扁”几乎看不到变化趋势。这个场景下就要用双 Y 轴左侧 Y 轴显示订单量右侧 Y 轴显示销售额柱状图和折线图混搭在一起展示。ECharts 实现这个配置其实很优雅核心就是在 yAxis 数组里定义两个 Y 轴然后每个 series 通过yAxisIndex指定自己挂在哪个 Y 轴上。在 ECharts 中实现某个月的订单量和销售额组合图option 配置如下const mixOption { title: { text: 订单量 销售额趋势, left: center }, tooltip: { trigger: axis }, legend: { data: [订单量, 销售额], bottom: 10 }, grid: { top: 60, left: 60, right: 60, bottom: 50 }, xAxis: { type: category, data: [1月, 2月, 3月, 4月, 5月, 6月] }, yAxis: [ { type: value, name: 订单量, position: left }, { type: value, name: 销售额万元, position: right, splitLine: { show: false } } ], series: [ { name: 订单量, type: bar, yAxisIndex: 0, data: [1200, 1800, 1500, 2200, 2800, 3500], itemStyle: { color: rgba(84, 112, 198, 0.7) } }, { name: 销售额, type: line, yAxisIndex: 1, data: [45, 60, 55, 85, 100, 130], smooth: true, symbolSize: 8, lineStyle: { width: 3, color: #ee6666 } } ] };把这段 option 传入 BaseChart 组件就能同时渲染出一根柱子和一条折线挂在不同量纲的两个 Y 轴上数据趋势一目了然。需要特别注意的是右侧 Y 轴的splitLine.show: false这样设置之后网格线只由左侧 Y 轴生成避免两边都画网格线导致视觉上线条太多。5.2 组合图中几个高频坑点结合我实际开发和网上被大量讨论的问题组合图有几个高频坑我列一下方便你排查时快速定位柱状图柱子宽度太窄。当柱状图和折线图共用坐标系时ECharts 为柱状图分配的类目宽度可能会变得很窄尤其是 x 轴分类多的时候。解决办法是在柱状图 series 里明确设置barWidth比如barWidth: 20或者设置barMaxWidth: 30来限制最大宽度避免柱子宽到变形。数据堆叠导致数值翻倍。如果你的多个 series 都设置了stack: totalECharts 会把它们垂直堆叠在一起显示。如果组合图里你不想堆叠一定不要设置 stack 属性。很多人从网上抄了一段代码没注意到 stack 的存在结果柱状图和折线图“加起来”了数值完全乱掉。图例点击后导致图表显示异常。这个不算 bug而是图例的联动交互。ECharts 默认图例点击后可以隐藏对应的系列如果你觉得这个交互容易让领导误触可以在 legend 里配置selectedMode: false来禁用它。5.3 关于双 Y 轴的扩展多个 Y 轴的类型适配双 Y 轴的玩法不止于柱状图 折线图比如热搜词里提到的箱线图甚至雷达图有时候也需要配合折线图或柱状图使用。ECharts 5 对多坐标轴的支持已经很完善了核心思路就是yAxis 是数组series 通过 yAxisIndex 指定轴各轴之间互不干扰。箱线图boxplot怎么做呢如果后端算好了五个分位数最小值、下四分位、中位数、上四分位、最大值ECharts 可以直接通过 type: boxplot 来渲染。需要注意数据格式ECharts 的 boxplot 接受类似[min, Q1, median, Q3, max]的数组形式如果你的后端算出来的字段名不一致需要做一层映射转换。这个需求其实经常被问到网上的教程也不少值得单独写一篇但这里就不再展开核心的引用方式跟柱状图折线图是一样的。6. 踩坑实录从渲染失败到性能优化的排查全过程6.1 图表不显示八成是容器尺寸的问题前面我反复强调容器要有宽高因为这是新手遇到“图表一片空白”最大的原因。ECharts 初始化时如果拿不到容器的宽高会直接在控制台报错Cant get DOM width or height.排查方法也很简单右键页面上容器 div选择“检查”然后看 Computed 样式里 width 和 height 是否都有值。如果父元素用了 flex 布局且没有明确高度子元素设置 height: 100% 很可能算出来是 0。解决方案是给图表容器一个具体的高度或者在 flex 布局下给父级设置确切的 flex-basis。还有一种情况是图表容器一开始是隐藏的比如用了 v-if、v-show 或者 tabs 组件切换初始状态容器display: none此时 ECharts init 后尺寸就是 0等容器显示出来时图还是空白。这个问题的解决办法有几种用 v-if 控制可以不渲染图表组件等切换显示后再挂载或者用chart.resize()强制刷新尺寸。我个人更推荐第一种因为 v-if 天然规避了“在隐藏 DOM 上初始化”的问题。6.2 数据更新后图表不刷新分清 setOption 的合并机制这个坑出现的频率也极高。我见过很多同行接口数据回来了直接把数组 push 了一下发现图表纹丝不动。原因可能有几个第一没有触发子组件的 watch。因为 option 是引用类型如果你只修改 option 内部某个数组里的元素而没有重新赋值Vue 可能无法触发监听。我在封装的 BaseChart 里使用了deep: true深度监听来解决这个问题但如果你在自己组件里手写的 watch 没有配 deep就很容易出现“改了数据但图表不更新”。第二setOption 的合并机制。ECharts 的 setOption 默认是“合并式”更新不是“覆盖式”更新。什么意思呢如果新传入的 option 里没有某个 config 字段它会保留旧值。这在很多时候是好事因为只需要传入变化的数据即可。但如果你希望完整替换可以给 setOption 传第二个参数notMergetruechartInstance.setOption(newOption, true);这个参数在从柱状图切换成折线图时特别有用。比如你用同一个图表实例先渲染柱状图再切成折线图如果不传 notMerge旧的柱子数据可能导致图表展示错乱。6.3 大屏项目图表卡顿关闭动画与合理使用 dataZoom做可视化大屏或者数据密集的折线图时常遇到图表交互卡顿的情况。这种场景下有两个优化方向第一个是关闭动画。ECharts 默认渲染完数据后有动画效果数据量不大时很流畅可当数据点到几千甚至几万条时动画会让渲染变慢。在 option 顶层设置animation: false即可或者只在初始化时开动画视觉效果animationDuration: 0, animationDurationUpdate: 0,第二个是使用sampling属性。折线图数据量极大时可以给 series 设置sampling: lttbECharts 会对数据进行降采样保留整体趋势同时大幅减少需要绘制的点从而提升性能。series: [ { type: line, sampling: lttb, data: largeData } ]这里lttb是 “Largest-Triangle-Three-Buckets” 的缩写是一种比较经典的折线降采样算法能较好地保留折线的视觉形状。如果图表数据是 10 万级别的实时动态数据流这个配置的优势会非常明显。6.4 按需引入时报错对应关系没注册按需引入 ECharts 时最常遇到的问题就是报错Component series.bar not exists. Load it first.意思是某图表类型没有注册。使用 ECharts 的按需引入方案时记得把常用组件一次性注册齐全。我自己在项目里是按下面这样封装的// echarts.js import * as echarts from echarts/core; import { BarChart, LineChart, PieChart, RadarChart } from echarts/charts; import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, DataZoomComponent, MarkLineComponent } from echarts/components; import { CanvasRenderer } from echarts/renderers; echarts.use([ BarChart, LineChart, PieChart, RadarChart, TitleComponent, TooltipComponent, GridComponent, LegendComponent, DataZoomComponent, MarkLineComponent, CanvasRenderer ]); export default echarts;在 main.js 里引入这个封装的 echarts 文件然后全局挂载组件的使用方式跟全量引入一致。如果后面遇到报错说某个组件不存在就回到这里补注册一个模块。时间长了积累出自己的配置文件后续项目里直接复制就能用。6.5 全量引入打包体积太大从依赖层面优化如果项目使用 Webpack 或 Vite 打包全量引入 ECharts 会产生一个很大的 chunk首屏加载明显变慢。除了按需引入还可以配合打包配置的静态拆包优化。Vite 里可以在vite.config.js配置build: { rollupOptions: { output: { manualChunks: { echarts: [echarts] } } } }这样 ECharts 会被单独拆成一个文件。配合浏览器缓存用户第二次访问时图表相关资源就不会重新下载了。如果项目里同时存在 ECharts 和其他大型依赖这个方法可以有效压缩首屏的 JS 请求体积。不过按需引入还是最推荐的。我做过的几个项目里从全量改成按需引入后echarts 相关代码体积从 1MB 左右降到了 400KB 上下效果立竿见影。7. 常见问题速查表与日常开发小技巧7.1 常见问题速查表整理一个速查表对应的排查顺序也一并写在里面方便遇到问题时直接对照问题现象可能原因解决方案图表不显示容器没有宽高检查 CSS给容器设置具体 height图表不显示在隐藏元素上初始化使用 v-if 控制显示后再渲染数据更新图表不刷新watch 未开启深监听BaseChart 的 watch 加deep: true新旧图表数据叠加setOption 合并机制传第二个参数notMergetrue柱状图柱子太窄类目过多导致分配宽度不足设置barWidth或barMaxWidth折线图 x 轴标签重叠标签文字过多设置axisLabel.rotate或interval页面切换后卡顿实例未销毁onBeforeUnmount 里调用 dispose打包后体积过大未做按需引入使用 echarts/core 按需注册这张表覆盖了我日常问的最多的八类问题你可以把它贴到项目文档里当排查手册用。7.2 开发效率小技巧用 ref computed 管理图表配置在 Vite Vue 3 项目里我用得最顺手的模式是“ref 数据 计算属性生成 option”。比如有一个筛选条件切换不同课程时图表应该显示对应课程的数据const selectedCourse ref(体育); const courseData reactive({ labelSet: [体育, 音乐, 美术, 编程], dataMap: { 体育: { male: 48, female: 20 }, 音乐: { male: 15, female: 40 }, 美术: { male: 12, female: 55 }, 编程: { male: 60, female: 18 } } }); const barOption computed(() { const data courseData.dataMap[selectedCourse.value]; return { xAxis: { type: category, data: [男生, 女生] }, yAxis: { type: value }, series: [ { type: bar, data: [data.male, data.female], itemStyle: { color: #5470c6 } } ] }; });模板里用 v-model 绑定一个下拉框选中课程后 selectedCourse 变化computed 重新计算 optionBaseChart 里的 watch 检测到 option 变化后自动调用 setOption。整个链路完全响应式不需要手动调用任何更新方法代码既好读又好维护。7.3 图表之间的联动与事件绑定有些管理后台需要图表之间联动比如点击左边柱状图的某个柱子右边折线图就要显示这个分类的详细趋势数据。这种事情如果用原生 ECharts 写要在业务组件里获取图表实例然后绑定事件。有了 BaseChart 组件可以借助chart-click事件把点击参数抛出来function handleChartClick(params) { // params.name 是柱状图的类目名比如 体育 // params.seriesName 是系列名比如 男生 // params.value 是当前柱子值 selectedCourse.value params.name; }然后右边折线图的 option 依赖 selectedCourse 重新计算就实现了联动。这个模式用 ref 和 computed 表达起来特别自然我觉得这也是 Vue 3 带给图表开发体验最大的提升之一。8. 组件扩展方向从柱状图折线图到更多图表类型8.1 封装 RadarChart、PieChart 等扩展BaseChart 组件不只适用于柱状图和折线图只要传进来的 option 合法它就能渲染出任何 ECharts 支持的图表类型。原因是它只负责最底层的 init/resize/dispose/setOption至于渲染什么图表完全是 option 里 series 的 type 决定的。比如要渲染一个雷达图option 里 series 类型写成radar即可饼图写成pie即可。我在项目里经常用这套组件渲染基础柱状图、折线图、饼图、雷达图和表格混合页面复用率非常高。有一段时间不少后台项目都做数据驾驶舱里面经常要同时展示十几个图表。如果用 BaseChart 这种组件整个页面代码会非常整洁模板部分是一堆 BaseChart 组件排列脚本部分是一堆 option 的计算属性后面维护时也方便通过拆分模块来管理每个图表的配置。8.2 ECharts 5 与 Vue 3 的响应式陷阱最后说一个 ECharts 5 Vue 3 结合最容易踩的响应式陷阱。ECharts 内部其实是直接把 option 对象拿来用的如果你把reactive声明的数据直接传给 setOptionECharts 5 中 option 里的数组会被 Proxy 代理。正常情况下这没问题因为 ECharts 能识别这类对象并拿到原始值。但某些边缘情况下比如你在自定义 tooltip formatter 里使用了传入的数据可能会拿到 Proxy 对象导致JSON.stringify或者深比较时行为异常。解决方案是在把 option 传给图表之前做一次深拷贝或者使用 computed 返回全新的对象字面量。我在 BaseChart 组件里就是在 setOption 内部调用了一个工具函数做浅层拆解const rawOption JSON.parse(JSON.stringify(props.option)); chartInstance.setOption(rawOption);不过说实话数据量不大时这一步可以不用因为深拷贝大数组会浪费性能。只有当你在自定义函数里遇到了 Proxy 相关奇怪行为时再加这个兜底。这也是“按需优化”而不是“永远优化”的典型场景。8.3 从“会做”到“做好”图表组件的设计权衡做一个图表组件其实和做任何需求一样都会有“能用”和“好用”的差距。能用是渲染出来了好用则要考虑图表自适应resize 的防抖处理组件销毁时的事件移除与实例释放配置项变化的合并策略对 loading 状态、空数据状态的处理对主题切换的支持。以 loading 为例ECharts 原生提供了chart.showLoading()和chart.hideLoading()。在 BaseChart 组件里我会加一个loadingpropwatch( () props.loading, (val) { if (!chartInstance) return; if (val) { chartInstance.showLoading(); } else { chartInstance.hideLoading(); } } );这样父组件在请求接口时传:loadingtrue图表上方就会显示一个转圈加载动画。这个交互在数据量大的报表页里非常实用否则屏幕空着用户还以为系统卡死了。空数据状态也是一样的道理如果接口返回的 series data 是空数组图表会显示一个空坐标轴。更好的体验是提示用户“暂无数据”。这个可以通过设置graphic元素或者 title 副标题来实现但要注意内部的细节处理我一般是在父组件里判断数据为空时展示自定义的空状态 div而图表组件本身只负责渲染这样职责更单一。这些设计细节其实就是“多做一步”的事情但恰恰是这一步拉开了普通开发者和资深开发者的差距。我在实际项目里把所有图表需求都收敛到这种结构之后开发速度提升了不少后期需求变更时也基本只需要改 option很少再去动组件内部的逻辑了。另外还有个使用体验上的小技巧在配置 tooltip 时加上confine: true可以防止 tooltip 溢出图表容器尤其当图表位于页面边缘时特别有效。对我这种经常做后台系统的来说这个属性几乎每次都加算是默认配置了。
返回列表