封装完了才发现有个 vue-echarts 的库。。。难受了。还好我没有封装的很复杂,对于事件这边是完全没做处理(暂时没有那么多互动需求)。
动机
先要明白封装 echarts 的目标是什么,不能上来就无脑封装。我封装它的目的就是因为我们的各类型图表设计类似,没有特别多的变化。以折线图为例,可能我们大多数的折线图都使用相同的设计、颜色,所以我不希望像 echarts 或 vue-echarts 那样,需要给每个图表都传一遍 options。
其实这个需求用其他的代码复用能力也能做到,但是我认为封装成组件更灵活,也更直观一些。
实现
清楚了为什么封装之后,我们就需要明确我们的组件想要隐藏什么细节了。既然我们需要隐藏的是整个图表的样式配置项,所以颜色、表现相关的配置项肯定是想要在组件内写好,而相关的数据配置需要使用者传入。
其实仔细思考一下,相关的表现配置项也不好写死在组件里,这样会导致我们的组件适应性太差。所以我选择将其作为默认值,可以让上层使用者随时可以覆盖。
封装基本的 echarts 组件
按着这个思路来的话,我可能会封装很多种类的图表组件。那么这么多组件,我希望能有一个基本的 echarts 组件能支撑其底层,即 echarts 的初始化、挂载等操作。
<!-- components/Echarts/Echarts.vue --> <template> <div ref="echartsRef" class="echarts-container"></div> </template> <script setup> import { ref, onMounted, onBeforeUnmount, watch } from 'vue' // 这里引入的echarts是按需引入,use了相关插件后的echarts import { echarts } from './utils/echarts' const props = defineProps({ option: { type: Object, default: () => ({}), }, // 控制loading状态 loading: { type: Boolean, default: false, }, }) const echartsRef = ref(null) const chart = ref(null) // 实现配置项自动更新,让上层无须反复设置setOption watch( () => props.option, (newOption) => { if (chart.value) { chart.value.setOption(newOption) } } ) watch( () => props.loading, (loading) => { if (loading) { chart.value?.showLoading() } else { chart.value?.hideLoading() } } ) onMounted(() => { // 挂载后,初始化echarts实例 if (!echartsRef.value) return chart.value = echarts.init(echartsRef.value) if (props.loading) { chart.value?.showLoading() } chart.value.setOption(props.option) }) onBeforeUnmount(() => { // 组件卸载,释放实例 if (chart.value) { chart.value.dispose() } }) // 将echarts实例暴露出去,万一满足不了上层所有需求,上层可以自己操作实例 defineExpose({ chart, }) </script> <script> export default { name: 'BaseChart', } </script> <style lang="less"> .echarts-container { width: 100%; height: 100%; } </style>
封装具体图表
这样我们就可以基于这一层,继续封装。以折线图为例:
<!-- components/Echarts/BaseLine.vue --> <template> <Echarts :option="customOption" :loading="loading"></Echarts> </template> <script setup> import { computed } from 'vue' import { merge, cloneDeep } from 'lodash-es' import Echarts from './Echarts.vue' // 将数据相关的配置项抛出去,让使用者确定 const props = defineProps({ option: { type: Object, default: () => ({}), }, legend: { type: Array, default: () => [], }, xAxis: { type: Array, default: () => [], }, yAxis: { type: Array, default: () => [], }, data: { type: Object, default: () => ({}), }, loading: { type: Boolean, default: false, } }) // 生成series配置项,其实这个函数也可以从上层传进来,这样能更自由,但这里我直接写死了 const generateSeries = (data) => { if (!data) return [] if (Array.isArray(data)) { return data.map(({ name, data }) => ({ name, type: 'line', symbol: 'circle', symbolSize: 6, emphasis: { scale: 1.5, }, itemStyle: { color({ color }) { return color }, borderColor: '#fff', borderWidth: 1, }, data, })) } else { return Object.entries(data).map(([name, data]) => ({ name, type: 'line', symbol: 'circle', symbolSize: 6, emphasis: { scale: 1.5, }, itemStyle: { color({ color }) { return color }, borderColor: '#fff', borderWidth: 1, }, data, })) } } // 默认的折线图配置项 const defaultOption = { tooltip: { trigger: 'axis', }, legend: { data: [], left: 'left', top: '6px', }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true, }, xAxis: { type: 'category', boundaryGap: false, }, yAxis: { type: 'value', }, } const customOption = computed(() => { // 合并默认配置、自定义的option、传入的数据。得到最终的option return merge(merge(cloneDeep(defaultOption), props.option), { legend: { data: props.legend, }, xAxis: { data: props.xAxis, }, yAxis: { data: props.yAxis, }, series: generateSeries(props.data), }) }) </script> <script> export default { name: 'BaseLineChart', } </script>
这里实现配置项的覆盖,其实是使用了 lodash 的 merge 来进行合并,而且进行了两次合并,这样能确保各配置项的优先级,让他们可以相互覆盖。
使用
这样封装完,我们在使用时就可以直接传入数据了:
<!-- 某个业务组件 --> <template> <div style="width: 100%; height: 100%"> <BaseLineChart :legend="legend" :xAxis="xAxis" :data="data" :loading="store.isLoadingChartData" ></BaseLineChart> </div> </template> <script setup> import { ref, computed } from 'vue' import BaseLineChart from '../../components/Echarts/baseLine.vue' import { useVisualizationStore } from '../visualizationStore' const store = useVisualizationStore() const legend = ref(['风险数', '得分']) const xAxis = computed(() => { return store.dataSafeList?.map((item) => item.month) }) const data = computed(() => { return { '风险数': store.dataSafeList?.map((item) => item.num) || [], '得分': store.dataSafeList?.map((item) => item.score) || [], } }) </script>
可以看到 我们就已经实现了在业务层内,非必要不使用样式相关的配置项。如果真的想要覆盖某个样式,也可以直接传入一个 options 来覆盖默认的样式。
个人认为这应该算是一个比较有用的封装,即使使用 vue-echarts,我看也是需要传入具体的配置项。也就意味着,如果想要实现具体业务不参与样式,依旧需要再封装一层图表组件。
唯一的遗憾可能是没有基于 vue-echarts 进行封装吧,如果基于他进行封装,可能有很多功能不需要自己实现,也更完善一些。
哦,可能不少场景下,需要侦听 window 的 resize,来让图表自动重渲染(auto resize)。我这一期没有封装这个功能,但是我觉得也好加,在基础组件内添加即可。