目錄
一、相關知識
1.1 什麼是ECharts
1.2 什麼是元件
二、為什麼要封裝ECharts元件
三、如何在Vue3專案中封裝ECharts元件
3.1 ECharts的全量引入和按需引入
3.2 支援資料響應式更新
3.3 簡化ECharts的配置工作
3.4 支援自適應視窗大小
四、進一步提升元件的實用性和便捷性
4.1 巧用dataset
4.2 結合axios請求進一步封裝
五、總結與後期改進
一、相關知識
1.1 什麼是ECharts
ECharts是一個基於JavaScript的開源視覺化庫,以資料驅動、直觀、互動豐富、可高度個性化定製為特點。它提供了豐富多樣的圖表型別和互動功能,可以幫助開發人員快速建立各種資料視覺化圖表,如折線圖、柱狀圖、餅圖、地圖等。同時,ECharts還提供了多種資料互動和動畫效果,使得資料視覺化更加生動和有趣。
ECharts具有良好的相容性和擴充套件性,相容當前絕大部分瀏覽器(IE8/9/10/11,Chrome,Firefox,Safari等),支援移動端和PC端展示,同時提供了豐富的配置項和API,使使用者能夠靈活地定製和調整圖表樣式和行為。
由於其功能強大、易於上手和社羣支援良好,ECharts已成為前端開發中常用的資料視覺化工具之一。
1.2 什麼是元件
元件是前端開發中一種模組化的設計方式,用於將特定功能、結構和樣式封裝成獨立的單元。透過元件化的設計,開發人員可以將複雜的介面拆分為多個獨立、可複用的部分,使程式碼更加清晰、可維護性更強;透過組合不同的元件,可以構建出豐富多樣的用戶界面。
在現代前端開發中,元件化已成為一種重要的開發模式,透過前端元件化能夠提高團隊協作效率,加快專案開發速度,便於後期進行功能的擴充套件和修改。
二、為什麼要封裝ECharts元件
資料視覺化圖表是前端開發中非常常見的功能需求,尤其在大屏和資料管理系統的開發中佔有很高的比例,因此ECharts成了我們前端工程師經常使用的一個工具庫。
在實際開發中,當專案中需要使用ECharts進行視覺化圖表的開發時,通常我們會直接參照官網提供的樣例配置來生成所需的圖表,類似這樣:
// 繪製圖表 myChart.setOption({ title: { text: 'ECharts 入門示例' }, tooltip: {}, xAxis: { data: ['襯衫', '羊毛衫', '雪紡衫', '褲子', '高跟鞋', '襪子'] }, yAxis: {}, series: [ { name: '銷量', type: 'bar', data: [5, 20, 36, 10, 10, 20] } ] });
以上只是一個柱狀圖的配置內容,而每個圖表都得有一份自己的配置,這就導致了一個問題:隨著專案中需要繪製的圖表數量增加,配置變得越來越複雜,當頁面中需要顯示十幾個ECharts圖時,我們需要寫十幾份獨立的配置。
這些配置不僅冗長還複雜,嚴重降低了程式碼的可讀性和可維護性,在接入後端介面時也會帶來一些麻煩;而且在同一個專案中我們用到的大部分ECharts配置都是相似的,每個圖表的配置都有很多重複的內容,當需要統一修改某個配置項的內容時重複的工作量很大。在這種情況下,封裝通用的ECharts元件成了一個更好的選擇。
封裝 Echarts 元件有以下幾個好處:
簡化使用:封裝後的元件可以提供更簡潔、更易用的 API,我們能夠更快速地完成頁面中 ECharts圖表的繪製。
隱藏實現細節:封裝可以隱藏 ECharts的具體實現細節,讓我們不必過多關心圖表配置項和底層實現,只需關注如何處理資料來進行展示。
提高複用性:封裝後的元件可以被多個頁面或專案共享使用,提高了程式碼的複用性和可維護性。
增強擴充套件性:封裝可以在原有基礎上進行功能擴充套件,比如新增自定義互動、動畫效果等,從而滿足更多定製化的需求。
提高可維護性:封裝可以將相關的程式碼邏輯集中在一個元件中,便於維護和管理,減少程式碼冗餘和維護成本。
透過封裝 ECharts元件,我們可以提高開發效率,降低程式碼維護成本,並使專案變得更加模組化和可擴充套件化。
三、如何在Vue3專案中封裝ECharts元件
3.1 ECharts的全量引入和按需引入
我們以ECharts 5.3.3版本為例,要在Vue3專案中使用ECharts,得安裝echarts
依賴包,如果要使用3D圖表功能,還得額外安裝echarts-gl
依賴:
npm install echarts echarts-gl --save
在使用ECharts時,我們可以選擇全量引入或按需引入ECharts資源。
全量引入會匯入ECharts中的所有圖表和元件,使用起來比較便捷:
<template> <div ref="chartDom"></div> </template> <script setup> import * as echarts from 'echarts' import { onMounted, ref, type Ref } from 'vue' const chartDom: Ref<HTMLDivElement | null> = ref(null) onMounted(() => { const chart = echarts.init(chartDom.value); chart.setOption({ // 圖表配置項 }); }) </script>
然而在我們通常開發的中小型系統中,對於視覺化圖表的複雜性需求通常較低,一般只需要使用ECharts中的柱狀圖、折線圖和餅圖等基本圖表型別,因此全量引入可能會顯得有些資源浪費。在適當的情況下,可以考慮使用按需引入的方式,僅匯入我們用到的圖表元件:
<template> <div ref="chartDom"></div> </template> <script setup> import * as echarts from 'echarts/core' import { BarChart } from 'echarts/charts' import { // 標題元件 TitleComponent, // 圖例元件 LegendComponent, // 提示框元件 TooltipComponent, // 座標系網格元件 GridComponent } from 'echarts/components' import { onMounted, ref, type Ref } from 'vue' echarts.use([ TitleComponent, LegendComponent, TooltipComponent, GridComponent, BarChart ]) const chartDom: Ref<HTMLDivElement | null> = ref(null) onMounted(() => { const chart = echarts.init(chartDom.value); chart.setOption({ // 圖表配置項 }); }) </script>
但是這樣的按需引入在使用時也並不方便,每次使用都要引入很多元件,複用性比較差。因此我們可以將ECharts的按需引入封裝成一個精簡版的ECharts放在專案的utils
資料夾下:
/* @/utils/echarts.ts */ import * as Echarts from 'echarts/core' import { BarChart, PieChart, LineChart } from 'echarts/charts' import { // 標題元件 TitleComponent, // 圖例元件 LegendComponent, // 提示框元件 TooltipComponent, // 座標系網格元件 GridComponent, // 資料集元件 DatasetComponent, // 內建資料轉換器元件 (filter, sort) TransformComponent, // 工具欄元件 ToolboxComponent, // 區域縮放元件 DataZoomComponent, // 原生圖形元素元件 } from 'echarts/components' import { LabelLayout, UniversalTransition } from 'echarts/features' import { CanvasRenderer } from 'echarts/renderers' import type { // 系列型別的定義字尾都為 SeriesOption BarSeriesOption, PieSeriesOption, LineSeriesOption } from 'echarts/charts' import type { // 元件型別的定義字尾都為 ComponentOption TitleComponentOption, TooltipComponentOption, GridComponentOption, DatasetComponentOption, ToolboxComponentOption, DataZoomComponentOption, GraphicComponentOption } from 'echarts/components' import type { ComposeOption } from 'echarts/core' // 註冊必須的元件 Echarts.use([ TitleComponent, LegendComponent, TooltipComponent, GridComponent, DatasetComponent, TransformComponent, ToolboxComponent, DataZoomComponent, GraphicComponent, LabelLayout, UniversalTransition, CanvasRenderer, BarChart, PieChart, LineChart ]) // 透過 ComposeOption 來組合出一個只有必須元件和圖表的 Option 型別 export type ECOption = ComposeOption< | BarSeriesOption | PieSeriesOption | LineSeriesOption | TitleComponentOption | TooltipComponentOption | GridComponentOption | DatasetComponentOption | ToolboxComponentOption | DataZoomComponentOption | GraphicComponentOption > export const echarts = Echarts
這樣在vue檔案中需要使用ECharts時可以直接引入封裝好的echarts.ts
,當增加了新的圖表型別(如雷達圖、熱力圖、桑基圖等)時直接修改echarts.ts
檔案就可以,提高使用按需引入的便捷性:
<template> <div ref="chartDom"></div> </template> <script setup> import { echarts, type ECOption } from '@/utils/echarts' import { onMounted, ref, type Ref } from 'vue' const chartDom: Ref<HTMLDivElement | null> = ref(null) onMounted(() => { const chart = echarts.init(chartDom.value); const options: ECOption = { // 圖表配置項 }; chart.setOption(options); }) </script>
3.2 支援資料響應式更新
搞定了ECharts的資源引入後,我們就可以正式開始封裝ECharts元件了。
我們以封裝一個柱狀圖BarChart元件為例,首先,我們要讓這個元件做到最基本的功能——以資料為驅動,支援響應式更新,即這個元件需要做到能夠接收父元件傳遞的資料繪製成柱狀圖,當父元件資料變化時也要能重新渲染重新整理圖表。因此我們可以使用defineProps
定義一個data屬性(y軸資料)和xAxisData屬性(x軸資料),並結合監聽器watch
對這些屬性進行監聽,當監聽到變化時用新的資料重新繪製柱狀圖:
/* @/components/BarChart.vue */ <template> <div ref="chartDom" style="height: 300px;"></div> </template> <script setup> import { echarts, type ECOption } from '@/utils/echarts' import { ref, shallowRef, watch, onMounted, type ShallowRef, type Ref } from 'vue' import type { EChartsType } from 'echarts/types/dist/core' //定義元件屬性 const props = withDefaults( defineProps<{ //資料 data: Array<string | number> //x軸資料 xAxisData: Array<string> }>(), { data: () => [], xAxisData: () => [] } ) //要渲染的Dom元素 const chartDom: Ref<HTMLDivElement | null> = ref(null) //渲染的chart物件要用shallowRef const chart: ShallowRef<EChartsType | null | undefined> = shallowRef(null) //監聽資料變化,重新繪製 watch( () => props, () => { drawChart() }, { deep: true } ) //繪製 function drawChart() { // 圖表配置項 const options: ECOption = { title: { text: 'ECharts柱狀圖' }, tooltip: {}, xAxis: { data: props.xAxisData }, yAxis: {}, series: [{ name: '數量', type: 'bar', data: props.data }] }; //開啟notMerge保證資料不會疊加 chart.value?.setOption(options, { notMerge: true }); } onMounted(() => { chart.value = echarts.init(chartDom.value); drawChart() }) </script>
📌注意:
echarts.init
初始化得到的chart物件要定義成響應式資料時,得使用shallowRef來代替ref,不然會出現像tooltips不顯示這樣的問題
此時我們就得到了一個最簡單的柱狀圖元件BarChart v1,可以完成最基本的資料展示:
<template> <el-card> <BarChart :data="data" :xAxisData="xData" /> </el-card> </template> <script setup> import BarChart from '@/components/charts/BarChart.vue'; import { ref } from 'vue' const data = ref<Array<number>>([]) const xData = ref<Array<string>>([]) data.value = [5, 20, 36, 10, 10, 20] xData.value = ['襯衫', '羊毛衫', '雪紡衫', '褲子', '高跟鞋', '襪子'] </script>
3.3 簡化ECharts的配置工作
目前我們的BarChart v1元件只能支援單類柱狀圖的顯示,當需要顯示多類柱狀圖時它就無能為力了,因此我們需要擴充套件這個元件的屬性,使它能夠接收ECharts的配置資料:
import type { XAXisOption, YAXisOption, LegendComponentOption, BarSeriesOption, DataZoomComponentOption } from 'echarts/types/dist/shared'; const props = withDefaults( defineProps<{ //資料 data: Array<string | number> //x軸資料 xAxisData: Array<string> //圖表標題 title?: string //系列配置 series?: Array<BarSeriesOption> //x軸配置 xAxis?: Array<XAXisOption> //y軸配置 yAxis?: Array<YAXisOption> //圖例配置 legend?: LegendComponentOption //區域縮放配置 dataZoom?: Array<DataZoomComponentOption> }>(), { data: () => [], xAxisData: () => [] } )
要使封裝的ECharts元件更加易用,必須解決使用ECharts時存在的一個痛點——配置項繁多、配置工作繁瑣。因此,我們進一步對BarChart元件進行改進,內建一些預設的ECharts配置,以簡化元件的配置流程,從而提高開發效率:
/* @/components/BarChart.vue */ <template> <div ref="chartDom" :style="{ height: getHeight }"></div> </template> <script setup> import { echarts, type ECOption } from '@/utils/echarts' import { ref, shallowRef, watch, computed, onMounted, type ShallowRef, type Ref } from 'vue' import type { EChartsType } from 'echarts/types/dist/core' import type { XAXisOption, YAXisOption, LegendComponentOption, BarSeriesOption, DataZoomComponentOption } from 'echarts/types/dist/shared' //定義元件屬性 const props = withDefaults( defineProps<{ //資料 data?: Array<string | number> //x軸資料 xAxisData: Array<string> //圖表標題 title?: string //系列配置 series?: Array<BarSeriesOption> //x軸配置 xAxis?: Array<XAXisOption> //y軸配置 yAxis?: Array<YAXisOption> //圖例配置 legend?: LegendComponentOption //區域縮放配置 dataZoom?: Array<DataZoomComponentOption> //圖形高度 height?: number | string }>(), { data: () => [], xAxisData: () => [], title: 'ECharts柱狀圖', } ) //要渲染的Dom元素 const chartDom: Ref<HTMLDivElement | null> = ref(null) //渲染的chart物件要用shallowRef const chart: ShallowRef<EChartsType | null | undefined> = shallowRef(null) //高度同時支援string和number const getHeight = computed(() => { return typeof props.height === 'number' ? props.height + 'px' : props.height }) //監聽資料變化,重新繪製 watch( () => props, () => { drawChart() }, { deep: true } ) //繪製 function drawChart() { let series: Array<BarSeriesOption> = props.series ? props.series : [{ name: '數量', type: 'bar', barMaxWidth: 30, emphasis: { focus: 'self' }, label: { show: true, position: 'inside', color: '#fff' }, data: props.data }] let xAxis: Array<XAXisOption> = props.xAxis ? props.xAxis : [{ type: 'category', axisTick: { show: false }, data: props.xAxisData }] let yAxis: Array<YAXisOption> = props.yAxis ? props.yAxis : [{ type: 'value', minInterval: 1 }] let legend: LegendComponentOption = props.legend ? props.legend : { show: true, type: 'scroll', orient: 'horizontal', top: 25, left: 'center' } let dataZoom: Array<DataZoomComponentOption> = props.dataZoom ? props.dataZoom : [] const options: ECOption = { backgroundColor: '', title: { text: props.title }, tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, // appendToBody:true }, legend: legend, grid: { left: 10, right: 10, bottom: props.dataZoom ? 40 : 10, containLabel: true }, toolbox: { show: true, feature: { magicType: { type: ['line', 'bar'] }, dataView: { readOnly: false }, saveAsImage: {} } }, xAxis: xAxis, yAxis: yAxis, dataZoom: dataZoom, series: series } //開啟notMerge保證配置資料不會疊加 chart.value?.setOption(options, { notMerge: true }); } onMounted(() => { chart.value = echarts.init(chartDom.value); drawChart() }) </script>
現在我們得到了更加靈活的柱狀圖元件BarChart v2,支援傳入ECharts配置資料,並且內建了預設配置,可以方便地實現多型別的柱狀圖顯示:
<template> <el-card> <BarChart title="多型別柱狀圖" :height="500" :xAxisData="xData" :series="barSereis" /> </el-card> </template> <script setup> import BarChart from '@/components/charts/BarChart.vue'; import { ref } from 'vue' import type { BarSeriesOption } from 'echarts/types/dist/shared' const xData = ref<Array<string>>([]) const barSereis = ref<Array<BarSeriesOption>>([]) xData.value = ['襯衫', '羊毛衫', '雪紡衫', '褲子', '高跟鞋', '襪子'] barSereis.value = [ { name: '庫存量', type: 'bar', barMaxWidth: 30, emphasis: { focus: 'series' }, label: { show: true, position: 'top', color: 'inherit' }, data: [5, 20, 36, 10, 10, 20] }, { name: '銷售量', type: 'bar', barMaxWidth: 30, emphasis: { focus: 'series' }, label: { show: true, position: 'top', color: 'inherit' }, data: [17, 43, 5, 28, 10, 36] } ] </script>
3.4 支援自適應視窗大小
此時的BarChart v2看起來似乎已經能滿足使用了,但是當我們調整了瀏覽器視窗大小就會發現,我們的元件渲染出來的柱狀圖仍保持著初始的大小,會因為視窗大小的改變而出現留白或顯示不全的問題。因此,我們還需要給BarChart元件加上resize
事件的監聽,當監聽到視窗大小變化時重新渲染ECharts圖表。
檢視ECharts提供的API會發現,它提供了一個resize
方法來重新渲染圖表,我們可以結合window.addEventListener
,在專案的utils
資料夾下再封裝一個resize.ts
工具來實現ECharts自適應視窗大小的功能:
/* @/utils/resize.ts */ //echarts圖表自適應視窗變化封裝方法 import { ref } from 'vue' import { debounce } from 'lodash' export default function () { //echarts圖的例項 const chartObject = ref() //使用防抖debounce函式,減少resize的次數 const chartResizeHandler = debounce(() => { if (chartObject.value) { chartObject.value.resize() } }, 100) const initResizeEvent = () => { //新增視窗大小變化監聽 window.addEventListener('resize', chartResizeHandler) } const destroyResizeEvent = () => { //移除視窗大小變化監聽 window.removeEventListener('resize', chartResizeHandler) } const addResize = () => { initResizeEvent() } const removeResize = () => { destroyResizeEvent() } return { chartObject, addResize, removeResize } }
有了這個工具類,再讓BarChart元件實現視窗自適應就很方便了:
...... import resize from '@/utils/resize' const { chartObject, addResize, removeResize } = resize() onMounted(() => { chart.value = echarts.init(chartDom.value); drawChart() //新增視窗自適應 chartObject.value = chart.value addResize() }) onBeforeUnmount(() => { removeResize() chart.value?.dispose() })
四、進一步提升元件的實用性和便捷性
透過上述步驟的封裝,我們得到了一個基本的ECharts柱狀圖元件BarChart v3,已經可以滿足常規的顯示需求了,但是這個元件在資料接入後端介面時會存在一個問題:因為業務和開發人員的不同,後端介面返回的資料的屬性名是不固定的,我們每次都要先把後端數據處理成純資料陣列傳給元件才能顯示。
在日常開發中,通常後端介面返回的資料形式是這樣的:
[ { name: '襯衫', saleNum: 17, stockNum: 5 }, { name: '羊毛衫', saleNum: 43, stockNum: 20 }, { name: '雪紡衫', saleNum: 5, stockNum: 36 }, { name: '褲子', saleNum: 28, stockNum: 10 }, { name: '高跟鞋', saleNum: 10, stockNum: 10 }, { name: '襪子', saleNum: 36, stockNum: 20 } ]
對於這樣的資料,我們的BarChart v3在使用時必須遍歷重組成兩個陣列才能實現正常使用:
const xData = ref<Array<string>>([]) const barSereis = ref<Array<BarSeriesOption>>([]) //獲取後端資料 function getData() { //假設此處請求後獲取到如下資料 let resData = [ { name: '襯衫', saleNum: 17, stockNum: 5 }, { name: '羊毛衫', saleNum: 43, stockNum: 20 }, { name: '雪紡衫', saleNum: 5, stockNum: 36 }, { name: '褲子', saleNum: 28, stockNum: 10 }, { name: '高跟鞋', saleNum: 10, stockNum: 10 }, { name: '襪子', saleNum: 36, stockNum: 20 } ] initChartData(resData) } //構建圖表資料 function initChartData(data: any[]) { xData.value = [] let saleData: number[] = [], stockData: number[] = [] if (data) { data.forEach(item => { xData.value.push(item.name) saleData.push(item.saleNum) stockData.push(item.stockNum) }) } barSereis.value = [ { name: '庫存量', type: 'bar', barMaxWidth: 30, emphasis: { focus: 'series' }, label: { show: true, position: 'top', color: 'inherit' }, data: saleData }, { name: '銷售量', type: 'bar', barMaxWidth: 30, emphasis: { focus: 'series' }, label: { show: true, position: 'top', color: 'inherit' }, data: stockData } ] } getData()
而對於不同的圖形和不同的介面,我們都得進行不同的遍歷,這樣一來就增加了使用元件的額外重複工作,說明目前這個元件還是不太通用,這時候就需要考慮使用ECharts的資料集(dataset)
配置了。
4.1 巧用dataset
在ECharts中,資料集(dataset)
是專門用來管理資料的元件。雖然每個系列都可以在 series.data
中設定資料,但是從 ECharts4 支援資料集開始,更推薦使用資料集來管理資料。因為這樣資料可以被多個元件複用,也方便進行 “資料和其他配置” 分離的配置風格。畢竟在執行時,資料是最常改變的,而其他配置大多並不會改變。透過巧妙地使用ECharts的dataset元件,可以更方便地處理資料,實現更靈活的圖表展示。
對於上述示例的介面資料,使用dataset來構建有兩種比較簡便的方式。
一種方式是配置dimensions
讓資料自動按順序對映到座標軸中:
option = { legend: {}, tooltip: {}, dataset: { /* 用 dimensions 指定了維度的順序。直角座標系中,如果 X 軸 type 為 category, 預設把第一個維度對映到 X 軸上,後面維度對映到 Y 軸上。 */ dimensions: ['name', 'stockNum', 'saleNum'], source: [ { name: '襯衫', saleNum: 17, stockNum: 5 }, { name: '羊毛衫', saleNum: 43, stockNum: 20 }, { name: '雪紡衫', saleNum: 5, stockNum: 36 }, { name: '褲子', saleNum: 28, stockNum: 10 }, { name: '高跟鞋', saleNum: 10, stockNum: 10 }, { name: '襪子', saleNum: 36, stockNum: 20 } ] }, xAxis: { type: 'category' }, yAxis: {}, series: [ { name: '庫存量', type: 'bar', barMaxWidth: 30, emphasis: { focus: 'series' }, label: { show: true, position: 'top', color: 'inherit' } }, { name: '銷售量', type: 'bar', barMaxWidth: 30, emphasis: { focus: 'series' }, label: { show: true, position: 'top', color: 'inherit' } } ] };
另一種方式是配置series.encode
,讓每個圖例按配置對映:
option = { legend: {}, tooltip: {}, dataset: { source: [ { name: '襯衫', saleNum: 17, stockNum: 5 }, { name: '羊毛衫', saleNum: 43, stockNum: 20 }, { name: '雪紡衫', saleNum: 5, stockNum: 36 }, { name: '褲子', saleNum: 28, stockNum: 10 }, { name: '高跟鞋', saleNum: 10, stockNum: 10 }, { name: '襪子', saleNum: 36, stockNum: 20 } ] }, xAxis: { type: 'category' }, yAxis: {}, series: [ { name: '庫存量', type: 'bar', barMaxWidth: 30, emphasis: { focus: 'series' }, label: { show: true, position: 'top', color: 'inherit' }, encode: { x:'name', y: 'stockNum' } }, { name: '銷售量', type: 'bar', barMaxWidth: 30, emphasis: { focus: 'series' }, label: { show: true, position: 'top', color: 'inherit' }, encode: { x:'name', y: 'saleNum' } } ] };
📌關於
dataset
的更多使用方式,可以參考ECharts文件:echarts.apache.org/handbook/zh…
考慮到資料的相容性和使用的複雜性,在這裏我們可以使用第二種配置series.encode
的方式來改造BarChart元件,使其支援使用dataset:
//定義元件屬性 const props = withDefaults( defineProps<{ //資料 data?: Array<string | number> //x軸資料 xAxisData: Array<string> //圖表標題 title?: string //系列配置 series?: Array<BarSeriesOption> //x軸配置 xAxis?: Array<XAXisOption> //y軸配置 yAxis?: Array<YAXisOption> //圖例配置 legend?: LegendComponentOption //區域縮放配置 dataZoom?: Array<DataZoomComponentOption> //圖形高度 height?: number | string //資料集 datasetSource?: Array<any> }>(), { data: () => [], xAxisData: () => [], title: 'ECharts柱狀圖', } ) ...... //繪製 function drawChart() { ...... const options: ECOption = { ...... dataset: { source: props.datasetSource }, series: series } //開啟notMerge保證配置資料不會疊加 chart.value?.setOption(options, { notMerge: true }); }
此時我們再接入後端介面時,就不需要再重組資料了:
<template> <el-card> <BarChart title="多型別柱狀圖" :height="500" :xAxisData="xData" :datasetSource="dataset" :series="barSereis" /> </el-card> </template> <script setup> import BarChart from '@/components/charts/BarChart.vue'; import { ref } from 'vue' import type { BarSeriesOption } from 'echarts/types/dist/shared' const xData = ref<Array<string>>([]) const dataset = ref<Array<any>>([]) const barSereis = ref<Array<BarSeriesOption>>([]) //獲取後端資料 function getData() { //假設此處請求後獲取到如下資料 let resData = [ { name: '襯衫', saleNum: 17, stockNum: 5 }, { name: '羊毛衫', saleNum: 43, stockNum: 20 }, { name: '雪紡衫', saleNum: 5, stockNum: 36 }, { name: '褲子', saleNum: 28, stockNum: 10 }, { name: '高跟鞋', saleNum: 10, stockNum: 10 }, { name: '襪子', saleNum: 36, stockNum: 20 } ] initChartData(resData) } //構建圖表資料 function initChartData(data: any[]) { if (data) { dataset.value = data xData.value = [] data.forEach(item => { xData.value.push(item.name) }) } } barSereis.value = [ { name: '庫存量', type: 'bar', barMaxWidth: 30, emphasis: { focus: 'series' }, label: { show: true, position: 'top', color: 'inherit' }, encode: { x: 'name', y: 'stockNum' } }, { name: '銷售量', type: 'bar', barMaxWidth: 30, emphasis: { focus: 'series' }, label: { show: true, position: 'top', color: 'inherit' }, encode: { x: 'name', y: 'saleNum' } } ] getData() </script>
4.2 結合axios請求進一步封裝
在BarChart v4中,雖然我們支援了適配不同屬性名的後端資料,但是元件的配置內容還可以再精簡,比如像用於構建圖表資料的initChartData
函式和柱狀圖系列配置barSereis
,我們在使用時完全不關心它的生成過程,似乎可以完全整合到BarChart元件內部。
同時在一般專案的開發過程中,我們獲取圖表的後端資料時,往往都是一個圖表對應一個介面,那麼在後端接口規範統一且資料可直接使用的情況下,我們也許可以讓BarChart繫結一個aixios方法,直接從該方法中獲取資料集。
根據這個思路,我們可以進一步再改造一下BarChart元件,增加一個options
屬性:
import type { ChartSetting } from '@/types/ChartData' //定義元件屬性 const props = withDefaults( defineProps<{ //資料 data?: Array<string | number> //x軸資料 xAxisData: Array<string> //圖表標題 title?: string //系列配置 series?: Array<BarSeriesOption> //x軸配置 xAxis?: Array<XAXisOption> //y軸配置 yAxis?: Array<YAXisOption> //圖例配置 legend?: LegendComponentOption //區域縮放配置 dataZoom?: Array<DataZoomComponentOption> //圖形高度 height?: number | string //資料集 datasetSource?: Array<any> //綜合配置 options?: ChartSetting }>(), { data: () => [], xAxisData: () => [], title: 'ECharts柱狀圖', } ) ...... //繪製 async function drawChart() { let datasetSource: Array<any> | undefined = props.datasetSource, series: Array<BarSeriesOption> = [], xAxisData: Array<string> = props.xAxisData if (props.options) { if (props.options.apiMethod) { //獲取介面資料作為資料集 datasetSource = await props.options.apiMethod() if (props.options.xProp) { //根據配置的x軸屬性名生成x軸資料 xAxisData = [] datasetSource?.forEach(data => { xAxisData.push(data[props.options.xProp]) }) } } if (props.options.sereisOption) { props.options.sereisOption.forEach(opt => { series.push({ name: '數量', type: 'bar', barMaxWidth: 30, emphasis: { focus: 'series' }, label: { show: true, position: 'top', color: 'inherit' }, ...opt }) }) } } else { series = props.series ? props.series : [{ name: '數量', type: 'bar', barMaxWidth: 30, emphasis: { focus: 'self' }, label: { show: true, position: 'inside', color: '#fff' }, data: props.data }] } let xAxis: Array<XAXisOption> = props.xAxis ? props.xAxis : [{ type: 'category', axisTick: { show: false }, data: xAxisData }] ...... const options: ECOption = { ...... dataset: { source: datasetSource }, series: series } //開啟notMerge保證配置資料不會疊加 chart.value?.setOption(options, { notMerge: true }); }
其中ChartSetting
是一個自定義型別,放在了專案types
目錄下的ChartData.ts
中:
/* @/types/ChartData.ts */ export interface SeriesData { name?: string data?: number[] color?: string yAxisIndex?: number, radius?: string | string[], itemStyle?: any, encode?: { x?: string y?: string itemName?: string value?: string } } export interface ChartSetting { //api介面方法 apiMethod: Function // x軸屬性名 xProp: string //圖例配置 sereisOption: SeriesData[] }
現在再呼叫柱狀圖元件就更進一步簡化了配置過程:
<template> <el-card> <template #header> <el-button type="primary" @click="search">查詢</el-button> </template> <BarChart title="多型別柱狀圖" :height="500" :options="chartOption" /> </el-card> </template> <script setup> import BarChart from '@/components/charts/BarChart.vue' import type { ChartSetting } from '@/types/ChartData' import { ref } from 'vue' //查詢引數 const queryData = ref<any>({ count: 0 }) const chartOption = ref<ChartSetting>() //獲取數據接口 function getDataApi(params?: any) { //模擬axios請求返回的資料 //return axios.request(...) return new Promise((resolve) => { params.count++ let randomInt = Math.floor(Math.random() * 50 + params.count) resolve([ { name: '襯衫', saleNum: 17 + randomInt, stockNum: 5 + randomInt }, { name: '羊毛衫', saleNum: 43 + randomInt, stockNum: 20 + randomInt }, { name: '雪紡衫', saleNum: 5 + randomInt, stockNum: 36 + randomInt }, { name: '褲子', saleNum: 28 + randomInt, stockNum: 10 + randomInt }, { name: '高跟鞋', saleNum: 10 + randomInt, stockNum: 10 + randomInt }, { name: '襪子', saleNum: 36 + randomInt, stockNum: 20 + randomInt } ]) }) } function search() { chartOption.value = { apiMethod: () => getDataApi(queryData.value), xProp: 'name', sereisOption: [ { name: '庫存量', encode: { x: 'name', y: 'stockNum' } }, { name: '銷售量', encode: { x: 'name', y: 'saleNum' } }, ] } } search() </script>
將api封裝進柱狀圖元件的好處是,當我們要實現類似這樣的頁面時:
只需要進行簡單的配置就可以完成:
<template> <el-row :gutter="16"> <el-col v-for="(item, index) in chartOptionList" :key="index" :lg="12" style="margin-bottom: 10px;"> <el-card> <BarChart :title="item.title" :height="item.height || 300" :options="item.chartOption" /> </el-card> </el-col> </el-row> </template> <script setup> import BarChart from '@/components/charts/BarChart.vue' import type { ChartSetting } from '@/types/ChartData' import { ref } from 'vue' //axios api請求方法 import { getDataApi1, getDataApi2, getDataApi3, getDataApi4 } from '@/assets/api/chartAPI' interface ChartCard { //標題 title?: string, //高度 height?: number, //圖表y軸配置 yAxis?: Array<any> chartOption: ChartSetting } const chartOptionList = ref<ChartCard[]>([ { title: '圖1', chartOption: { apiMethod: () => getDataApi1(), xProp: 'name', sereisOption: [ { name: '數量', encode: { x: 'name', y: 'count' } } ] } }, { title: '圖2', chartOption: { apiMethod: () => getDataApi2(), xProp: 'date', sereisOption: [ { name: '數量', encode: { x: 'date', y: 'count' } }, ] } }, { title: '圖3', chartOption: { apiMethod: () => getDataApi3(), xProp: 'name', sereisOption: [ { name: '庫存量', encode: { x: 'name', y: 'stockNum' } }, { name: '銷售量', encode: { x: 'name', y: 'saleNum' } }, ] } }, { title: '圖4', chartOption: { apiMethod: () => getDataApi4(), xProp: 'date', sereisOption: [ { name: '剩餘量', encode: { x: 'date', y: 'num1' } }, { name: '進貨量', encode: { x: 'date', y: 'num2' } }, ] } }, ]) </script>
五、總結與後期改進
在進行了一系列的完善後,最終我們得到了這樣一個柱狀圖元件:
/* @/components/BarChart.vue */ <template> <div ref="chartDom" :style="{ height: getHeight }"></div> </template> <script setup> import { echarts, type ECOption } from '@/utils/echarts' import { ref, shallowRef, watch, computed, onMounted, onBeforeUnmount, type ShallowRef, type Ref } from 'vue' import type { EChartsType } from 'echarts/types/dist/core' import type { XAXisOption, YAXisOption, LegendComponentOption, BarSeriesOption, DataZoomComponentOption } from 'echarts/types/dist/shared' import resize from '@/utils/resize' import type { ChartSetting } from '@/types/ChartData' //定義元件屬性 const props = withDefaults( defineProps<{ //資料 data?: Array<string | number> //x軸資料 xAxisData?: Array<string> //圖表標題 title?: string //系列配置 series?: Array<BarSeriesOption> //x軸配置 xAxis?: Array<XAXisOption> //y軸配置 yAxis?: Array<YAXisOption> //圖例配置 legend?: LegendComponentOption //區域縮放配置 dataZoom?: Array<DataZoomComponentOption> //圖形高度 height?: number | string //資料集 datasetSource?: Array<any> //綜合配置 options?: ChartSetting }>(), { data: () => [], xAxisData: () => [], title: 'ECharts柱狀圖', } ) //要渲染的Dom元素 const chartDom: Ref<HTMLDivElement | null> = ref(null) //渲染的chart物件要用shallowRef const chart: ShallowRef<EChartsType | null | undefined> = shallowRef(null) //高度同時支援string和number const getHeight = computed(() => { return typeof props.height === 'number' ? props.height + 'px' : props.height }) //監聽資料變化,重新繪製 watch( () => props, () => { drawChart() }, { deep: true } ) //繪製 async function drawChart() { let datasetSource: Array<any> | undefined = props.datasetSource, series: Array<BarSeriesOption> = [], xAxisData: Array<string> = props.xAxisData if (props.options) { if (props.options.apiMethod) { //獲取介面資料作為資料集 datasetSource = await props.options.apiMethod() if (props.options.xProp) { //根據配置的x軸屬性名生成x軸資料 xAxisData = [] datasetSource?.forEach(data => { xAxisData.push(data[props.options.xProp]) }) } } if (props.options.sereisOption) { props.options.sereisOption.forEach(opt => { series.push({ name: '數量', type: 'bar', barMaxWidth: 30, emphasis: { focus: 'series' }, label: { show: true, position: 'top', color: 'inherit' }, ...opt }) }) } } else { series = props.series ? props.series : [{ name: '數量', type: 'bar', barMaxWidth: 30, emphasis: { focus: 'self' }, label: { show: true, position: 'inside', color: '#fff' }, data: props.data }] } let xAxis: Array<XAXisOption> = props.xAxis ? props.xAxis : [{ type: 'category', axisTick: { show: false }, data: xAxisData }] let yAxis: Array<YAXisOption> = props.yAxis ? props.yAxis : [{ type: 'value', minInterval: 1 }] let legend: LegendComponentOption = props.legend ? props.legend : { show: true, type: 'scroll', orient: 'horizontal', top: 25, left: 'center' } let dataZoom: Array<DataZoomComponentOption> = props.dataZoom ? props.dataZoom : [] const options: ECOption = { backgroundColor: '', title: { text: props.title }, tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, // appendToBody:true }, legend: legend, grid: { left: 10, right: 10, bottom: props.dataZoom ? 40 : 10, containLabel: true }, toolbox: { show: true, feature: { magicType: { type: ['line', 'bar'] }, dataView: { readOnly: false }, saveAsImage: {} } }, xAxis: xAxis, yAxis: yAxis, dataZoom: dataZoom, dataset: { source: datasetSource }, series: series } //開啟notMerge保證配置資料不會疊加 chart.value?.setOption(options, { notMerge: true }); } const { chartObject, addResize, removeResize } = resize() onMounted(() => { chart.value = echarts.init(chartDom.value); drawChart() //新增視窗自適應 chartObject.value = chart.value addResize() }) onBeforeUnmount(() => { removeResize() chart.value?.dispose() }) </script>
這個元件支援僅傳入橫、縱座標資料來顯示基礎的柱狀圖,也支援傳入JSON配置來顯示多系列的複雜柱狀圖,足以應對日常基本需求。同樣地,利用類似的設計邏輯,我們也能輕鬆地封裝出餅圖、折線圖和熱力圖等元件。
不過在實際專案應用中,特別是面對大屏展示等複雜多變的視覺化需求時,現有的封裝形式可能還略顯不足,因此仍有待進一步拓展和最佳化。在後續的改進中,我們可以進一步最佳化元件的功能和效能,以滿足不同專案的需求:比如可以為這個元件擴充套件更多的動態配置項屬性,如tooltip、grid等,使其可以更靈活地使用;也可以對series屬性做更好的適配,將所有的圖表元件整合為一個,僅透過配置不同的sereis.type
,就可以讓這個元件展示柱狀圖、餅圖或折線圖等。
在Vue 3中封裝ECharts元件無疑為前端開發人員提供了一種高效、便捷的方式來構建視覺化圖表,極大地提升了開發效率和程式碼複用性。但值得注意的是,封裝元件更適合用於處理基礎圖表的構建,而在面對高度定製化的ECharts圖表時,過度依賴封裝可能會增加程式碼的複雜性和維護成本。因此,是否選擇封裝ECharts元件應根據具體專案需求進行權衡。
本文是在借鑑現有ECharts元件封裝經驗的基礎上,提供了一種可行的封裝的技巧和思路,也許並不是最優的解決方案。希望透過這篇文章能夠啓發大家的思考,幫助大家更便捷地在Vue 3專案中使用ECharts。