首页 前端知识 这样封装echarts简单好用

这样封装echarts简单好用

2024-03-17 00:03:22 前端知识 前端哥 205 416 我要收藏
为什么要去封装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>
转载请注明出处或者链接地址:https://www.qianduange.cn//article/3908.html
标签
评论
发布的文章

jQuery事件绑定

2024-04-13 09:04:31

Jquery——基础

2024-04-03 12:04:28

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!