Vue2中ECharts简单封装的一种实践-详细版

ECharts是著名的开源JavaScript图表库,支持20+图表类型,常用的有折线图、柱状图和饼图,这里是基于vue2对其进行封装的一种实践。在此之前建议通读以下篇章:

  1. ECharts-教程篇章
  2. ECharts-使用手册篇章

封装的原因:

  1. 直接使用存在冗余,与业务逻辑耦合,增加复杂度。

使用ECharts的基本步骤是echarts.init(dom)生成实例,然后setOption,即可渲染出不同的图表,在图表容器销毁之后,调用 echartsInstance.dispose 销毁实例。这是一个完整的步骤,但在真实的业务环境中,一个页面会有很多种不同的图表,此时我们可能需要去关注随业务变化带来的图表的创建、setOption和销毁,并会在代码中存在大量的option,实际上这会增加组件或页面的复杂度,会存在代码冗余的问题,图表的展示逻辑与业务逻辑耦合也会增加复杂度;

  1. 直接使用粒度较大,不方便复用。

封装的思路或需求

  • ECharts 按需导入,减少打包体积
  • 屏幕尺寸发生变化自动缩放
  • 自带loading
  • 无数据缺省设计为插槽(待确认)或默认缺省提示
  • 使用ref代替id的dom(防止id重复性)
  • 保持简单、可读性与扩展性,不过度封装
  • 将不同页面的不同样式的option公共配置抽离为文件,具体使用时,仅更新必要的数据,做到基本的样式与数据分离配置(动态配置与静态配置分离)。
  • 封装组件的权衡:开放性 & 可维护性 & 适合真实需求 > 性能

目录结构、代码与示例

不同数据粒度的图表更新:

图表实例新建和销毁

kotlin 复制代码
this.chart = this.$echarts.init(this.$refs.chart, "", {
        renderer: this.type
      }) //创建一个 ECharts 实例
this.chart.setOption(this.chartOption)  //设置图表实例的配置项以及数据,万能接口,
//所有参数和数据的修改都可以通过 setOption 完成,ECharts 会合并新的参数和数据,然后刷新图表
kotlin 复制代码
this.chart.dispose() //销毁实例,销毁后实例无法再被使用。

图表类型改变 & 数据改变 (实例不用销毁)

kotlin 复制代码
this.chart.clear() //清空当前实例,会移除实例中所有的组件和图表。
this.chart.setOption(this.chartOption) //更新视图

图表类型不变 & 仅改变展示数据 或 其他组件(legend、tooltip等)(可能随特定数据改变,尽可能在配置函数内实现则后续无需更新)

kotlin 复制代码
// 方式1: 修改整体option后重新setOption
this.chart.setOption(this.chartOption) 
// 方式2: 只需给setOption传递要修改的属性
this.chart.setOption({xAxis: {type: "category",data: [],}}) 
//传入一个option,其中仅添加需要修改的属性,ECharts会与之前的进行自动合并,更多参数见文档

所有数据不变,所在节点宽高变化,此时重新渲染,仅改变图表尺寸

kotlin 复制代码
this.chart.resize() //场景1.改变图表**尺寸,在容器大小发生改变时需要手动调用。
//场景2.初始隐藏的标签在初始化图表的时候因为获取不到容器的实际高宽,可能会绘制失败,
//因此在切换到该标签页时需要手动调用 resize** 方法获取正确的高宽并且刷新画布

渲染图表的过程

全量更新

  1. 组装option

    1. 通过函数获取样式配置
    2. 通过接口获取数据,添加option中的数据字段
  2. 组件内部监听option修改自动setOption

增量更新

  1. 通过接口获取变化的数据,

  2. 判断option是否存在

    1. 存在则不需要再通过函数获取样式配置
    2. 直接更新需要改变的数据字段
  3. 组件内部监听option修改自动setOption

增量更新的另一种思路:

  1. 通过接口获取变化的数据

  2. 判断option是否存在

    1. 组装变化的option
    2. 通过组件的setOption来直接渲染更新图表

