欢迎点击领取-《前端面试题进阶指南》:前端登顶之巅-最全面的前端知识点梳理总结
*分享一个使用比较久的
1、echarts是我们后台系统中最常用的数据统计图形展示,外界对它的二次封装也不计层数;
2、在业务代码内每次的初始dom和绑定setOption导致代码量的堆积不利于维护
3、拓展公共echarts组件,通过不同入参开启对应的功能更利于维护和排查问题
4、echarts的版本5.4.x
该文件用作于处理和初始化echarts的公用逻辑,抽离出来使用vue3的hooks用来注水操作
该文件用作于echarts的dom承载和相关api的入参只有少量逻辑;
useCharts.ts文件
import * as echarts from 'echarts/core'
import {
TitleComponent,
LegendComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
ToolboxComponent,
MarkAreaComponent,
MarkLineComponent,
MarkPointComponent
} from 'echarts/components'
import { BarChart, LineChart, PieChart, GaugeChart } from 'echarts/charts'
import { LabelLayout, UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
import { ShallowRef, shallowRef, Ref, onBeforeUnmount, watch, useAttrs, shallowReactive } from 'vue'
interface ChartHookOption {
theme?: Ref<string>
el: ShallowRef<HTMLElement>
options: any
}
/**
* 视口变化时echart图表自适应调整
*/
class ChartsResize {
#charts = new Set<echarts.ECharts>() // 缓存已经创建的图表实例
#timeId = null
constructor() {
window.addEventListener('resize', this.handleResize.bind(this)) // 视口变化时调整图表
}
getCharts() {
return [...this.#charts]
}
handleResize() {
clearTimeout(this.#timeId)
this.#timeId = setTimeout(() => {
this.#charts.forEach((chart) => {
chart.resize()
})
}, 350)
}
add(chart: echarts.ECharts) {
this.#charts.add(chart)
}
remove(chart: echarts.ECharts) {
this.#charts.delete(chart)
}
removeListener() {
window.removeEventListener('resize', this.handleResize)
}
}
export const chartsResize = new ChartsResize()
export const useCharts = ({ el, theme, options }: ChartHookOption) => {
echarts.use([
BarChart,
LineChart,
BarChart,
PieChart,
GaugeChart,
TitleComponent,
LegendComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
LabelLayout,
UniversalTransition,
CanvasRenderer,
ToolboxComponent,
MarkAreaComponent,
MarkLineComponent,
MarkPointComponent
])
const charts = shallowRef<echarts.ECharts>()
const setOptions = (opt: echarts.EChartsCoreOption) => {
charts.value.setOption(opt)
}
const initChart = () => {
charts.value = echarts.init(el.value, theme)
charts.value.setOption(options)
chartsResize.add(charts.value) // 将图表实例添加到缓存中
initEvent() // 添加事件支持
}
// 初始化事件
const attrs = useAttrs()
const initEvent = () => {
Object.keys(attrs).forEach((attrKey) => {
if (/^on/.test(attrKey)) {
const cb = attrs[attrKey]
attrKey = attrKey.replace(/^on(Chart)?/, '')
attrKey = `${attrKey[0]}${attrKey.substring(1)}`
typeof cb === 'function' && charts.value?.on(attrKey, cb as () => void)
}
})
}
onBeforeUnmount(() => {
chartsResize.remove(charts.value) // 移除缓存
})
return {
charts,
setOptions,
initChart,
initEvent
}
}
export const chartsOptions = <T extends echarts.EChartsCoreOption>(option: T) => shallowReactive<T>(option)
v-charts.vue代码模块
<template>
<div class="v-charts" ref="chartRef" />
</template>
<script lang="ts" setup>
import * as echarts from 'echarts/core'
import { useCharts, chartsResize } from './useCharts'
import { PropType, toRefs, shallowRef, onMounted, watch, nextTick } from 'vue'
const props = defineProps({
theme: String,
delay: [String, Boolean],
isWatch: [String, Boolean, Object],
options: {
type: Object as PropType<echarts.EChartsCoreOption>,
default: () => ({})
},
})
const { theme, options } = toRefs(props)
const chartRef = shallowRef()
const { charts, setOptions, initChart } = useCharts({ theme, el: chartRef, options })
// 开启默认放大缩放功能
const turnOndataZoom = () => {
charts.value.dispatchAction({
type: 'takeGlobalCursor',
key: 'dataZoomSelect',
dataZoomSelectActive: true
})
}
onMounted(async () => {
await initChart()
setOptions(options.value)
})
watch(
options,
() => {
setOptions(options.value)
nextTick(() => turnOndataZoom())
},
{
deep: true
}
)
watch(
() => props.isWatch, // 是否开启外部左侧菜单引起的布局适配问题
() => {
chartsResize.handleResize()
},
{
deep: true,
immediate: true
}
)
defineExpose({
chartRef: chartRef,
$charts: charts
})
</script>
<script lang="ts">
export default { name: "v-charts" };
</script>
<style lang="scss" scoped>
.v-charts {
width: 100%;
height: 100%;
clear: both;
min-height: 360px;
}
</style>
3.1 创建index.js文件
import vCharts from './v-charts/v-charts.vue'
export * from './v-charts/useCharts';
const components = [vCharts]; // 可添加需要全局注入的公共组件
const install = function (Vue: any) {
components.forEach((app) => {
Vue.component(app.name, app);
});
};
export default install;
3.2 在main.ts文件内进行引用
import CustomUi from '@/components/index'
app.use(CustomUi)
<template>
<v-charts ref="myCharts" :isWatch="isActiveName" :options="setOptions" />
</template>
<script lang="ts" setup>
import { ref, reactive, shallowRef, onBeforeMount } from 'vue'
const option = {
toolbox: {
feature: {
dataZoom: {
icon: null
}
}
},
xAxis: {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
},
yAxis: {},
series: [
{
type: "line",
data: [23, 24, 18, 25, 27, 28, 25],
},
],
};
const myCharts = ref(null)
const isActiveName = ref<boolean>(false)
const setOptions = shallowRef<Record<string, any>>({})
onBeforeMount(() => {
setOptions.value = option
})
</script>
- 第一版的时候setOption这块的内容是在组件内部;因为我们的业务偏复杂,操作setOption内容较多又涉及到轮询处理数据结构;所以将该内容放置外了;这块可以依据自身需求而定
- 封装echarts没有过于封装,主要是针对按需情况和初始化绑定,自适应屏幕而定的封装
- 不通的需求处理方式不通,有不合理之处还请各位谅解