【问题场景】 vue项目,封装了echarts相关的图表组件,在父组件中引入这些组件,图表宽度由父组件中的包裹元素决定,而这些包裹元素的宽度不固定,是按比例计算来的,页面刷新时,偶现部分图表渲染不出来,F12定位发现 这些图表的内容宽度是 0。
【原先的代码】
1、父组件
<template>
<div class="chart-wrapper">
<div class="chart-card margin-right-16">
<PieChart v-bind="pieChart" />
</div>
<div class="chart-card margin-right-16">
<LineChart v-bind="lineChart" />
</div>
<div class="chart-card">
<BarChart v-bind="barChart" />
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import PieChart from '@/components/Charts/PieChart'
import LineChart from '@/components/Charts/LineChart'
import BarChart from '@/components/Charts/BarChart'
const pieChart = ref(
{
id: 'pieChart',
chartStyle: {
height: '180px'
}
}
)
const fetchPieData = () => {
...
pieChart.value = {
id: 'pieChart',
chartStyle: {
height: '180px'
},
title: ...
legend: ...
series: ...
}
}
const lineChart =
const fetchLineData =
const barChart =
const fetchBarData =
onMounted(() => {
fetchPieData()
fetchLineData()
fetchBarData()
})
</script>
<style lang="scss" scoped>
.chart-wrapper {
width: 100%;
.chart-card {
display: inline-block;
width: calc((100% - 32px) / 3);
height: 240px;
}
.margin-right-16 {
margin-right: 16px;
}
}
</style>
2、echarts图表组件 BarChart.vue
<template>
<div :id="id" :style="chartStyle" class="chart-contianer"></div>
</template>
<script setup>
import { onMounted, nextTick, watch } from 'vue'
import * as echarts from 'echarts'
const props = defineProps({
id: {
type: String,
default: ''
},
chartStyle: {
type: Object,
default: () => ({})
},
xAxis: ...
yAxis: ...
series: ...
})
const drawChart = async () => {
await nextTick()
const container = document.getElementById(props.id)
const chartsDom = echarts.init(container)
container.removeAttribute('_echarts_instance_')
const option = {
grid: ...
tooltip: ...
legend: ...
color: ...
xAxis: ...
yAxis: ...
series: ...
}
chartsDom.setOption(option)
}
watch(() => props.seriesData, (newVal) => {
if (newVal) drawChart()
}, { deep: true })
onMounted(() => {
drawChart()
})
</script>
<style lang="scss" scoped>
.chart-contianer {
width: 100%;
height: 100%;
}
</style>
【错误的思考方向】 echarts组件的数据获取是在父组件的 onMounted 中进行的,父组件 DOM 节点挂载完成的时候子组件应该也挂载完成了,而且子组件中还有 watch 监听图表数据的变化可以重新渲染,不应该会出现获取不到容器元素的问题。
【原因分析】 其实echarts能渲染,表示容器元素是存在的,所以不是获取不到容器元素的问题,而是容器元素的宽度还未撑开。真正的原因是:即使 DOM 挂载完成,浏览器的 layout/reflow 还未完成,导致子元素 size 还没确定。即 DOM 挂载完成不代表渲染完成。所以什么把数据获取放在 onMounted中进行、获取数据之后在 nextTick 之后渲染图表,都没有用。
【解决方法】 监听容器元素的尺寸变化,即时重新渲染图表,这里可以用 ResizeObserver 。具体API参考 ResizeObserver
【修改子组件代码】
<template>
<div :id="id" :style="chartStyle" class="chart-contianer"></div>
</template>
<script setup>
import { onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
import * as echarts from 'echarts'
...
// ——————监听容器元素 start——————
// 创建一个 ResizeObserver实例
const observer = new ResizeObserver(entries => {
entries.forEach(entry => {
if (entry?.target?.id === props.id) drawChart()
})
})
onMounted(() => {
// 监听指定元素
observer.observe(document.getElementById(props.id))
drawChart()
})
onBeforeUnmount(() => {
observer.unobserve(document.getElementById(props.id))
})
// ——————监听容器元素 end——————
</script>
<style lang="scss" scoped>
.chart-contianer {
width: 100%;
height: 100%;
}
</style>