Skip to content

记一次 ECharts 在侧边栏折叠/展开时不自适应大小的 Bug 排查与修复

问题现象

在项目中,每次点击展开或收起侧边栏时,主容器的宽度发生了变化,页面中的 MaEchartsVue 图表组件却仍然保持原始大小,并没有随容器自动调整尺寸(缩放)。通过审查元素发现,ECharts 其实被定死了原始尺寸的一个固定像素宽度(例如 1581px),并没有继承和更新为父容器改变后的宽度(例如 1782px)。由于宽度异常,部分右侧的图表信息发生了裁切或错位。

原因分析

这个问题主要是由两方面的原因共同叠加导致的:

1. Flex 布局下内部宽元素反撑父容器(CSS 隐式尺寸规则)

在传统的 Flex 或 Grid 布局体系下,弹性容器内部的元素往往带有类似于 min-width: auto 的隐式规则(即永远不会自动压缩到小于其内部强撑子元素的真实尺寸)。 由于 ECharts 在首次渲染时是由底层的 <canvas> 生成了一块确定像素宽度的画布,这就等同于子元素被硬性“撑”开。这导致了外部侧边栏展开挤占空间时,包含 ECharts 的这个层级父容器根本没有变小。这直接抹杀掉了外部布局传递下放进来的尺寸收缩,从而让后续任何 ResizeObserver 都无法检测到自身容器是否真正变小。

2. Vue3 Hook 异步生成的实例与闭包同步获取冲突(核心症结)

在排查一开始,为了尝试让图表跟随 DOM 缩放,我们试图给 chartRef DOM 增加 useResizeObserver 监听来调用 chartInstance.resize()

typescript
let chartInstance: ECharts | null = null
chartInstance = getInstance() // 此处在 onMounted 里同步执行赋值

useResizeObserver(chartRef, () => {
  if (chartInstance) {
    chartInstance.resize()
  }
})

然而,这么做图表却依然完全不缩放! 经过源码排查发现,在 @mineadmin/echarts 提供的 useEcharts hook 中,它对内部真实 ECharts 对象的挂载和初始化是利用了 await nextTick()异步生成的。 这导致我们在当前封装组件的 onMounted 钩子中第一时间同步调用 [getInstance()](file:///d:/work/code/wat-chivvy/web/node_modules/@mineadmin/echarts/dist/index.es.js#40-41) 时,图表在底层还没创建完,我们拿到的竟然是空值(undefinednull),随后又将这个空值永久赋给了普通的内部作用域变量 chartInstance 里。使得后来当父容器实际发生放大,且 useResizeObserver 的确被触发的时候,因为闭包里访问到的 chartInstance 一直是被冻结缓存下来的那个属于早期的 null,这就令 resize() 的指令永远被意外拦截跳过了,导致界面只能保持首次画完的样子。


解决方案

综上,我们需要从 DOM 层限制它不可被内部撑开,并同时修正生命周期闭包下意外导致的方法掉线问题。

行动一:为外层容器追加 min-w-0(等同于 min-width: 0)

通过显式重置 Flex 的最小限制,使得在遇到空间收缩时容器会先顺从外部强硬收缩尺寸。

html
<template>
  <div class="w-full min-w-0"> <!-- 注意这里添加的 min-w-0 非常重要 -->
    <div ref="chartRef" :style="{ height: addUnit(height), width: addUnit(width) }" />
  </div>
</template>

行动二:采用实时的拉取实例执行 resize

绝不能相信组件刚加载瞬间所获取的本地缓存引用(因为它很有可能是空的)。我们要做到在图表发生形变的随时一刻,去内部随时取出最新的有效组件去下达调整大小命令:

typescript
import { useResizeObserver } from '@vueuse/core'

useResizeObserver(chartRef, () => {
  // 注意:要在侦听器触发的回调函数内部去发起获取,保证对象永不过期
  const instance = getInstance()
  if (instance) {
    instance.resize()
  }
})

个人总结

在业务使用 Vue 的图表封装组件开发时,必须要时刻留意以下这两类常见的坑点:

  1. DOM 的宽度继承陷阱:侧边栏收缩并不触发 window.onresize 而是纯粹更改容器大小;普通容器可能会因为 Flex/Grid 隐性继承问题去拒绝强行缩小并导致图表错位,这时候一定要记得给装载其的容器加上特定的约束属性(如 min-width: 0overflow: hidden)。
  2. Hook 里的异步赋值过时引用:Vue 3 中很多第三方库的 Hook 提供的返回值(或者是内部渲染对象)往往都不是实时建立的。如果你把它们取出来储存在本地而不去随用随取,很容易被空引用折磨并吃掉本来正确的逻辑。对于这种带有回调和监听的高频闭包环境,一定要保持在触发时再拉取实例对象。