这样封装echarts简单好用

发布时间 2023-03-22 19:15:17作者: 南风晚来晚相识

为什么要去封装echarts?

在我们的项目中,有很多的地方都使用了echarts图表展示数据。
在有些场景,一个页面有十多个的echarts图。
这些echarts只是展示的指标不一样。
如果我们每一个echarts图都写一份配置型的话,
会有非常多的冗余代码,并且如果需要某一个配置项。
我们需要给一个图修改一次,这样不仅麻烦,还恶心。
为了方便后面的维护,我们决定将echarts做一个简单实用的封装

我们将实现以下这些功能

1.父组件只需要传递X轴和Y轴的数据。
2.如果无数据的话,将展示暂无数据。
3.在渲染之前清空当前实例(会移除实例中所有的组件和图表)
4.子组件用watch监听数据变化达到数据变化后立刻跟新视图
5.给一个页面可以单独配置echarts的各个属性
6.可以设置多条折线图
7.根据屏幕大小自动排列一行显示多少个图
8.echarts随屏幕大小自动进行缩放
由于echarts的类型很多,我们这里只对折线图进行封装
其他类型的图,我们可以按照这个思路来就行。

父组件传递X轴和Y轴数据以及自动显示暂无数据

1.父组件通过 echartsData 进行传递echarts各个坐标的数据。
2.this.echartsData.Xdata 来判断是否显示暂无数据
3.通过ref来获取dom节点。为什么不使用 id来获取echarts呢?
因为id重复的话将会导致echarts无法渲染。
<template>
  <div>
    <div class="box">
      <echartsLine v-for="(item,index) in listArr" 
      :echartsData="item" :key="index"></echartsLine>
    </div>
  </div>
</template>
<script>
import echartsLine from "@/components/echarts/echarts-line.vue"
export default {
data() {
  return {
    // 父组件传递的数据
    listArr: [
      { 
        Xdata: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
        Ydata: [10, 30, 50, 60, 70, 80, 90],
      },
      {
        Xdata: [], // 表示X横坐标的数据
        Ydata: [], // Y纵坐标的数据
      }
    ]
  }
},
components: {
  echartsLine
}
}
</script>

子组件

<template>
  <div>
    <div class="chart"  ref="demo"></div>
  </div>
</template>
<script>
import echarts from 'echarts'
export default {
  props: {
    echartsData: { // 接受父组件传递过来的参数
      type: Object,
      default: () => {
        return  {
          Xdata:[],
          Ydata: [],
        }
      }
    }
  },
  data() {
    return {
      // echarts的dom节点实例
      char: null
    }
  },
  mounted() {
   this.showEcharts()
  },
  methods:{
    showEcharts(){
    // 获取dom节点,
    let demo = this.$refs.demo
    // 初始化echarts
    this.char = echarts.init(demo);
    // 在渲染之前清空实例
    this.char.clear()
    let option = {}
    // 如果无数据的话,将展示暂无数据
    if (this.echartsData.Xdata && this.echartsData.Xdata.length == 0) {
      option = {
        title: {
          text: '暂无数据',
          x: 'center',
          y: 'center',
          textStyle: {
            fontSize: 20,
            fontWeight: 'normal',
          }
        }
      }
    } else {
      option = {
        xAxis: {
          type: 'category',
          data: this.echartsData.Xdata
        },
        yAxis: {
          type: 'value'
        },
        series: [
          {
            data: this.echartsData.Ydata,
            type: 'line',
            smooth: true
          }
        ]
      };
    }
    this.char.setOption(option);
    }
  }
}
</script>

props中的数据更新后为什么视图没有重新渲染?

如果按照上面这样的写法,我们新增一个点击按钮跟新数据,。
echarts图表是不会变化的。
因为在子组件中渲染是在mounted中被触发的,一个图表只会触发一次。
即使后面我们更新了数据,子组件中的 mounted 不会被执行。
所以不会在重新更新视图。
我们可以使用wachtch来解决这个问题

