在前端开发中,数据可视化是一个至关重要的环节。Echarts 作为一款强大的图表库,为我们提供了丰富多样的图表类型和灵活的配置选项。在本文中,我将分享如何在 Vue 项目中构建一个可复用的 Echarts 组件,并在父组件中进行灵活使用。
子组件
<template>
<div ref="elRef" :style="styles"></div>
</template>
<script setup>
import echarts from "./plugins/index.js";
import { useEchartsResizeAndDestory } from "@/hooks/web/EchartsResizeAndDestoryHooks.js";
defineOptions({ name: "EChart" });
const props = defineProps({
options: {
type: Object,
required: true,
},
width: {
type: String,
default: "100%",
},
height: {
type: String,
default: "360px",
},
});
console.log("props", props);
const elRef = ref();
const chartInstance = shallowRef();
const is = (val, type) => {
return toString.call(val) === `[object ${type}]`;
};
const isString = (val) => {
return is(val, "String");
};
const options = computed(() => {
return props.options;
});
const styles = computed(() => {
const width = isString(props.width) ? props.width : `${props.width}px`;
const height = isString(props.height) ? props.height : `${props.height}px`;
return {
width,
height,
};
});
useEchartsResizeAndDestory(chartInstance, elRef);
const initChart = () => {
const echartsContainer = elRef.value;
if (echartsContainer && props.options) {
console.log("开始渲染数据");
chartInstance.value = echarts.init(echartsContainer);
chartInstance.value.setOption(options.value);
}
};
watch(
() => options.value,
(options) => {
console.log("options", options);
if (chartInstance.value) {
chartInstance.value.setOption(options);
}
},
{ deep: true }
);
onMounted(() => {
initChart();
});
</script>
<style lang="scss" scoped></style>
import * as echarts from "echarts/core";
// 引入图表类型
import {
BarChart,
FunnelChart,
GaugeChart,
LineChart,
MapChart,
PictorialBarChart,
PieChart,
RadarChart,
} from "echarts/charts";
// 引入内置组件
import {
AriaComponent,
GridComponent,
LegendComponent,
ParallelComponent,
PolarComponent,
TitleComponent,
ToolboxComponent,
TooltipComponent,
VisualMapComponent,
} from "echarts/components";
// 引入渲染器
import { CanvasRenderer } from "echarts/renderers";
echarts.use([
LegendComponent,
TitleComponent,
TooltipComponent,
ToolboxComponent,
GridComponent,
PolarComponent,
AriaComponent,
ParallelComponent,
VisualMapComponent,
BarChart,
LineChart,
PieChart,
MapChart,
CanvasRenderer,
PictorialBarChart,
RadarChart,
GaugeChart,
FunnelChart,
]);
export default echarts;
/**
* @name: EchartsResizeAndDestoryHooks
* @author: win10
* @date: 2024/3/14 17:48
* @description:EchartsResizeAndDestoryHooks
* @update: 2024/3/14 17:48
*/
import { onBeforeUnmount, onMounted } from "vue";
/**
*
* @param {Ref} chartInstance - ECharts实例的Vue响应式引用
* @param {Ref} echartsContainerRef - ECharts容器的Vue响应式引用
* @returns {Object} - 包含销毁ECharts实例的方法
*/
export function useEchartsResizeAndDestory(chartInstance, echartsContainerRef) {
/**
* 挂载时监听容器大小变化
*/
onMounted(() => {
listenChartsResize();
});
/**
* 卸载时销毁ECharts实例并停止监听容器变化
*/
onBeforeUnmount(() => {
destoryEcharts();
destoryListenChartsResize();
});
/**
* 创建实例用于监听容器大小变化
*/
const observer = new ResizeObserver(() => {
chartContainerResize();
});
/**
* 调整图表容器
* @returns
*/
function chartContainerResize() {
const echartsContainer = echartsContainerRef.value;
if (!echartsContainer) return;
if (chartInstance.value) {
chartInstance.value.resize();
}
}
/**
* 开始监听图表容器大小变化
* @returns
*/
function listenChartsResize() {
const echartsContainer = echartsContainerRef.value;
if (!echartsContainer) return;
observer.observe(echartsContainer);
}
/**
* 停止监听图表容器大小变化
*/
function destoryListenChartsResize() {
observer.disconnect();
}
/**
* 销毁ECharts实例
*/
function destoryEcharts() {
if (chartInstance.value) {
chartInstance.value.dispose();
chartInstance.value = undefined;
}
}
return { destoryEcharts };
}
// 在Vue组件中的示例用法
// const { destoryEcharts } = useEchartsResizeAndDestory(registerThemeMethod, chartInstance, echartsContainerRef);
在子组件中,我们首先导入了 Echarts 相关的依赖,并定义了组件的属性,包括图表的配置选项 options 、宽度 width 和高度 height 。通过计算属性 styles 来处理宽度和高度的样式。使用 useEchartsResizeAndDestory 钩子函数来处理图表的大小调整和销毁逻辑。在 initChart 方法中初始化图表,并通过 watch 监听 options 的变化来更新图表。同时使用钩子函数处理组件的挂载和卸载时的逻辑,包括监听容器大小变化、调整图表大小、销毁图表实例以及停止监听容器变化。
父组件
<template>
<div class="echarts-container">
<el-card shadow="never" class="mb-2">
<template #header>
<h3 class="font-bold">基础</h3>
</template>
<Echarts v-if="isShow" :options="options" />
</el-card>
</div>
</template>
<script setup>
import Echarts from "@/components/Echarts/index.vue";
const days = ref(null);
const networkDelayData = ref(null);
const packetLossRateData = ref(null);
const isShow = ref(false);
const generateRandomData = () => {
days.value = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
networkDelayData.value = days.value.map(() =>
Math.floor(Math.random() * 300)
);
packetLossRateData.value = days.value.map(() =>
Math.floor(Math.random() * 300)
);
isShow.value = true;
};
const options = computed(() => ({
backgroundColor: "#fff",
title: {
textStyle: {
fontSize: 12,
fontWeight: 400,
},
left: "center",
top: "5%",
},
legend: {
icon: "circle",
top: "5%",
right: "5%",
itemWidth: 6,
itemGap: 20,
textStyle: {
color: "#556677",
},
},
tooltip: {
trigger: "axis",
axisPointer: {
label: {
show: true,
backgroundColor: "#fff",
color: "#556677",
borderColor: "rgba(0,0,0,0)",
shadowColor: "rgba(0,0,0,0)",
shadowOffsetY: 0,
},
lineStyle: {
width: 0,
},
},
backgroundColor: "#fff",
textStyle: {
color: "#5c6c7c",
},
padding: [10, 10],
extraCssText: "box-shadow: 1px 0 2px 0 rgba(163,163,163,0.5)",
formatter: function (params) {
let tooltipText = "";
params.forEach((param) => {
if (param.seriesName === "网络延时") {
tooltipText += `${param.seriesName}: ${param.value}ms<br>`;
} else {
tooltipText += `${param.seriesName}: ${param.value}%<br>`;
}
});
return tooltipText;
},
},
grid: {
// top: "15%",
bottom: 20,
},
xAxis: [
{
type: "category",
data: days.value,
axisLine: {
show: false,
},
axisLabel: {
show: true, // 也应该隐藏y轴标签
},
splitLine: {
show: false,
},
axisTick: {
show: false,
},
},
],
yAxis: [
{
type: "value",
show: true, // 添加这一行来隐藏这个y轴
axisLine: {
show: false,
},
axisLabel: {
show: true, // 也应该隐藏y轴标签
formatter: function (value) {
return value + "%";
},
},
splitLine: {
show: false,
},
axisTick: {
show: false,
},
},
{
type: "value",
position: "right",
show: true, // 同样添加这一行来隐藏右侧的y轴
axisLine: {
show: false,
},
axisLabel: {
show: true, // 也应该隐藏y轴标签
formatter: function (value) {
return value + "%";
},
},
splitLine: {
show: false,
},
axisTick: {
show: false,
},
splitLine: {
show: false,
},
},
],
series: [
{
name: "网络延时",
type: "line",
data: networkDelayData.value,
barWidth: "40%",
symbol: "circle",
yAxisIndex: 0,
showSymbol: false,
lineStyle: {
width: 1,
},
},
{
name: "丢包率",
type: "line",
data: packetLossRateData.value,
symbolSize: 1,
yAxisIndex: 1,
symbol: "circle",
showSymbol: false,
lineStyle: {
width: 1,
},
},
],
}));
onMounted(() => {
setTimeout(() => {
console.log("模拟网络延迟请求");
generateRandomData();
}, 3000);
});
</script>
<style lang="scss" scoped>
.echarts-container {
position: relative;
padding: 20px;
}
</style>
在父组件中,我们定义了一些数据,并通过计算属性 options 生成了图表的配置。通过条件渲染 v-if 来控制子组件的显示,并将配置传递给子组件。
为什么使用计算属性呢?
通过使用延迟函数模拟网络请求慢的场景。当子组件开始渲染的时候,父组件的数据并没有获获取到。会造成echarts渲染空白。使用计算属性能够根据依赖的数据进行自动更新。当依赖的数据如当依赖的数据(如 days、networkDelayData 、packetLossRateData 等)发生变化时,计算属性 options 会自动重新计算并更新图表的配置,确保图表能够及时反映数据的最新状态。同时计算属性具有缓存机制,如果依赖的数据没有发生变化,不会重复进行计算,提高了性能。