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