watch来解决数据变化后视图立即更新

<!-- 父组件更新数据updateHandler  -->
<template>
  <div>
    <el-button @click="updateHandler">跟新数据</el-button>
    <div class="box">
      <echartsLine v-for="(item,index) in listArr" 
        :echartsData="item" :key="index">
      </echartsLine>
    </div>
  </div>
</template>

data() {
  return {
    listArr: [
      {
        Xdata: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
        Ydata: [10, 30, 50, 60, 70, 80, 90],
        id:'demo01'
      },
      {
        Xdata: [],
        Ydata: [],
        id: 'demo02'
      }
    ]
  }
},
methods: {
  updateHandler() {
    this.listArr[1].Xdata=['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
    this.listArr[1].Ydata = [101, 230, 250, 260, 720, 820, 290]
  }
}
<!-- 子组件使用watch进行监听 关键代码-->
mounted() {
  this.showEcharts()
},
methods:{
  showEcharts(){
    // 渲染了 echarts
  }
},
watch: {
  // echartsData 是props中传递给echarts中需要渲染的数据
  // 通过watch监听属性去监视props 中echartsData数据的变化
  // 当属性发生变化的时候,调用showEcharts方法重现渲染echarts图表
  echartsData: {
    handler(newVal, oldVal) {
      this.showEcharts()
    },
    // 这里的deep是深度监听,因为我们传递过来的是一个对象
    deep: true,
  }
},

每个页面可以单独配置echarts的各个属性

按照我们目前的写法,父页面无法对echarts图表进行配置。
因为我们子组件中的配置项写死了。
为了是组件更加的灵活,我们需要对子组件中的配置项进行修改。
让它可以接收父页面中的配置项哈,我们将使用 Object.assign 将它实现
// 父组件进行单独设置某一个配置项 
updateHandler() {
  this.listArr[1].Xdata = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  this.listArr[1].Ydata = [101, 230, 250, 260, 720, 820, 290]
  // 点击按钮的时候,右边的那个echarts 图不显示Y轴线
  this.listArr[1]['setOptionObj'] = {
    yAxis: [{
      type: 'value',
      show: false,// 是否显示坐标轴中的y轴
    }]
  }
}
// 子组件使用 Object.assign 对数据进行合并
props: {
  echartsData: {
    type: Object,
    default: function() {
      return  {
        Xdata:[],
        Ydata: [],
        setOptionObj: { }
      }
    }
  },
},
// xxxx 其他代码
option = {
    xAxis: {
      type: 'category',
      data: this.echartsData.Xdata
    },
    yAxis: {
      type: 'value'
    },
    series: [
      {
        data: this.echartsData.Ydata,
        type: 'line',
        smooth: true
      }
    ]
  };
// xxxx 其他代码
// 使用对象合并的方式让父组件可以对配置项可以单独设置
option= Object.assign(option, this.echartsData.setOptionObj)
// 设置 echats,在页面上进行展示
this.char.setOption(option);

可以设置多条折线图

按照我们目前的代码,是无法设置多条折线的。
多条折线 series 中有多条数据,单条只有一条
单条折线的 series: [{
  data: [820, 932, 901, 934, 1290, 1330, 1320],
  type: 'line',
  smooth: true
}]
多条折线 series: [{
  name: 'Email',
  type: 'line',
  stack: 'Total',
  data: [120, 132, 101, 134, 90, 230, 210]
},
{
  name: 'Union Ads',
  type: 'line',
  stack: 'Total',
  data: [220, 182, 191, 234, 290, 330, 310]
}]
所以我们只要判断是否有series字段,如果有说明是多条折线。
否者就是单条折线 
优化一下子组件中的代码
// 父页面
updateHandler() {
  this.listArr[1].Xdata = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  this.listArr[1].Ydata = [101, 230, 250, 260, 720, 820, 290]
  this.listArr[1]['setOptionObj'] = {
    yAxis: [{
      type: 'value',
      show: false,// 是否显示坐标轴中的y轴
    }]
  }
  // 设置多条折线
  this.listArr[1]['series'] = {
    data: [{
      name: 'Email',
      type: 'line',
      stack: 'Total',
      data: [120, 132, 101, 134, 90, 230, 210]
    },
    {
      name: 'Union Ads',
      type: 'line',
      stack: 'Total',
      data: [220, 182, 191, 234, 290, 330, 310]
    }]
  }
}
// 子组件
// xxxx 其他代码
option = {
  xAxis: {
    type: 'category',
    data: this.echartsData.Xdata
  },
  yAxis: {
    type: 'value'
  },
  series: []
};
// 如果父组件中有 series 这个字段,我们渲染多条折线
if (this.echartsData.series 
    && this.echartsData.series.data 
    && this.echartsData.series.data.length){
    let legendArr =[]
    for (let i = 0; i < this.echartsData.series.data.length; i++){
      option.series.push(this.echartsData.series.data[i])
      legendArr.push(this.echartsData.series.data[i].name)
    }
    // 同时默认设置设置 legend, 当然父组件是可以到单独设置的
    option.legend = {
      x: 'center',
      data: legendArr,
      icon: "circle", // 这个字段控制形状 类型包括 circle,rect ,roundRect,triangle,diamond,pin,arrow,none
      itemWidth: 10, // 设置宽度
      itemHeight: 10, // 设置高度
      itemGap: 32 // 设置间距
    }
  } else {
    // 否者就是单条折线
    option.series.push({
      data: this.echartsData.Ydata,
      type: 'line',
      smooth: true
    })
  }
  // 使用对象合并的方式让父组件可以对配置项可以单独设置
  option= Object.assign(option, this.echartsData.setOptionObj)
}
this.chart.setOption(option);

