最近接了一个老项目维护工作,技术栈是Vue3+ts+Element-Plus。其中有一个场景问题觉得蛮有意思,给大家分享出来。
问题起因
有一个仪表盘页面,其中一个模块是展示一个tab,每个tab里有若干echarts图表,在开发时因为Element-Plus的机制,默认为渲染未展示的tab-pane(通过设置display:none实现tab-pane隐藏)
未展示的tab-pane会设置样式为display:none
这就为echarts的初始化带来的一定的麻烦,因为echarts图形初始化时要求容器必须有物理宽、高,如果没有则会造成画布初始化尺寸异常。下图是问题表现示意:
DevTools的控制台同步输出了echarts的警告信息:
解决方案
既然是因为高度坍塌造成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-pane的lazy属性实现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属性,让其延迟加载。其效果如下:
可以发现通过懒加载确实可以保证echarts图表能渲染成功。
不过被动触发懒加载总是不够“聪明”,有没有一种办法让echarts组件自己实现懒加载?
可以试试~
主动懒加载
所谓主动懒加载就是echarts组件自主决定渲染时机,以保证用户访问到自己时能以完整的形态展现出来。
首先,echarts的最佳渲染时机是当前容器已经完全挂载到文档上,并且其宽高均不为零。所以问题的关键就变成了如何监听到容器已经渲染到页面上。通过查阅MDN文档,我找到了这个API:
IntersectionObserver
IntersectionObserver接口(从属于 Intersection Observer API)提供了一种异步观察目标元素与其祖先元素或顶级文档视口(viewport)交叉状态的方法。其祖先元素或视口被称为根(root)。
当一个IntersectionObserver对象被创建时,其被配置为监听根中一段给定比例的可见区域。一旦IntersectionObserver被创建,则无法更改其配置,所以一个给定的观察者对象只能用来监听可见区域的特定变化值;然而,你可以在同一个观察者对象中配置监听多个目标元素。
查看其兼容性, caniuse:
没有了恶心的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>。
其效果跟 被动懒加载的效果一致,这里就不重复列出了。
小结
- echarts图形的初始化需要确保容器存在真实的宽、高,以防止高度坍塌出现渲染失败或画布展示不完全问题;
- 被动懒加载不够“聪明”,主动懒加载适用性更广泛;
- 除了用
IntersectionObserver监听,还可以用ResizeObserver或MutationObserver来监听dom的变化,同样可以实现对加载时机的确定