切換語言為:簡體
在 Vue3 專案中封裝 ECharts 元件技巧和思路

在 Vue3 專案中封裝 ECharts 元件技巧和思路

  • 爱糖宝
  • 2024-06-13
  • 2172
  • 0
  • 0

目錄

  • 一、相關知識

    • 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 元件有以下幾個好處:

  1. 簡化使用:封裝後的元件可以提供更簡潔、更易用的 API,我們能夠更快速地完成頁面中 ECharts圖表的繪製。

  2. 隱藏實現細節:封裝可以隱藏 ECharts的具體實現細節,讓我們不必過多關心圖表配置項和底層實現,只需關注如何處理資料來進行展示。

  3. 提高複用性:封裝後的元件可以被多個頁面或專案共享使用,提高了程式碼的複用性和可維護性。

  4. 增強擴充套件性:封裝可以在原有基礎上進行功能擴充套件,比如新增自定義互動、動畫效果等,從而滿足更多定製化的需求。

  5. 提高可維護性:封裝可以將相關的程式碼邏輯集中在一個元件中,便於維護和管理,減少程式碼冗餘和維護成本。

透過封裝 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>


在 Vue3 專案中封裝 ECharts 元件技巧和思路

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>


在 Vue3 專案中封裝 ECharts 元件技巧和思路

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>


在 Vue3 專案中封裝 ECharts 元件技巧和思路

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>


在 Vue3 專案中封裝 ECharts 元件技巧和思路

將api封裝進柱狀圖元件的好處是,當我們要實現類似這樣的頁面時:

在 Vue3 專案中封裝 ECharts 元件技巧和思路

只需要進行簡單的配置就可以完成:

<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。

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.