根据屏幕大小自动排列一行显示多少个图

由于用户的设备不同,有大有小。
所以我们需要对一行显示多少个进行自动调整。
我们将使用 el-row 和 el-col 来实现
我们会获取用户的屏幕大小。
然后控制 el-col中的 span 的大小来决定一行显示多少个
 <el-row :gutter="20" class="el-row-box">
  <el-col class="el-col-m" :span="gutterNum" 
    v-for="(item, index) in listArr" :key="index">
    <div class="grid-content bg-purple">
      <echartsLine  :echartsData="item" ></echartsLine>
    </div>
  </el-col>
</el-row>

gutterNum:8, // 默认一行显示3个图

created() {
  // 获取页面的宽高可以在 created 函数中,
  // 如果获取的是dom节点者【最早】需要在 mounted
  // 以前以为获取页面宽高需要在 mounted中
  this.getClientWidth()
},
// 注册事件,进行监听
mounted(){
  window.addEventListener('resize', this.getClientWidth)
},
beforeDestroy(){
  window.removeEventListener('resize', this.getClientWidth)
},
 methods: {
    getClientWidth() {
      // 获取屏幕宽度按动态分配一行几个图
      let clientW = document.body.clientWidth;
      console.log('clientW', clientW)
      if (clientW >= 1680) {
        this.gutterNum = 8
      } else if(clientW >= 1200){
        this.gutterNum = 12
      } else if(clientW < 1200){
        this.gutterNum = 24
      }
    },
}



echarts随屏幕大小自动进行缩放

我们将会使用echarts提供的 resize 方法来进行缩放屏幕的大小。
在mounted注册监听屏幕大小变化的事件,然后调用 resize
data() {
  return {
    char: null
  }
},