图表的三种状态:

  1. 加载

    初始化图表时,我们有 无数据的只有样式配置的option,此时已经可以渲染出来,但实际图表应该展示loading,实现方式:

    a. 传参数,表示是否业务数据已就绪,可以使用ECharts自带的loading或者组件(自己写的或组件库提供的); 如果使用组件,问题在于如何让ECharts不渲染,当ECharts实例被初始化但无option时,会占位但不会渲染canvas,还有一种方式就是封装一个更小的chart组件,但如果更新数据时都需要v-if,就会引起ECharts实例的频繁创建和释放,这里的问题在于如何在chart内部既不会频繁创建EChart实例也能够支持通过loading组件的方式实现加载状态

  2. 展示

  3. 无数据缺省展示

目录结构

安装: npm i echarts -S npm i resize-detector -S

less 复制代码
|- components
    |- chart-view //图表组件
    |- index.vue
    |- options //所有页面图表配置,以页面作为文件夹
        |- sales-statistics
            |- chart1-bar.js //具体某个图表配置,详见注1
        |- chart2-pie.js
|- plugins
    |- charts.js //按需导入
|- view
    |- sales
        |- sales-statistics.vue //销售统计页面
|- main.js //vue主文件

注1: 这里个人命名规则为'chart序号-图表类型.js': 如sales-statistics/chart1-pie,就表示销售统计页面的第一个图表,类型为柱状图。 序号规则一般为从左往右、从上到下,方便快速按页面和位置找到对应图表配置文件。

相关代码

按需导入echarts

javascript 复制代码
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口
import * as echarts from "echarts/core"
​
// 引入常用的图表,图表后缀都为 Chart
import { BarChart, LineChart, ScatterChart, PieChart } from "echarts/charts"
​
// 引入提示框,标题,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
import {
  TitleComponent,
  TooltipComponent,
  GridComponent,
  DatasetComponent,
  TransformComponent,
  LegendComponent,
  DataZoomComponent
} from "echarts/components"
​
// 标签自动布局,全局过渡动画等特性
import { LabelLayout, UniversalTransition } from "echarts/features"
​
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { CanvasRenderer } from "echarts/renderers"
​
// 注册必须的组件
echarts.use([
  TitleComponent,
  TooltipComponent,
  GridComponent,
  DatasetComponent,
  TransformComponent,
  LegendComponent,
  DataZoomComponent,
  BarChart,
  PieChart,
  LineChart,
  ScatterChart,
  LabelLayout,
  UniversalTransition,
  CanvasRenderer
])
​
// 导出 echarts
export default echarts
​

挂载全局方法,注册全局组件

javascript 复制代码
import Vue from "vue"
​
import echarts from "./plugins/charts"
Vue.prototype.$echarts = echarts
import ChartView from "@/components/chart-view/index.vue"
Vue.component(ChartView.name, ChartView) //注册全局组件
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app")

组件实现 与 使用示例

xml 复制代码
<template>
  <div class="chart">
    <div ref="chart" :class="{ 'opacity-0': loading || !hasData }" :style="{ height: height, width: width }" />
    <div v-if="loading" class="loading-box">
      <!-- 1使用相关组件库的loading -->
      <van-loading />
      <!-- 2使用自定义的Loading -->
      <!-- <div class="loading"></div> -->
    </div>
    <div v-if="!loading && !hasData" class="empty-box">
      <slot v-if="$slots.default"></slot>
      <div v-else class="empty-tip">
        <img src="" alt="" class="empty-bg" />
        <div class="tip">暂无数据</div>
      </div>
    </div>
  </div>
