ARTICLE DETAIL

资讯详情

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

若依分离版集成ECharts:可视化看板组件化实践与踩坑实录

若依分离版集成ECharts:可视化看板组件化实践与踩坑实录 最近在做若依分离版的一个二次开发项目需求是在生产管理模块里加几个可视化看板把设备状态、订单进度、质量合格率这些数据用图表展示出来。折腾了一周多把ECharts在若依分离版框架里跑通了顺带把图表组件化的封装思路也整理了一遍。这篇接着上一篇学习记录聊聊如何通过组件实现数据可视化给正在做同类需求或者准备上手若依二次开发的朋友做个参考。先交代一下背景。我这次用的是 RuoYi-Vue3 版本前端 Vue 3 Element Plus TypeScript后端 Spring Boot MyBatis Redis。数据源是生产管理模块里的设备状态表、工单进度表、质量检验表。需求方最初的说法很简单看板上能实时反映车间运行情况。听起来一句话真拆开做的时候才发现牵扯到数据来源、图表渲染、权限控制、定时刷新、大屏适配一堆问题。我把自己踩过的坑和最终落地的方案都整理在下面按从设计到实现的顺序讲。1. 项目概述与需求拆解1.1 先搞清楚若依分离版的技术底座若依分离版RuoYi-Vue是目前国内使用量很大的一个前后端分离脚手架基于 Spring Boot 和 Vue 构建。它把登录认证、权限管理、字典管理、操作日志、代码生成这些通用能力都提前做好了二次开发时不需要再从零搭建基础设施。说白了它就是一个已经把地基和水电管道装好的毛坯房你只需要按自己的业务去砌墙、贴砖。我之前用过老的 RuoYi-VueVue2 Element UI版本这次切到 RuoYi-Vue3 也是不得已新项目的前端规范要求 TypeScript。Vue3 版本在组件化、类型推导、编译性能上的优势比较明显但也因此多了一堆 TS 类型上的坑这个后面专门用一节讲。如果你只是做一个内部管理后台Vue2 版本其实够用如果团队已经统一了 Vue3 TS 的规范那直接上 RuoYi-Vue3 更合适。还有一点要提醒若依的生态里还有一个 RuoYi-Plus 增强版多了一些额外的组件和功能但官方分离版的学习资料最多社区案例也最丰富。第一次接触若依的话建议先用分离版把核心逻辑跑通再考虑要不要上增强版。1.2 可视化需求在真实项目里的三种形态接手需求后我没有直接开写图表代码而是先花了一天把需求拆成了三种类型。因为不同形态的处理方式完全不同混在一起做必然返工。第一种是概览型看板核心是让管理者一眼看清全局。比如车间地图上标注每台设备的运行/停机/维修状态旁边放环形图展示订单按时交付率用柱状图对比不同产线的产量。这类页面信息密度高一屏要放四到六个图表但交互很少数据刷新周期可以长一些。第二种是分析型报表核心是筛选和钻取。按时间维度筛选、按班组维度对比、点击某个柱子弹出对应明细数据。这种更接近传统报表的需求但是要嵌在若依的权限体系里跑不同角色看到的范围还不一样。第三种是告警监控型核心是实时性和自动刷新。比如设备温度超限、质检合格率跌破阈值要在看板上高亮弹出提示。这里轮询和状态切换是重点图表本身反而简单。拆完之后我意识到一个共同点如果不做组件化每接一个需求都重新写一遍 ECharts 初始化代码改改数据、调调样式短期内似乎很快但项目里看板数量一旦超过三个维护成本就会成倍上涨。所以方案选型上我直接定了组件化封装的路子。1.3 方案选型为什么选择组件化封装而不是页面内直写ECharts 本身的用法其实不复杂无非就是 init、setOption、resize、销毁这几步。但问题在于它的配置项体系非常庞大一个完整的 option 可能上百行而且不同图表之间 tooltip、legend、grid 这些公共配置高度相似。如果每个页面都从零写一遍代码会极度冗余。举个例子一个简单柱状图的配置就包含 xAxis、yAxis、series、tooltip、grid 五个核心模块折线图又多了 smooth、symbol 之类的字段饼图则完全换了一套配置逻辑。页面一旦多起来你改一个公共样式可能要同时改五六个文件漏掉一个就很尴尬。所以我定下的方案是写一个通用的 BaseChart 组件对外只暴露 dataset、type、loading、optionCover 这几个参数内部统一处理初始化、数据更新、窗口自适应和实例销毁。业务页面不再直接碰 ECharts只需要把数据按约定格式传进来配置一个图表类型剩下的交给组件去处理。技术选型上没有太多纠结直接用了 ECharts没有考虑 D3 或者 AntV。原因也很简单ECharts 社区成熟、文档全、图表类型丰富尤其在大屏场景下地图、关系图、桑基图这些复杂图表的默认表现力很强。团队里其他人也都有 ECharts 的基础换成别的库学习成本会高很多。2. 数据可视化组件的核心设计在写第一个图表之前我先把组件的边界、通信方式、数据格式、权限集成这四块设计好。这一步看着繁琐但做完了之后后面每个业务页面的开发时间基本能缩短一半以上。2.1 组件封装必须划清的三个边界封装组件最怕的就是越写越万能最后什么需求都想往组件里塞组件变成一个大杂烩。我在动手前给自己划了三条边界。第一个边界是组件只负责画图不负责拿数据。数据由父页面通过 props 传入或者由组件内部调用统一的 fetch 函数获取但组件内部绝不放置具体业务逻辑。这样图表和业务完全解耦换个数据源不需要动组件本身。第二个边界是组件对外只暴露必要的事件和方法。我除了透传 ECharts 的 click 等事件之外还暴露了一个 getChart 方法让父页面在极特殊情况下可以拿到图表实例直接操作。这个是兜底方案常规业务不应该依赖它。第三个边界是样式和主题由组件统一管理。图表的配色、字体大小、间距统一收敛在组件的公共配置模块里不允许散落在各个业务页面。这样一个主题调整全局所有图表跟着变不用一个个页面去改。上面这三点听起来抽象落到代码上其实就是在组件内部把 option 的生成逻辑拆成两块一块是图表类型对应的公共配置比如 tooltip、grid、legend另一块是业务数据对应的 series 数据。组件负责把两块合并后交给 ECharts 渲染业务方只关心数据本身。2.2 组件通信设计父传子、子传父、事件解耦Vue 组件通信的方式很多props、emit、provide/inject、Pinia 都可以用。我在这个可视化组件里的设计是这样的父传子用 props传递的数据主要有三个dataset 表示图表数据type 表示图表类型loading 控制骨架屏显隐。dataset 是一个统一格式的数组结构不分柱状图还是饼图type 决定组件内部调用哪个 option 生成函数。子传父用 emit主要事件是 chart-click图表点击、chart-ready图表初始化完成、error数据处理失败。chart-click 特别关键因为很多场景下点击柱状图的某一根柱子需要联动右侧的明细表格刷新。这种交互如果不用事件抛出去父页面就只能通过全局变量传值代码会非常别扭。如果页面里有多个图表需要联动比如总览页面上一个时间范围筛选器要同时控制六个图表我用 Pinia 来做状态同步。筛选条件统一写入 store各个图表组件监听 store 里的变化后各自重新请求数据。这样比事件链式的 A 传 B、B 传 C 要清爽得多调试的时候也能直接在 DevTools 里看到 store 的状态变化。这里有个非常容易踩的坑ECharts 实例一定不要直接放进响应式数据里。一旦把 chart 实例赋值给 ref 或者 reactiveVue 的响应式系统就会去劫持 chart 对象内部的属性很容易触发循环更新甚至控制台报错。正确做法是用 shallowRef 保存实例只把它当作一个普通引用不做深层响应式代理。2.3 数据格式与后端接口的约定组件设计好了前后端数据格式不约定清楚照样乱套。我用的是一个比较通用的 JSON 结构几乎覆盖了我遇到的图表场景{ categories: [1月, 2月, 3月], series: [ { name: 订单数, type: line, data: [120, 200, 150] }, { name: 交付数, type: bar, data: [98, 180, 132] } ] }categories 是 X 轴的类目数据series 数组对应 ECharts 里的一个或多个系列。组件拿到这个结构后根据 type 参数生成对应的 option再把 series 里的数据填进去。这个结构的好处是折线、柱状、饼图都能覆盖。饼图只需要把 categories 当作 legend 的类目series 里的 data 映射成 value 就可以。如果将来要支持双 Y 轴在 series 的某一项里加一个 yAxisIndex 字段组件内部稍微扩展一下就能兼容。这就是提前约定数据格式的好处前端组件和后端接口可以并行开发只要接口返回的结构符合约定前端进度完全不受后端影响。后端这里走的就是若依的普通 REST API接口上加 PreAuthorize 权限注解前端调用用若依封装的 request 工具。整个链路不需要额外处理跨域和 token 传递脚手架自带能力直接吃透这也是用若依做二次开发最省心的地方。2.4 和若依权限体系的深度绑定很多人做若依二次开发时容易忽略一件事菜单权限和接口权限是两套系统。前端路由是通过 v-hasPermi 指令控制按钮显隐后端接口是通过 PreAuthorize 注解控制访问。可视化页面往往涉及多个业务模块的数据所以图表组件背后每一个数据接口都要单独配置权限标识不能因为页面做了权限控制就默认接口也安全了。我在这个项目里的做法是在若依的菜单管理里创建一个生产看板的菜单目录和子菜单权限标识填 production:dashboard:list然后在后端 Controller 对应查询方法上加上 PreAuthorize(ss.hasPermi(production:dashboard:list))。这样没有权限的用户连菜单都看不到即使手动输入路由后端也会返回 403。再说一个小细节v-hasPermi 自定义指令只能作用在真实 DOM 元素上ECharts 渲染出来的 canvas 内部是管不到的。所以如果图表上有导出、刷新这类按钮而且这些按钮要根据权限显隐就把它们放到 ECharts 的 toolbox 配置里同时配合前端的权限判断单独控制。我一开始没注意结果后端接口权限没问题前端却把没有权限的刷新按钮渲染到了 canvas 里很难看。3. 实操过程与核心代码实现设计做完就开始落地。这一节我从零开始写一个可用的 BaseChart 组件配套后端统计接口、页面组装和菜单配置最后讲大屏适配和定时刷新。代码部分是完整的可以直接在若依项目里复用。3.1 从安装依赖到基础图表组件落地先装依赖这里注意版本锁定npm install echarts5 --save我建议按需引入不要全量引入。全量引入会把 ECharts 里所有图表和组件都打包进来体积能大到几百 KB首屏加载明显变慢。按需引入的做法是import * as echarts from echarts/core import { BarChart, LineChart, PieChart } from echarts/charts import { GridComponent, TooltipComponent, LegendComponent, TitleComponent } from echarts/components import { CanvasRenderer } from echarts/renderers echarts.use([ BarChart, LineChart, PieChart, GridComponent, TooltipComponent, LegendComponent, TitleComponent, CanvasRenderer ])然后用 TypeScript 写一个 BaseChart.vue。核心逻辑是接收 dataset 和 type监听 dataset 变化后调用 setOption初始化时绑定 resize 事件卸载时销毁实例并解绑事件。script setup langts import { ref, onMounted, onBeforeUnmount, watch, shallowRef } from vue import * as echarts from echarts/core import type { EChartsType } from echarts/core const props defineProps{ dataset: any type: string loading?: boolean }() const emit defineEmits([chart-click, chart-ready, error]) const chartEl refHTMLDivElement | null(null) const chart shallowRefEChartsType | null(null) const buildOption (dataset: any, type: string) { // 根据 type 生成不同 option这里以柱状图为例 return { tooltip: { trigger: axis }, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: category, data: dataset.categories || [] }, yAxis: { type: value }, series: (dataset.series || []).map((s: any) ({ name: s.name, type: s.type || type, data: s.data || [], smooth: true, barMaxWidth: 32 })) } } const renderChart () { if (!chartEl.value) return if (!chart.value) { chart.value echarts.init(chartEl.value) chart.value.on(click, (params: any) { emit(chart-click, params) }) } chart.value.setOption(buildOption(props.dataset, props.type), true) emit(chart-ready, chart.value) } onMounted(() { renderChart() window.addEventListener(resize, handleResize) }) onBeforeUnmount(() { window.removeEventListener(resize, handleResize) chart.value?.dispose() chart.value null }) watch(() props.dataset, () { renderChart() }, { deep: true }) const handleResize () { chart.value?.resize() } /script template div div refchartEl classbase-chart :style{ height: 360px }/div el-skeleton v-ifloading :rows6 animated classchart-skeleton / /div /template组件里有些细节值得说明一下。watch 里加 deep: true 是因为 dataset 通常是嵌套对象如果父页面在原有引用上直接改字段浅比较会失效。不过 deep watch 在数据量大时有性能开销所以如果每次接口返回的都是全新对象用不到 deep。我后面的写法就是让父页面每次赋值一个新对象这里的 deep 其实可以去掉留着只是为了应对一些粗糙的写法。setOption 的第二个参数传了 true表示 notMerge。意思就是每次设置都完全替换掉旧配置而不是增量合并。这样在图表类型切换或者数据字段变化比较大的场景下不会残留上一次的旧状态下拉导致显示错乱。如果只是更新数据且图表类型不变也可以传 falseECharts 会做平滑的动画过渡。3.2 编写后端统计接口与数据组装前端组件就绪开始写后端接口。数据库查询我用的还是 MyBatisMapper XML 里写统计 SQL。举一个例子统计本月每日的订单数量select idselectDailyOrderCount resultTypejava.util.Map SELECT DATE_FORMAT(order_date, %Y-%m-%d) AS category, COUNT(*) AS count FROM production_order WHERE order_date BETWEEN #{startDate} AND #{endDate} GROUP BY DATE_FORMAT(order_date, %Y-%m-%d) ORDER BY category /selectService 层把查询结果组装成前端约定的 JSON 结构public AjaxResult getDashboardSummary(DashboardQuery query) { ListMapString, Object rows productionOrderMapper.selectDailyOrderCount(query); ListString categories new ArrayList(); ListObject values new ArrayList(); for (MapString, Object row : rows) { categories.add(String.valueOf(row.get(category))); values.add(row.get(count)); } MapString, Object result new HashMap(); result.put(categories, categories); ListMapString, Object series new ArrayList(); MapString, Object orderSeries new HashMap(); orderSeries.put(name, 订单数); orderSeries.put(type, bar); orderSeries.put(data, values); series.add(orderSeries); result.put(series, series); return AjaxResult.success(result); }这段代码逻辑很简单核心工作就是数据形态转换。实际项目里可能要多表查询、多个指标我会在 Service 里组合查询或分别查询后再组装。需要提醒一个老坑从 MyBatis 返回的 Map 里取字段时字段名受数据库列命名策略影响。如果数据库字段是下划线命名比如 order_date而 MyBatis 没有开启驼峰映射取出来的 key 还是 order_date不是 orderDate。所以要么在 SQL 里给字段起别名要么在若依的配置文件里打开 map-underscore-to-camel-case。我一开始就踩了这个前端一直说 categories 为空排查了半天结果是字段名对不上。3.3 页面组装与菜单配置后端接口有了组件也有了把它们组合成一个业务页面就很快了。我在若依的 src/views 目录下创建了 production/dashboard/index.vue页面结构大概是template el-card el-row :gutter16 el-col :span12 BaseChart :datasetorderData typebar :loadingloading / /el-col el-col :span12 BaseChart :datasetdeliveryData typeline :loadingloading / /el-col /el-row /el-card /template script setup langts import { ref, onMounted } from vue import { getDashboardSummary } from /api/production/dashboard import BaseChart from /components/BaseChart/index.vue const orderData ref({ categories: [], series: [] }) const deliveryData ref({ categories: [], series: [] }) const loading ref(false) onMounted(async () { loading.value true const res await getDashboardSummary({}) // res 是若依封装的响应data 字段里是 categories 和 series orderData.value { categories: res.data.categories, series: res.data.series } deliveryData.value { categories: res.data.categories, series: res.data.series } loading.value false }) /script这里要注意若依的 request 封装统一返回 { code, msg, data } 结构code 为 200 才表示成功。所以取数据要用 res.data不要直接拿整个 res 赋给 dataset不然图表组件拿到的结构完全不对。菜单配置走若依后台的系统管理 - 菜单管理。新增目录生产看板再往下加具体菜单路由地址填 dashboard组件路径填 production/dashboard/index菜单类型选菜单权限标识填 production:dashboard:list。配置完让管理员重新登录或者刷新路由菜单就出现在侧边栏了。这里有个新手经常绕不明白的点如果角色没有分配菜单权限即使前端文件存在路由也不会生效没有任何报错页面就是打不开。所以测试时不要只盯着文件看先检查当前用户的角色有没有挂这个菜单。我带过好几个新人他们都说我明明写了页面为什么路由不显示十有八九是漏了角色分配。3.4 大屏适配与定时刷新的处理可视化做出来只是第一步真正让人挠头的是大屏适配和定时刷新。先说大屏。大屏上如果还用手工写死的 px 单位在不同分辨率的屏幕上字体会忽大忽小图表比例也会失调。我的处理方案是页面设定一个基准宽度 1920根节点的 font-size 按实际宽度除以基准宽度来动态计算图表和文字统一用 rem 单位。ECharts 的 option 里凡是涉及字体大小的都用自定义的 px2rem 函数转一遍。这种方案不是唯一选择也可以用 scale 缩放整体布局但对多图联动和自适应高度不太友好。rem 方案在若依项目里实施成本最低只需要在大屏页面入口写一段动态计算根字号代码即可。定时刷新方面核心是数据自动更新但页面不能闪。我在组件外部维护一个 setInterval 定时请求接口每次拿到新数据后先更新 loading 状态再用一个全新的对象覆盖 dataset。这样图表会通过 setOption 自动做过渡动画视觉上不会有白屏或者闪烁。关键点是定时器必须在组件卸载时清理掉。很多人用的是路由钩子但在若依的多页签场景下菜单页可能被缓存也可能被销毁生命周期和普通路由不完全一样。我最终的做法是只在组件实例里管理onMounted 里开定时器onBeforeUnmount 里清理同时监听若依 TagView 的关闭事件手动关闭页签时也主动清理定时器。否则你切到别的菜单再切回来旧的定时器还在后台跑数据一直刷新白耗资源严重的还会导致图表实例冲突。4. 常见问题与排查技巧实录这一节把我在折腾过程中遇到的典型问题整理成一份速查表方便以后直接照着排查。现象可能原因快速处理办法图表白屏容器高度为 0给容器设置 min-height组件加载后再 init图表不更新dataset 引用没有变化每次赋值一个新对象触发 watchTS 编译报错ECharts 类型引用错误用 EChartsType不要用 ECharts图表变形窗口尺寸变化没处理绑定 resize 事件Tab 切换时手动 resize定时器重复请求组件缓存未清理onBeforeUnmount 里 clearInterval接口 403权限标识未配置检查菜单权限和后端 PreAuthorize数据解析为空后端字段名下划线SQL 起别名或开启驼峰映射下面挑几个高频问题详细展开都是实际写过跑过之后才总结出来的东西。4.1 Vue3 和 TypeScript 的报错与类型坑若依 Vue3 版本 TS 最让人头疼的问题集中体现在 ECharts 实例类型的写法上。很多老教程里写的是import * as echarts from echarts const chart refecharts.ECharts | null(null)这句在 Vue3 的 TS 环境里很容易报错因为 ECharts 5 按需引入之后类型入口变成了 echarts/core 里的 EChartsType而不是命名空间下的 ECharts。正确写法是import type { EChartsType } from echarts/core const chart shallowRefEChartsType | null(null)还有一个常见问题是 props 的类型定义。如果直接用 any 定义 dataset虽然省事但后续接口字段一旦变更编译器完全帮不上忙。我建议至少定义一个松散的接口类型把 categories 和 series 的结构固定下来即便某些业务需要额外字段也可以在这个基础上扩展。4.2 图表白屏和 Tab 切换后不显示的怪异现象图表初始化时容器宽度为 0是白屏最常见的原因。在若依的布局体系里菜单切换时页面经常处于隐藏状态隐藏状态下容器宽度天然就是 0ECharts 初始化出来自然什么都画不出来。你切走再切回来如果不主动调用 resizechart 也不会自动恢复。我排查这种问题时的固定套路是先在浏览器控制台打印 chartEl 的 offsetWidth如果是 0说明容器布局还没出来就 init 了。解决方案有两个方向一个是给容器设置最小高度另一个是在 Tab 切换事件里调用 resize()。若依的 Layout 组件有 tab 切换逻辑可以在主页面 watch 当前 TagView 的 path变化时对页面内所有图表实例统一 resize。我在 BaseChart 组件里也对 window resize 做了监听但 Tab 隐藏导致的高度变化不触发 window resize所以必须手动补一发。另外按需引入 ECharts 时漏组件也会导致白屏。比如用了饼图但只注册了 BarChart没有引入 PieChartECharts 会在控制台打警告但图表区域依然是空白。排查时先看控制台有没有这类警告一般一眼就能定位。4.3 组件通信失效的根源有一阵子图表不更新排查了很久最后发现是父页面用响应式对象直接改了内部字段而不是整体替换引用。Vue 的 watch 默认只比较对象的引用地址虽然 deep 监听可以在某些情况下触发但依赖 deep 来感知变化总是不那么可靠。我在项目里定的规矩是接口数据返回后一律生成一个新对象再赋值给 ref让子组件稳定感知变化。比如const res await getDashboardSummary({}) orderData.value { categories: res.data.categories, series: res.data.series }不要写成 orderData.value.categories res.data.categories。前者每次都是新引用后者只是在原对象上改了属性watch 如果不加 deep 就不会触发。这个习惯在数据量大、嵌套层级深的时候尤其重要deep watch 的性能隐患很大。4.4 时序与资源释放问题图表页里最容易埋雷的是 setInterval 的资源释放。我见过一个项目页面上有三个图表每个图表组件内部都开了一个 30 秒的定时器组件被销毁后定时器没有清理结果用户退出页面后接口请求还在继续一天能产生几千条无意义的请求日志。正确的写法是用生命周期钩子成对管理onMounted 里开onBeforeUnmount 里关。如果你用了 keep-alive 缓存组件那么 deactivated 和 activated 也要配合处理。在若依的场景里简单粗暴地每次进入页面都重新初始化、离开页面就销毁反而最不容易出问题。4.5 和若依框架本身相关的几个小坑最后说几个我在集成过程中遇到的若依框架特有的问题。第一个是接口响应结构的兼容问题特别是当后端接口通过网关转发时响应结构可能被包装一层直接取 res.data 会拿到错误的数据结构。稳妥做法是通过 response 拦截器统一处理后再返回或者在接口定义层就做一次解包。第二个是字典翻译。若依前端用 dicts 组件做字典翻译很方便但 ECharts 的图表内部没法直接用这个组件。如果要在图表的 label 或者 tooltip 里显示设备状态运行这种文案得在页面取数据时就把字典标识翻译成中文或者后端直接下发字典标识前端再通过 useDict 映射成文案后传给图表组件。不然图表上显示一堆 0、1、2 的数字领导看了也看不懂。第三个是打包兼容。若依 Vue3 项目默认没装 ECharts装完依赖后运行 npm run dev 可能遇到一些版本兼容警告。ECharts 本身问题不大但如果你装了最新版可能和 vite 的预构建缓存有冲突常见现象是启动后图表区域白屏、控制台报 module 解析错误。解决方法是固定 ECharts 版本清理 node_modules 和 vite 缓存后重新安装基本就能解决。5. 组件化思路的延伸与一点心得这一节不写太多代码聊一聊我在完成第一个可视化页面之后的延伸思考。毕竟做一次项目最后留下来的不该只是一两个页面而是一套可以复制的方法。5.1 从单图组件到可视化页面引擎当项目里图表组件多起来之后我慢慢发现很多页面长得越来越像上方一排筛选器中间几个图表下方一张明细表格。这种页面如果每个都手写模板和逻辑其实是在重复造轮子。我后来把 BaseChart 又升级成了 ChartBoard通过一组 JSON 配置来描述页面结构包括图表类型、位置、数据接口、刷新频率、联动关系。业务人员只需要改配置就能生成一个新的看板页面不需要写前端代码。当然这个抽象不是第一天就做的是在做了四五个看板页面之后总结出来的规律。如果你只是快速做个 demo不建议一上来就建设这种引擎先泡一段时间的重复劳动再判断是否需要抽象更稳妥。5.2 和若依代码生成器的配合使用若依自带的代码生成器可以帮助快速生成单表 CRUD 的前后端代码这个能力在二次开发里非常实用。我的经验是先通过代码生成器把业务表对应的 Service、Mapper、列表管理页面生成出来再在生成的管理页面里嵌入图表组件。这样既能保证基本的增删改查功能可用又能在列表上方提供统计可视化两边互不干扰。比如订单管理页面生成好之后我在页面顶部加了一行图表筛选条件同时控制列表和图表。点击柱状图时通过之前设计的 chart-click 事件可以把列表的查询条件改成对应日期然后去刷新列表。这个交互比较顺滑体验也好。有一点要注意代码生成器生成的页面里查询表单的初始化逻辑是固定的图表加入后需要同步调整查询按钮的事件确保点击查询时同时刷新列表和图表。如果忘了用户会看到列表数据变了、图表没变非常影响体验。5.3 性能优化与懒加载细节可视化页面往往图表多、数据量大性能优化不能等到上线前才做。我这边落地了三个优化动作。第一个是按需引入 ECharts只注册用到的图表和组件打包体积能缩小一半以上。第二个是数据量大的时候开启采样ECharts 的折线图默认采样效果还可以1000 条以上的数据点能保持视觉无损的同时明显降低渲染压力。第三个是用 v-if 控制非核心图表的渲染时机首屏只渲染关键指标等数据返回后再插入其他图表减少首屏白屏等待时间。还有一个容易被忽略的点路由懒加载组件时如果给图表容器设置了 v-if 条件渲染一定要给容器设定一个 min-height否则懒加载的瞬间容器高度可能为 0图表 init 之后是空的后面即使数据来了也不一定渲染。这个在低配服务器上访问大屏的时候尤其容易出现一卡就是白屏用户感知很糟糕。5.4 组件化过程中的一些心得最后说点个人体会。做组件化的初衷从来不是为了显得代码高级而是为了让业务需求变化时你能用最小的代价去响应。我在最开始其实也想着直接把页面做出来拉到但后来随着看板数量增加复制粘贴的成本越来越高才真正理解了封装的意义。组件化也不是一步到位的。我的 BaseChart 前后重构了三次第一次只做了初始化和销毁的封装第二次加入了 loading 和事件透传第三次才加了主题管理和按需渲染。每一次重构都是被真实需求逼出来的不是为了设计模式而设计模式。如果你也在做若依的二次开发我的建议是先找一个小需求完整实现一个图表组件从封装到使用再到改版的过程跑通了再横向推广。这样踩坑的成本最低也最能体会框架能力和业务需求之间的边界在哪里。数据可视化这个需求本身并不复杂复杂的永远是对数据的理解、对权限的控制以及对资源释放和性能的敬畏。把这些问题都处理干净图表本身反而是最简单的那一部分。
返回列表