mounted() {
  console.log('有几个echarts图,mounted函数就会被执行几次')
  this.showEcharts()
  window.addEventListener('resize', this.changeSize)
},
beforeDestroy() {
  console.log('有几个echarts图,beforeDestroy函数就会被执行几次')
  window.removeEventListener('resize', this.changeSize)
},
methods: {
  changeSize() {
    console.log('这里有可能是undefined为啥还可以正常缩放echarts', this.chart)
    this.char && this.char.resize()
  }
}


总结

1. 使用watch去监听props中的对象,不能这样写
watch: {
   // echartsData假设为props中定义了的。
   echartsData: function (newValue,oldValue) {
    console.log('newValue', newValue);
    console.log('oldValue', oldValue);
  },
  deep: true,
}
上面这样去监听对象将无法触发。上面这样的只能够监听基本数据类型
我们应该改写为:
watch: {
  echartsData: {
      handler() {
        this.showEcharts()
      },
      deep: true,
    }
}

2.子组件中 mounted 将会被多次渲染。
它的渲染次数取决于父页面中需要显示多少个echarts图。
这也是为什么echarts不会渲染出错(A指标中数据不会被渲染到C指标中)
同理,由于子组件中mounted 将会被多次渲染,它会给每一个echarts注册上缩放事件(resize)
离开的页面的时候,beforeDestro也将会被多次触发,依次移除监听事件

3.获取文档中页面的大小可以放在created。
以前看见其他小伙伴document.body.clientWidth 是写在 mounted 中的。
不过获取节点只能写在 mounted 中

4.小伙伴可能发现了,this.char 也就是echarts的实例是undefined。
也可以正常的缩放成功呢?
这个问题我们下次可以讲一下。
各位大佬,麻烦点个赞,收藏,评论

全部代码

父页面
<template>
  <div class="page-echarts">
    <el-button @click="updateHandler">跟新数据</el-button>
    <el-row :gutter="20" class="el-row-box">
      <el-col class="el-col-m" :span="gutterNum" v-for="(item, index) in listArr" :key="index">
        <div class="grid-content bg-purple">
          <echartsLine  :echartsData="item" ></echartsLine>
        </div>
      </el-col>
    </el-row>
  </div>
</template>
<script>
import echartsLine from "@/components/echarts/echarts-line.vue"
export default {
  components: {
    echartsLine
  },
  data() {
    return {
      gutterNum:8,
      listArr: [
        {
          Xdata: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
          Ydata: [10, 30, 50, 60, 70, 80, 90],
          id:'demo01'
        },
        {
          Xdata: [],
          Ydata: [],
          id: 'demo02',
        },
        {
          Xdata: [],
          Ydata: [],
          id: 'demo03',
        },
      ]
    }
  },
  created() {
    // 获取页面的宽高可以在 created 函数中,
    // 如果获取的是dom节点者【最早】需要在 mounted
    // 以前以为获取页面宽高需要在 mounted中
    this.getClientWidth()
  },
  mounted() {
    // 注册事件,进行监听
    window.addEventListener('resize', this.getClientWidth)
  },
  beforeDestroy(){
    window.removeEventListener('resize', this.getClientWidth)
  },
  methods: {
    getClientWidth() {
      // 获取屏幕宽度按动态分配一行几个图
      let clientW = document.body.clientWidth;
      console.log('clientW', clientW)
      if (clientW >= 1680) {
        this.gutterNum = 8
      } else if(clientW >= 1200){
        this.gutterNum = 12
      } else if(clientW < 1200){
        this.gutterNum = 24
      }
    },
    updateHandler() {
      this.listArr[1].Xdata = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
      this.listArr[1].Ydata = [101, 230, 250, 260, 720, 820, 290]
      this.listArr[1]['setOptionObj'] = {
        yAxis: [{
          type: 'value',
          show: false,// 是否显示坐标轴中的y轴
        }]
      }
      this.listArr[1]['series'] = {
        data: [{
          name: 'Email',
          type: 'line',
          stack: 'Total',
          data: [120, 132, 101, 134, 90, 230, 210]
        },
        {
          name: 'Union Ads',
          type: 'line',
          stack: 'Total',
          data: [220, 182, 191, 234, 290, 330, 310]
        }]
      }
    }
  }
}
</script>

