事情的开端是集团驾驶舱项目的统一改版。有一天产品经理拿着截图来找我:同一块KPI卡片,在A系统里是红色数字,在B系统里是绿色数字;同一类折线图,在销售后台是圆滑曲线带渐变阴影,在经营分析里是直角折线还带闪烁动画。这不是视觉偏好问题——是代码里散落着几十份互相矛盾的ECharts配置。我当时统计了一下,光option就有两百多份,还不包括为了特殊效果写出的“一次性图表”。从那天起,我意识到企业级web开发早就过了“图表能画出来就行”的阶段,真正需要的是一个能统一标准、沉淀规范、经得起迭代的企业级图表组件库。这篇文章记录这个库从0到1的完整实现过程,包括设计规范、分层架构、质量工程、性能优化和推广落地,希望能给同样在搞企业级数据可视化平台的同学一些参考。
1. 从两百份ECharts option说起:为什么必须自建图表组件库
1.1 混乱的现状:一个系统里藏着七种风格
先说背景。我当时所在的前端团队维护着一套大型后台系统,涵盖经营分析、销售管理、库存监控、财务报表等多个子模块。系统经过多年迭代,前端技术栈从最开始的jQuery慢慢迁到Vue 2,后来又局部升级到Vue 3。图表部分一直是最薄弱的环节——几乎所有图表都是“用到哪里画到哪里”,直接从ECharts官网示例复制配置,再按当时的临时需求改一改。
这种做法的直接后果,就是视觉风格完全失控。有的模块用ECharts默认的蓝色主题,有的用定制过的品牌色;有的tooltip带阴影圆角,有的还是默认白底黑字;同一份柱状图数据,在A页面柱子宽度是12px,在B页面变成20px。改版时更是灾难,运营要换品牌主色,我们得拿着正则去代码库里匹配十六进制色值,改完还得逐个页面人工核对,生怕漏掉某个角落的自定义配置。
更深层的问题在于,这些图表代码没有统一的数据格式。同一个“销售额趋势”接口,在不同页面被包装成了三种不同结构的JSON,再各自映射成ECharts的series。后续接手的同事根本不敢动这些代码——你永远不确定改了一个字段映射,会不会影响另外两个隐式依赖相同数据的图表。
1.2 选型对比:为什么不直接用成熟的开箱方案
团队决定做统一图表组件库之后,第一个讨论焦点就是:自己封装,还是直接用现成的企业级组件库?当时我们认真对比了几条路线,我列了个表供大家拍板:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 直接用ECharts裸写 | 灵活、无学习成本 | 无人统一规范,问题依旧 | 一次性的小型图表 |
| Ant Design Charts / S2 | 组件丰富、交互成熟、开箱即用 | 依赖Ant Design体系,定制品牌视觉成本高,自定义组件能力受限 | React栈且愿意接受antd设计语言 |
| Chart.js / ApexCharts | 轻量、简单 | 复杂图表的表达能力弱,企业级大屏场景不够 | 轻量报表场景 |
| 基于ECharts二次封装 | 底层渲染能力最强,可完全控制规范 | 需要投入人力做架构、测试、文档 | 有长期演进计划的中大型团队 |
考虑到我们的核心诉求不是“找一个能画图表的库”,而是“统一企业级数据可视化的规范”,并且要在ECharts强大的渲染能力之上做深度的品牌定制和业务抽象,最终选择了“基于ECharts 5二次封装自研组件库”这条路。技术栈定为Vue 3 + TypeScript + pnpm monorepo,图表渲染仍由ECharts负责,我们专注做规范沉淀和易用性封装。
1.3 边界划分:组件库管什么、不管什么
一个企业级组件库最容易犯的错,就是什么都想做。我的经验是,开工之前必须先划定边界,否则后续每个需求都会吵一架。
我们当时明确了三条边界。
第一,组件库不做数据请求。组件只接收已经就绪的数据,至于数据是从接口来、从Mock来、还是从WebSocket推送来,一概不管。这样组件库和业务请求层彻底解耦,测试也好写。
第二,组件库不追求“大而全的图表类型”。优先覆盖业务中出现频率最高的折线图、柱状图、条形图、饼图、面积图、散点图、雷达图、仪表盘,以及由它们组合出的双轴图、堆叠图等。过于生僻的图表类型(比如桑基图、弦图),保留透传option的能力,但不做成正式组件。
第三,底层渲染引擎的能力要完全保留。我们允许使用者在任何组件上通过option属性传入自定义配置,并且这个配置的优先级最高。也就是说,组件永远不阻碍你玩出花活,只是给了一套默认的规矩。
2. 设计规范先行:把视觉规范翻译成可执行的Token
2.1 图表专属Token的拆解
“高端UI设计”这个词我听过很多次,但落到代码层面,它本质上不是炫技,而是极致的统一。要统一,就得有令牌——Design Token。
一般设计规范里都有色板、字体、间距这些基础Token,但图表组件库需要一套更细颗粒度的图表专属Token。比如轴线颜色、分割线颜色、tooltip背景色、图例文字大小、柱状图圆角、折线图宽度、动画时长,这些在普通业务组件里根本不存在,但对图表观感影响极大。
我们和设计团队一起梳理了一份图表Token清单,分四层:
- 基础层:品牌主色、功能色(成功/警告/错误/信息)、中性色(文字/边框/背景)、字体栈、字号梯度。
- 数据系列层:分类色板(通常8-10个色值,用于不同系列区分)、语义色映射(例如“目标完成率>100%显示品牌色,<80%显示警告色”)。
- 坐标系层:轴线颜色、轴线宽度、刻度线颜色、分割线样式、网格背景色、坐标轴标签字号。
- 交互反馈层:tooltip背景色、tooltip阴影、图例间距、高亮状态色、动画时长和缓动函数。
Token不是写个JSON就完了,关键是让它成为组件库的唯一数据源。我们把它做成一个TypeScript常量模块,再通过一个generateEChartsTheme函数把这些Token映射成ECharts的theme对象。这样,只要改Token,整个组件库所有图表的视觉风格都会跟着变。
// packages/theme/src/tokens.ts export const lightTokens = { colorPrimary: '#3366FF', colorSuccess: '#00B42A', colorWarning: '#FF7D00', colorError: '#F53F3F', colorInfo: '#4080FF', textColorPrimary: '#1D2129', textColorSecondary: '#4E5969', axisLineColor: '#E5E6EB', splitLineColor: '#F2F3F5', tooltipBg: 'rgba(255,255,255,0.96)', tooltipShadow: '0 4px 16px rgba(0,0,0,0.12)', legendTextColor: '#4E5969', seriesColors: ['#3366FF', '#00B42A', '#FF7D00', '#F53F3F', '#4080FF', '#FFC107'], animationDuration: 500, animationEasing: 'cubicOut', } as const;2.2 交互规范:tooltip、图例、动画必须统一
除了视觉,交互层面也需要一套默认规范。比如所有图表默认开启tooltip,并且tooltip内容统一展示“系列名称:数值(单位)”;所有折线图默认显示数据点标记,鼠标悬浮时高亮;所有柱状图默认支持点击选中,并联动右侧数据明细面板;所有图表初次渲染时播放一次入场动画,数据刷新时不做整图重绘动画,只对变化的部分做过渡。
这条“入场动画与刷新动画分离”的规则是后来实践出来的。刚开始所有setOption都带动画,导致数据每5秒刷新一次时整个图表闪个不停,非常干扰视线。后来我们调整了策略:初始渲染开动画,后续更新通过setOption(option, { notMerge: true })关闭动画,只让数值部分的过渡保持短促平滑。
2.3 从设计稿到代码:Token如何落到ECharts配置
Token映射到ECharts,核心是生成一个标准的ECharts theme对象。ECharts的registerTheme接受一个包含color、textStyle、categoryAxis、valueAxis、tooltip等字段的对象,我们只需要把Token分别塞到对应位置即可。
// packages/theme/src/index.ts import * as echarts from 'echarts/core'; import { lightTokens } from './tokens'; export function generateEChartsTheme(tokens = lightTokens) { return { color: tokens.seriesColors, backgroundColor: 'transparent', textStyle: { fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif', fontSize: 12, color: tokens.textColorSecondary, }, categoryAxis: { axisLine: { lineStyle: { color: tokens.axisLineColor } }, axisLabel: { color: tokens.textColorSecondary }, splitLine: { show: false }, }, valueAxis: { axisLabel: { color: tokens.textColorSecondary }, splitLine: { lineStyle: { color: tokens.splitLineColor } }, }, tooltip: { backgroundColor: tokens.tooltipBg, borderWidth: 0, shadowBlur: 16, shadowColor: tokens.tooltipShadow, textStyle: { color: tokens.textColorPrimary }, }, legend: { textStyle: { color: tokens.legendTextColor }, }, }; } export function registerTheme(name: string, tokens = lightTokens) { const theme = generateEChartsTheme(tokens); echarts.registerTheme(name, theme); return theme; }这里有个容易踩的坑:ECharts theme只负责“默认样式”,如果调用方在option里手动指定了color: '#xxx',会覆盖theme。所以我们后来在组件内对option做了一层“默认值合并”,保证用户没有显式配置的字段才从theme取,而不是把用户配置覆盖掉。
3. 三级组件体系:BaseChart引擎、基础图表、复合图表与业务模板
3.1 BaseChart核心引擎:组件库的地基
整个组件库最核心的组件叫BaseChart,所有图表组件都是它的子类或包装。它负责四件事:ECharts实例的创建与销毁、option的响应式更新、容器尺寸变化的监听、以及事件转发。
<!-- packages/charts/src/base-chart/BaseChart.vue --> <template> <div ref="chartEl" class="vc-chart" :style="{ width: '100%', height }"></div> </template> <script setup lang="ts"> import { onMounted, onBeforeUnmount, ref, watch } from 'vue'; import * as echarts from 'echarts/core'; const props = withDefaults(defineProps<{ option: ECBasicOption; theme?: string; height?: string; autoresize?: boolean; loading?: boolean; }>(), { height: '320px', autoresize: true, loading: false, }); const emit = defineEmits(['chart-ready', 'chart-click', ...]); const chartEl = ref<HTMLDivElement>(); let chart: echarts.ECharts | null = null; let resizeObserver: ResizeObserver | null = null; function initChart() { if (!chartEl.value) return; chart = echarts.init(chartEl.value, props.theme); chart.setOption(props.option); emit('chart-ready', chart); } function updateChart() { if (!chart) return; chart.setOption(props.option, { notMerge: true }); } watch(() => props.option, updateChart, { deep: true }); watch(() => props.theme, () => { if (!chart) return; chart.dispose(); initChart(); }); onMounted(() => { initChart(); if (props.autoresize) { resizeObserver = new ResizeObserver(() => chart?.resize()); resizeObserver.observe(chartEl.value!); } }); onBeforeUnmount(() => { resizeObserver?.disconnect(); chart?.dispose(); chart = null; }); </script>这里有几个细节值得展开。
第一,setOption默认是merge模式,如果不传notMerge: true,新旧数据中的残留series不会被清掉,很容易出现“数据从5条变成3条后,图表里还残留着第4第5条”的诡异现象。我们统一在数据更新时使用notMerge: true,全量替换。
第二,ResizeObserver比window.resize更靠谱。以前的方案是在window resize事件里遍历所有图表实例挨个resize,数据量大时性能很差,而且容器尺寸变化(比如侧边栏折叠)根本监听不到。用ResizeObserver精确监听容器尺寸,才是企业级应用该有的做法。
第三,组件销毁时一定要dispose。ECharts实例绑定了大量DOM事件监听,不销毁会造成内存泄漏。这点后面性能章节还会细说。
3.2 基础图表:薄封装的能力
基础图表层是业务用得最多的层,每个组件都是对BaseChart的薄封装。所谓“薄”,意思是尽量不引入复杂的配置逻辑,只做一件事:把业务数据的结构映射成ECharts的series数组。
以折线图LineChart为例。业务传入的是一个统一的数据结构:
interface ChartDatum { name: string; // 系列名 data: Array<{ label: string | number; value: number; [key: string]: any }>; }组件内部把data映射成ECharts需要的{ xAxis.data, series }。这样一来,整个系统的折线图数据格式都统一了,新接入的业务方只要会组装ChartDatum,就能画出一张符合规范的图表。
基础图表还包括BarChart、PieChart、AreaChart、ScatterChart等。每类组件都必须在文档里写清楚“支持哪些props”、“默认开了哪些交互”。比如PieChart默认启用legend展示比例、tooltip显示百分比,还支持showLegend: false关闭图例。
3.3 复合图表:双轴、堆叠、迷你图的实现思路
复合图表是基础图表的排列组合,也是企业级场景里最容易出痛点的地方。我们实现了几个高频的复合组件:
DualAxisChart:双Y轴图表,左边柱状图、右边折线图,处理“销售额”和“同比增长率”这类量纲差异巨大的数据。StackBarChart:堆叠柱状图,自动处理总量、占比、tooltip汇总。GroupBarChart:分组柱状图,支持横向条形模式。MiniAreaChart:迷你面积图,用在表格单元格和KPI卡片里,不需要坐标轴、不需要tooltip,只展示趋势。
复合组件的核心不是“把两个option拼起来”,而是要解决很多细节问题。比如双轴图的最大值、刻度数对齐——如果左右两个Y轴的刻度数不一致,柱状图和折线图就会上下错位,看起来非常业余。我们在DualAxisChart里统一根据“参考轴”计算两侧的最大值,并对刻度数取整:
function alignAxisMax(maxA: number, maxB: number, tickCount = 5) { const stepA = Math.ceil(maxA / tickCount); const stepB = Math.ceil(maxB / tickCount); const step = Math.max(stepA, stepB); return { maxA: step * tickCount, maxB: step * tickCount }; }这类细节如果散落在业务代码里,每个页面都会写得不一致;放进组件库,一次实现,全员受益。
3.4 业务模板层:要不要做,得想清楚
业务模板层是三层架构里最敏感的一层。曾经有业务方提需求:要一个“销售目标达成率”组件,带进度环、带单位、带同比。我们知道这种东西直接做成业务组件,能帮业务方省80%的功夫,但也担心过度抽象——换个业务就废了。
我的取舍原则是:只沉淀“数据结构和展示形式都明确”的业务组件,而且要由通用图表组合而成,而不是写死业务逻辑。比如“目标达成率”组件,底层其实就是PieChart或RingChart加一个中心文本,组件接收target和actual两个数值,内部计算百分比。这样即使换一个业务模块,只要数据模型类似,组件还能复用。
4. 企业级质量工程:测试、文档、版本治理一个都不能少
4.1 单元测试到底测什么
图表组件的单元测试很容易被忽略,因为“画出来的东西”难以直接断言。但我们还是引入了Vitest + @vue/test-utils,同时用mock库把ECharts实例替换成可控的桩对象。核心测试点有三个:
第一,option结构是否正确。传入特定的data和props,断言ECharts收到的option中series数组的长度、第一个series的type、data内容是否与预期一致。这是最有效、也最不容易误报的测试。
第二,交互事件是否正确转发。模拟用户点击图表,断言组件对外emit的事件参数是否完整。我们在BaseChart里做了一个事件桥接,把ECharts的事件对象重整为{ seriesName, dataIndex, value, rawEvent },业务方拿到这个对象直接就能用。
第三,响应式更新是否触发正确的setOption调用。用mock的实例记录调用次数和参数,确认在数据变化时组件执行了正确逻辑。
对了,跑单测的时候一定要mock掉canvas环境,否则会报“canvas is not defined”。我们用的是jest-canvas-mock(Vitest下对应的包是vitest-canvas-mock),在测试setup文件里全局引入一次就行。
4.2 视觉回归测试:把“看起来怪怪的”拦截在发布前
单元测试只能保证逻辑正确,保证不了“好看”。视觉回归测试才是图表组件库质量的大门。
我们选择了Playwright + 截图对比。做法是:维护一个story清单,里面每个story对应一个组件的一种典型配置(比如LineChart默认状态、DualAxisChart双轴状态、暗黑模式状态等)。在CI里启动一个静态服务器,用Playwright逐个访问这些页面,截图保存为baseline。后续每次提交代码,重新截图与baseline对比,像素差异超过阈值就判失败。
这个方案落地时要注意两点。第一,截图环境必须固定,包括浏览器版本、字体、Canvas渲染精度,否则每次跑都会出现无意义的diff。第二,图表有入场动画,截图前必须等待动画结束,最简单的方式是全局关闭ECharts的animation,或者等到chart.on('finished')回调触发后再截图。
我们一开始在CI里遇到了大量噪音:因为系统字体差异,Tooltip里的中文在Mac和Linux上渲染宽度不同,导致几十个story集体报红。后来统一在Docker容器里跑视觉测试,并在环境里预置好中文字体,问题才彻底解决。
4.3 文档与Demo:组件库好用的最后一块拼图
没有完善文档的组件库,根本不能叫企业级组件库。组件写得再好,业务方学不会、不敢用,就是白搭。
文档这块我们做法比较务实,没有强制用Storybook的MDX写一堆花哨说明,而是:
- 每个组件一份独立文档,包含:功能描述、API表格(props/events/slots/supports)、基础用法代码示例、常见问题链接。
- Storybook作为在线Demo环境,每个API变更必须带一个可交互的示例story。
- 单独维护一份《从业务视角看图表组件》的推荐手册,讲什么场景用折线图、什么场景用柱状图、双轴图什么时候不该用——这类贴近实际的知识,反而是业务开发最需要的。
文档写得好不好,直接决定组件库的推广成本。我们后来统计过,文档齐全后,新同学接入新图表的平均时间从原来的半天缩短到半小时。
4.4 版本发布:从Commit规范到自动化发布
企业级组件库一定会有多个团队依赖,版本兼容性极其重要。我们通过一套约定和工具来保证发布质量。
首先,强制约定Commit Message规范(feat/fix/docs/chore等),并且用commitlint校验。其次,用Changesets管理版本号与changelog。每个PR涉及组件变更时,必须附带一个changeset文件,说明是major、minor还是patch变更。发布时由CI读取所有changeset,根据语义化版本自动提升版本号、生成聚合的CHANGELOG,再发布到私有npm仓库。
这套流程看起来很重,但它是“企业级”三个字的基本功。没有版本治理,组件库升级就是一场灾难:某个业务方锁定老版本三年不升,你的新图表格式他永远用不上;或者你某次minor变更不小心破坏了兼容性,一堆业务方在群里哀嚎。有了语义化版本和自动变更日志,至少每个依赖方都能清楚知道自己升级后会遇到哪些变化。
5. 性能与包体积:企业应用最难过的两道关
5.1 按需加载:包体积从1MB降到350KB
企业级组件库如果打包后动辄几MB,业务方接入意愿会断崖式下跌。ECharts 5已经支持按需引入,但很多人用的时候仍然习惯import * as echarts from 'echarts'把整个库带上。我们在组件库里统一改成从echarts/core导入,并只注册用得到的图表、组件和渲染器。
// packages/charts/src/echarts.ts import * as echarts from 'echarts/core'; import { LineChart, BarChart, PieChart, ScatterChart, RadarChart, GaugeChart } from 'echarts/charts'; import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, DataZoomComponent, } from 'echarts/components'; import { CanvasRenderer } from 'echarts/renderers'; echarts.use([ LineChart, BarChart, PieChart, ScatterChart, RadarChart, GaugeChart, TitleComponent, TooltipComponent, GridComponent, LegendComponent, DataZoomComponent, CanvasRenderer, ]);这样整个ECharts从约1MB压缩到350KB左右(gzip后更少)。但要注意,按需注册是一把双刃剑——如果业务方在透传option里使用了一个我们没有注册的图表类型(比如type: 'treemap'),运行时ECharts会直接报错,而且报错信息往往不直观。所以我们在文档里明确列出“已注册图表清单”,并且在API校验时对series.type做白名单检查,给出更友好的错误提示。
5.2 大数据量渲染优化实测
企业数据动辄几万条记录,直接把所有点渲染到Canvas上,再强的机器也会卡。我们在组件库里内置了几项策略:
- 数据采样:ECharts有
sampling: 'lttb',可以对折线图做降采样,保留趋势特征的同时减少绘制点数。默认不开启,数据量超过5000点时自动开启。 - dataZoom窗口:大数据量的场景下滚动查看比全量展示更实用,我们在组件里预留了
dataZoom开关,默认只显示最近30个点,用户通过滑块查看历史区间。 - progressive渲染:对柱状图/散点图开启
progressive: 2000,让ECharts分块绘制,画面响应速度有明显提升。 - 动画降级:数据量超过阈值时强制关闭动画,因为动画在大规模渲染下只会带来卡顿,没有视觉价值。
这些策略最好都做成props暴露出来,让业务方根据场景自行切换默认值。我们踩过一个坑:把sampling默认开启后,用户放大某个局部区间时发现数据点变少了,怀疑数据丢失,其实只是采样算法把峰值过滤了。后来改成数据量阈值触发的方案,并且提供一个shrinkToFit开关,用户自行决定是否启用采样。
5.3 实例管理与内存泄漏排查
这是一个非常容易被忽视的坑。很多业务页面在切换Tab、关闭弹窗后,图表实例仍然残留。表面上页面没崩,但时间一长,内存上涨、卡顿、页面切换变慢全都来了。
我们做了一整套实例管理机制:每个BaseChart在onBeforeUnmount里必须执行resizeObserver.disconnect()和chart.dispose();同时维护一个全局的活跃图表实例Map,在组件销毁时自动移除。排查内存泄漏时,我们会用Chrome DevTools的Memory面板做heap snapshot对比,观察ECharts实例数量是否随页面操作持续上涨。
另外还要注意,组件内如果有window.addEventListener,必须在onBeforeUnmount里对称移除。像“窗口尺寸变化触发所有图表resize”这种想法,在新架构里已经不需要了——ResizeObserver只关心自己所在容器的尺寸变化,性能更好,也不会出现无关图表被误触发resize的问题。
6. 主题定制与暗黑模式:Token带来的自由
6.1 主题API设计
企业级应用往往面临多品牌、多主题的需求。有的集团下有子品牌,每个品牌有独立的品牌色;有的系统需要支持暗黑模式。因为我们在第2章做了Token体系,主题切换的复杂度大大降低。
组件库对外暴露两个核心API:registerVChartTheme(name, tokens)和useVChartTheme()。前者把一份Token映射成ECharts theme并注册;后者是一个Composable,负责当前主题状态的响应式管理。
// 业务方自定义品牌主题 import { registerVChartTheme } from '@vchart/theme'; registerVChartTheme('dark-blue', { colorPrimary: '#1E90FF', seriesColors: ['#1E90FF', '#00CED1', '#FFA500', '#FF6B6B', '#7B68EE'], tooltipBg: 'rgba(13,17,23,0.92)', // ... });业务方在应用入口注册好主题,然后外层组件设置<VChartProvider theme="dark-blue">,所有图表组件都会自动消费这个主题,不需要每个业务页面手动传theme属性。
6.2 暗黑模式实施路径
暗黑模式的落地比预想中麻烦。最初我们以为只要换一套Token就行,实际做下来发现,很多图表之外的地方也需要联动,比如图表容器背景色、tooltip内文字的对比度、图例在暗色背景下的可读性。
我们的做法是:组件库通过VChartProvider接收当前主题,再配合CSS变量控制图表容器外围的视觉样式(比如背景色、边框色、下拉按钮)。ECharts内部的颜色通过注册主题解决,外围的样式通过CSS变量解决,两者双轨并行。
系统主题切换时,我们用matchMedia('(prefers-color-scheme: dark)')监听系统偏好,也提供一个手动theme开关给业务方覆盖。这里有一个细节:切换主题时重新执行registerTheme后,必须对已经渲染过的图表实例调用chart.setOption(option, { notMerge: true })才能刷新视觉,组件内部会监听theme变化自动做这个操作。
暗黑模式下最容易忽略的是“透明度叠加”问题。比如某个系列用了rgba(51, 102, 255, 0.3)作为面积图的填充色,在白色背景下还好,在暗色背景下就变得更深、甚至看不清。所以我们在设计Token时,特意为暗黑模式准备了一套更亮的语义色值,并对seriesColors做了微调,确保对比度达到无障碍标准。
7. 推广落地与踩坑记录:从没人敢用到默认接入
7.1 渐进迁移策略
组件库做出来只是第一步,最难的是让已有的几十个业务模块都切过来。我们的经验是不要搞“一刀切”式迁移,而是用兼容层+灰度推进。
第一步,在新页面和新需求里默认使用新组件库,从源头避免再产生新的“野代码”。第二步,挑两三个最典型、改动范围可控的旧页面做试点,把实际改动量和收益量化给团队看。第三步,提供“兼容模式”——允许业务方直接传入一个完整的ECharts option,组件库只负责创建实例和自动resize,视觉规范暂时不强约束,后续逐步把option里的自定义样式迁移到Token体系里。
这套策略最大的好处是降低了业务方的心理门槛。大家不用一步到位理解整个Token体系,先用起来,再慢慢收敛。
7.2 高频坑位清单
推广过程中我们积累了大量的踩坑经验,这里挑几个最常见的列成清单,方便后来人少走弯路。
| 现象 | 根因 | 解决方案 |
|---|---|---|
| 图表渲染出来是空白/高度为0 | 容器初始化时display:none,宽度为0 | 延迟初始化或容器可见后手动调用resize |
| 弹窗、Tabs里的图表偶尔不渲染 | 容器在组件挂载后才显示,ECharts init时机太早 | 监听容器可见性,或弹窗打开后再创建实例 |
| Tooltip被遮挡/样式被覆盖 | 全局CSS选择器影响了tooltip内部的HTML | tooltip使用appendToBody模式 |
| 切换主题后旧实例没变 | 只注册了新主题,没有重新setOption | 主题变更时dispose并重新init,或调用refresh |
| 数据频繁刷新时图表闪动 | setOption未关闭动画,旧数据被动画过渡 | 数据更新采用notMerge,并关闭transition动画 |
| 用了未注册的图表类型报错 | 按需加载没注册该类图表 | 在统一注册文件里补充并导出清单 |
| 大数据量图表拖动卡顿 | 未开启采样或渐进渲染 | 超过阈值自动开启sampling和progressive |
| 组件被销毁后定时器还在运行 | 忘了清理setInterval/requestAnimationFrame | 在onBeforeUnmount中统一清理 |
最后再分享一个判断组件库成熟度的土办法:看团队里新同学的上手时间。我们的组件库达到稳定状态后,一个刚入职的初级前端,照着文档和demo,第一天就能把一个带tooltip、图例、数据刷新的折线图接入到真实页面。那个时候你会觉得,前面踩过的坑、写过的文档、做过的视觉回归,全部都有了回报。组件库这种东西,不是做到某个版本就结束了,它更像一个产品,需要跟着业务和设计规范一起持续演进。但这恰恰是企业级数据可视化最有意思的地方。