</template>
<script>
import { Loading } from "vant"
import { addListener, removeListener } from "resize-detector" //兼容性更好的resize监听
import { debounce } from "./utils"
export default {
  name: "ChartView",
  components: { VanLoading: Loading },
  props: {
    width: {
      type: String,
      default: "100%"
    },
    height: {
      type: String,
      default: "350px"
    },
    autoResize: {
      type: Boolean,
      default: true
    },
    chartOption: {
      type: Object,
      required: true
    },
    type: {
      type: String,
      default: "canvas"
    },
    hasData: {
      type: Boolean,
      default: true
    },
    loading: {
      type: Boolean,
      default: false
    },
    flushClear:{
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      chart: null
    }
  },
  watch: {
    chartOption: {
      deep: true,
      handler(newVal) {
        this.setOptions(newVal)
      }
    }
  },
  mounted() {
    this.initChart()
    if (this.autoResize) {
      addListener(this.$el, this.resizeHandler)
    }
  },
  beforeDestroy() {
    if (!this.chart) {
      return
    }
    if (this.autoResize) {
      removeListener(this.$el, this.resizeHandler)
    }
    this.chart.dispose()
    this.chart = null
  },
  methods: {
    resizeHandler: debounce(function() {
      //this存在
      this.chart.resize()
    }),
    initChart() {
      this.chart = this.$echarts.init(this.$refs.chart, "", {
        renderer: this.type
      })
      this.chart.setOption(this.chartOption)
      this.chart.on("click", this.handleClick)
    },
    handleClick(params) {
      this.$emit("click", params)
    },
    setOptions(option) {
      this.flushClear && this.clearChart()
      this.resizeHandler()
      if (this.chart) {
        this.chart.setOption(option)
      }
    },
    refresh() {
      this.setOptions(this.chartOption)
    },
    clearChart() {
      this.chart && this.chart.clear()
    },
    setOption(option){
      if (this.chart) {
        this.chart.setOption(option)
      }
    }
  }
}
</script>
<style scope lang="scss">
.chart {
  height: 100%;
  position: relative;
}
.loading-box {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  .loading {
    width: 30px;
    height: 30px;
    border: 2px solid #000;
    border-top-color: transparent;
    border-radius: 100%;
    animation: circle infinite 0.75s linear;
  }

  @keyframes circle {
    0% {
      transform: rotate(0);
    }
    100% {
      transform: rotate(360deg);
    }
  }
}
.empty-box {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
.opacity-0 {
  opacity: 0;
}
</style>
css 复制代码
export const getChart1BarOption = rawData => {
  const defaultConfig = {
    tooltip: {
      trigger: "item"
    },

    color: "#FAD632",
    grid: {
      top: 0,
      bottom: 30,
      left: 80
    },
    yAxis: {
      type: "category",
      data: [],
      axisTick: {
        show: false
      },
      axisLabel: {
        color: "#999"
      },
      axisLine: {
        lineStyle: {
          color: "#ccc"
        }
      }
    },
    xAxis: {
      type: "value",

      axisLabel: {
        color: "#999",
        formatter: function(value) {
          if (value % 1 !== 0) {
            return ""
          }
          return value
        }
      },
      splitLine: {
        lineStyle: {
          color: "#eee"
        }
      }
    },
    series: [
      {
        data: [],
        type: "bar",
        barWidth: "9px"
      }
    ]
  }
  return defaultConfig
}
xml 复制代码
<template>
  <div class="page-sales-statistics">
    <chart-view
      :chart-option="chart1_option"
      :has-data="chart1_has_data"
      :auto-resize="true"
      class="chart1-bar"
      height="100%"
    >
    </chart-view>
	</div>
</template>
<script>
import {getChart1BarOption} from '@/components/options/chart1-bar'
export default {
  name: "SalesStatistics",
  data() {
    return {
    	chart1_has_data: false
      chart1_option: null,
    }
  },
  mounted(){
    this.updateChart1Data()
  },
  methods:{
    // 更新图表1数据
  	updateChart1Data(){
      try {
        const res = setTimeout(()=>{
          return [{key:'北京',value:10},{key:'上海',value:9},{key:'广州',value:8}]
        },)
        this.chart_has_data = true;
        // 数组处理与组装,在逻辑上尽量动静分离,静态样式和动态数据分离。
        this.chart1_option = getChart1BarOption() || this.chart1_option
        this.chart1_option.series[0].data = res.map(i => i.value / 100)
        this.chart1_option.yAxis.data = res.map(i => i.key)
        // 数据量很庞大时结合组件方式,采用增量更新
        this.$nextTick(() => {
          this.$refs.chart3bar.setOption({
            series: [
              {
                data: res.map(i => i.value / 100),
                type: "bar",
                barWidth: "9px"
              }
            ]
          })
        })
      } catch (error) {
        console.log('请求异常', error);
      }
    }
  }
}
</script>

提示

  • 通过v-if,达到组件级的渲染粒度控制;(1)
  • 组件默认每次重新渲染为:图表类型改变 & 数据改变 (2)
  • 通过flushClear参数和调用组件setOption达到上述3中方式1和方式2的两种渲染粒度控制;
  • resize后自动setOption,以达到4的渲染粒度控制

相关规则

  1. 通常情况下,在每次调用 setOption 方法之前不需要调用 clear 方法。只有在需要清空现有图表或切换图表类型时,才需要使用 clear 方法。
  2. 示例网站见参考索引23

参考

  1. 在项目中引入 ECharts - 入门篇 - Handbook - Apache ECharts
  2. 大屏图表,ECharts 从"熟练"到入门 - 掘金
  3. 图表容器及大小 - 概念篇 - Handbook - Apache ECharts
  4. Documentation - Apache ECharts
  5. vue项目中封装echarts的比较优雅的方式 - 掘金
  6. 数据大屏:聊聊常见可视化大屏的产品实现 | 人人都是产品经理
  7. 数据可视化在移动端的应用 | 人人都是产品经理
  8. www.youtube.com/watch?v=C3M...
  9. 这样封装echarts超级简单好用 - 掘金
  10. vue3中echarts组件的最佳封装形式 - 掘金
  11. Echarts 在 Vue 中的最佳实践 - 掘金
  12. 在vue项目中封装echarts的正确姿势_vue中封装echarts组件mixin_JessicaLilyAn的博客-CSDN博客
  13. Vue项目中全局引入和按需引入Echarts - 掘金
  14. 我不允许有人不会封装 ECharts - 掘金
  15. Vue项目中如何优雅地封装Echarts - 掘金
  16. GitHub - vueblocks/ve-charts: 📈 ECharts 4.x for Vue.js 2.x. | 📈 ECharts 5.x for Vue.js 3.x in next version.
  17. echarts 快速入门(教学篇) - 掘金
  18. echarts性能优化 - 掘金
  19. echarts 使用小结
  20. React数据大屏的应用实践
  21. 移动端数据可视化图表_林深不见路和鹿-站酷ZCOOL
  22. 一文和你介绍数据可视化:目的、设计、流程及注意事项 | 人人都是产品经理
  23. 全网echarts案例资源大总结和echarts的高效使用技巧(细节版) - 掘金
  24. Echarts图表管理方式总结 - 掘金
  25. 整理echarts的一些常用配置 - 掘金
  26. Vue实用echarts组件封装 - dev-zuo 技术日常
相关推荐
菲力蒲LY2 分钟前
输入搜索、分组展示选项、下拉选取,全局跳转页,el-select 实现 —— 后端数据处理代码,抛砖引玉展思路
java·前端·mybatis
MickeyCV1 小时前
Nginx学习笔记:常用命令&端口占用报错解决&Nginx核心配置文件解读
前端·nginx
祈澈菇凉2 小时前
webpack和grunt以及gulp有什么不同?
前端·webpack·gulp
zy0101012 小时前
HTML列表,表格和表单
前端·html
初辰ge2 小时前
【p-camera-h5】 一款开箱即用的H5相机插件,支持拍照、录像、动态水印与样式高度定制化。
前端·相机
HugeYLH2 小时前
解决npm问题:错误的代理设置
前端·npm·node.js
六个点3 小时前
DNS与获取页面白屏时间
前端·面试·dns
道不尽世间的沧桑3 小时前
第9篇:插槽(Slots)的使用
前端·javascript·vue.js
bin91533 小时前
DeepSeek 助力 Vue 开发:打造丝滑的滑块(Slider)
前端·javascript·vue.js·前端框架·ecmascript·deepseek
uhakadotcom3 小时前
最新发布的Tailwind CSS v4.0提供了什么新能力?
前端