<style lang="scss" scoped>
// 有些是否感觉 x轴有滚动条
.page-echarts{
  overflow: hidden;
}
.el-row-box{
  margin-left: 0px !important;
  margin-right: 0px !important;
}
.el-col-m{
  margin-bottom: 10px;
}
</style>
子组件
<template>
  <div class="echarts-box">
    <div :style="{ height:height}" class="chart" :id="echartsData.id" ref="demo"></div>
  </div>
</template>
<script>
import echarts from 'echarts'
export default {
  props: {
    height: {
      type: String,
      default:'300px' 
    },
    echartsData: {
      type: Object,
      default: function() {
        return  {
          Xdata:[],
          Ydata: [],
          setOptionObj: { }
        }
      }
    },
    showData: {
      type: String,
    }
  },
  data() {
    return {
      char: null
    }
  },
  mounted() {
    console.log('有几个echarts图,mounted函数就会被执行几次')
    this.showEcharts()
    window.addEventListener('resize', this.changeSize)
  },
  beforeDestroy() {
    console.log('有几个echarts图,beforeDestroy函数就会被执行几次')
    window.removeEventListener('resize', this.changeSize)
  },
  watch: {
    // 通过watch监听属性去监视props 中echartsData数据的变化
    // 当属性发生变化的时候,调用showEcharts方法重现渲染echarts图表
    echartsData: {
      handler() {
        this.showEcharts()
      },
      // 这里的deep是深度监听,因为我们传递过来的是一个对象
      deep: true,
    }
  },
  methods: {
    changeSize() {
      console.log('这里有可能是undefined为啥还可以正常缩放echarts', this.chart)
      this.char && this.char.resize()
    },
    showEcharts() {
      // 获取dom节点,
      let demo=this.$refs.demo
      // 初始化echarts
      this.char = echarts.init(demo)
      this.char.clear()  // 在渲染之前清空实例
      let option = {}
      // 如果无数据的话,将展示暂无数据
      if (this.echartsData.Xdata && this.echartsData.Xdata.length == 0) {
        option = {
          title: {
            text: '暂无数据',
            x: 'center',
            y: 'center',
            textStyle: {
              fontSize: 20,
              fontWeight: 'normal',
            }
          }
        }
      } else {
        option = {
          xAxis: {
            type: 'category',
            data: this.echartsData.Xdata
          },
          yAxis: {
            type: 'value'
          },
          series: []
        };
        // 如果父组件中有 series 这个字段,我们渲染多条折线
        if (this.echartsData.series && this.echartsData.series.data&& this.echartsData.series.data.length) {
          let legendArr =[]
          for (let i = 0; i < this.echartsData.series.data.length; i++){
            option.series.push(this.echartsData.series.data[i])
            legendArr.push(this.echartsData.series.data[i].name)
          }
          // 同时默认设置设置 legend, 当然父组件是可以到单独设置的
          option.legend = {
            x: 'center',
            data: legendArr,
            icon: "circle", // 这个字段控制形状 类型包括 circle,rect ,roundRect,triangle,diamond,pin,arrow,none
            itemWidth: 10, // 设置宽度
            itemHeight: 10, // 设置高度
            itemGap: 32 // 设置间距
          }
        } else {
          // 否者就是单条折线
          option.series.push({
            data: this.echartsData.Ydata,
            type: 'line',
            smooth: true
          })
        }
        // 使用对象合并的方式让父组件可以对配置项可以单独设置
        option= Object.assign(option, this.echartsData.setOptionObj)
      }
      this.char.setOption(option);
    }
  }
}
</script>

<style scoped>
.echarts-box{
  width: 100%;
  height: 100%;
}
.chart {
  background: #eee7e7;
}
</style>