Echarts使用之容器高度坍塌问题处理

1,535 阅读4分钟

最近接了一个老项目维护工作,技术栈是Vue3+ts+Element-Plus。其中有一个场景问题觉得蛮有意思,给大家分享出来。

问题起因

有一个仪表盘页面,其中一个模块是展示一个tab,每个tab里有若干echarts图表,在开发时因为Element-Plus的机制,默认为渲染未展示的tab-pane(通过设置display:none实现tab-pane隐藏)

image.png

未展示的tab-pane会设置样式为display:none

这就为echarts的初始化带来的一定的麻烦,因为echarts图形初始化时要求容器必须有物理宽、高,如果没有则会造成画布初始化尺寸异常。下图是问题表现示意:

Kapture 2024-07-01 at 18.32.17.gif

DevTools的控制台同步输出了echarts的警告信息:

image.png

解决方案

既然是因为高度坍塌造成echarts渲染异常,那是不是可以采取一种办法,让echarts等待一会儿加载,直到容器宽高完全正常后再执行渲染呢?
这其实就是典型的懒加载场景,顺着这个思路,实现了下面两种懒加载方式。

被动懒加载

在上面的实现中,为了提高组件的复用性,我将echarts图表封装成了独立的组件,比如下面的LineChart:

// LineChart
<template>
  <div class="line-chart" ref="container"></div>
</template>

<script lang="ts">
import { ref, nextTick, onMounted } from "vue";
import echarts from "@/utils/echarts";
import { lineOption } from "./constant";

export default {
  name: "LineChart",
  setup() {
    const container = ref<HTMLDivElement>();

    const mountedCallback = async () => {
      if (container.value) {
        const instance = echarts.init(container.value);
        await nextTick();
        instance.setOption(lineOption);
      }
    };

    onMounted(mountedCallback);

    return {
      container,
    };
  },
};
</script>

<style lang="scss" scoped>
.line-chart {
  padding: 10px 20px;
  width: 100%;
  min-height: 400px;
}
</style>

被动的意思是渲染组件无法控制自己的渲染时机,而是由父组件进行控制的。
通过查阅 Tabs的文档,发现了其支持通过设置tab-panelazy属性实现tab-pane懒加载的目的:

<template>
  <div class="tab-echarts">
    <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
      <el-tab-pane label="LineChart" name="LineChart">
        <LineChart />
      </el-tab-pane>
      <el-tab-pane label="RadarChart" name="RadarChart" lazy>
        <RadarChart />
      </el-tab-pane>
      <el-tab-pane label="BarChart" name="BarChart" lazy>
        <BarChart />
      </el-tab-pane>
    </el-tabs>
  </div>
</template>

上面给非默认tab-pane设置了lazy属性,让其延迟加载。其效果如下:

Kapture 2024-07-01 at 18.12.25.gif

可以发现通过懒加载确实可以保证echarts图表能渲染成功。
不过被动触发懒加载总是不够“聪明”,有没有一种办法让echarts组件自己实现懒加载?
可以试试~

主动懒加载

所谓主动懒加载就是echarts组件自主决定渲染时机,以保证用户访问到自己时能以完整的形态展现出来。
首先,echarts的最佳渲染时机是当前容器已经完全挂载到文档上,并且其宽高均不为零。所以问题的关键就变成了如何监听到容器已经渲染到页面上。通过查阅MDN文档,我找到了这个API: IntersectionObserver

IntersectionObserver 接口(从属于 Intersection Observer API)提供了一种异步观察目标元素与其祖先元素或顶级文档视口(viewport)交叉状态的方法。其祖先元素或视口被称为根(root)。
当一个 IntersectionObserver 对象被创建时,其被配置为监听根中一段给定比例的可见区域。一旦 IntersectionObserver 被创建,则无法更改其配置,所以一个给定的观察者对象只能用来监听可见区域的特定变化值;然而,你可以在同一个观察者对象中配置监听多个目标元素。

查看其兼容性, caniuse

image.png

没有了恶心的IE干扰,其兼容性几乎是100%,完全可用!
借助该API对 LineChart进行改造:

<template>
  <div class="line-chart" ref="container"></div>
</template>

<script lang="ts">
import { ref, watch } from "vue";
import useEchartsFactory from "@/hooks/useEchartsFactory";
import { lineOption } from "./constant";

export default {
  name: "LineChartLazy",
  setup() {
    const container = ref<HTMLDivElement>();
    const echartInstance = useEchartsFactory(container);

    watch(
      () => echartInstance.value,
      (val) => {
        if (val) {
          val.setOption(lineOption);
        }
      }
    );

    return {
      container,
    };
  },
};
</script>

<style lang="scss" scoped>
.line-chart {
  padding: 10px 20px;
  width: 100%;
  min-height: 400px;
}
</style>

其中 useEchartsFactory 是一个自定义hook,其会兼容当前容器是否展现在视窗中,然后决定是否实例化echarts对象,并返回。

import { ref, onMounted } from "vue";
import type { Ref } from "vue";
import { noop } from "lodash";
import echarts from "@/utils/echarts";

export default function useEchartsFactory(
  dom?: Ref<HTMLDivElement | undefined>
): Ref<echarts.ECharts | null> {
  const chartInstance = ref<echarts.ECharts | null>(null);
  const isInit = ref<boolean>(false);

  onMounted(() => {
    const initFlag = isInit;
    const observer = new IntersectionObserver((entries) => {
      // 如果 intersectionRatio 为 0,则目标在视野外,不做任何处理
      if (entries[0].intersectionRatio <= 0) {
        noop();
        // 容器已经展示在视窗里,但是echarts图形仍然没有实例化,需要执行实例化
      } else if (!initFlag.value) {
        initFlag.value = true;
        const instance = echarts.init(dom?.value);
        chartInstance.value = instance;
        // 执行完实例化后就可以解除监听了
        observer.disconnect();
      }
    });
    if (dom?.value) {
      observer.observe(dom?.value);
    }
  });

  return chartInstance;
}

useEchartsFactoryhook的核心逻辑就是监听echarts容器的状态,当其处在视窗范围内时,执行实例化动作,然后返回Ref<echarts.ECharts>。 其效果跟 被动懒加载的效果一致,这里就不重复列出了。

小结

  1. echarts图形的初始化需要确保容器存在真实的宽、高,以防止高度坍塌出现渲染失败或画布展示不完全问题;
  2. 被动懒加载不够“聪明”,主动懒加载适用性更广泛;
  3. 除了用IntersectionObserver监听,还可以用 ResizeObserverMutationObserver 来监听dom的变化,同样可以实现对加载时机的确定