为什么要去封装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>