最近接到一个需求, 公司这边需要几个趋势图, 能够直观反映出一系列指标 ; 身为一名专业的 api 调用师, 那肯定是毫不犹豫的就接下来了 😝
需求分析到选择
由于我们这个项目本身就是基于
vue
的项目,所以技术这一块毋庸置疑肯定还是使用vue
既然有几个图表,拆分理解 图表 肯定是
Echarts
,几个那我肯定要想办法封装一下;为什么要封装呢 ?
- 每个开发者在制作图表时都需要从头到尾书写一遍完成的
option
配置十分冗余- 在同一个项目中,各类图表设计十分相似,甚至相同,没有必要一直做重复配置
- 比如窗口缩放的响应式,没有必要每一个图表都重新写一遍
此时好在前段时间看了
vue-element-admin
还有点印象,知道他用了Echarts
😎 码农的优良传统不能丢(主要还是自己封装没有别人的好);那咱们就先看看他是怎么封装的;
Vue-element-admin
既然要看人家的代码,那肯定先
clone
到本地;然后跑起来,过程我就不说了,大家都知道
经过我们的观察,页面中用到图表的就这几个页面,现在我们去代码中找到他们
Dashboard
views/dashboard/admin/components/LineChart.vue
复制代码
咱们就以折线图为例,看看大佬是如何封装的,有没有可以借鉴的;这里就直接把人家的代码拷贝下来以加注释的形式理解吧
<template>
<div :class="className" :style="{height:height,width:width}" />
</template>
<script>
// 导入 echarts 和 theme
import echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
// resize 初步猜测是封装了 图表的 resize 相关事件
import resize from './mixins/resize'
export default {
mixins: [resize],
props: {
// 样式以及类名
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '350px'
},
// 这个还真的说不好具体功能,没有看到传进来,也没有看到用
autoResize: {
type: Boolean,
default: true
},
// 这个肯定是数据了
chartData: {
type: Object,
required: true
}
},
data() {
return {
chart: null
}
},
// 只要进来的图表数据有变化,就重新渲染
watch: {
chartData: {
deep: true,
handler(val) {
this.setOptions(val)
}
}
},
// mounted 钩子中创建图表
mounted() {
this.$nextTick(() => {
this.initChart()
})
},
// 实例即将销毁时清空/注销实例
beforeDestroy() {
if (!this.chart) {
return
}
// dispose 销毁方法 echarts
this.chart.dispose()
this.chart = null
},
methods: {
// 初始化实例,并且渲染
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.setOptions(this.chartData)
},
// 这个算是渲染方法了 接受主要数据
setOptions({ expectedData, actualData } = {}) {
this.chart.setOption({
xAxis: {
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
boundaryGap: false,
axisTick: {
show: false
}
},
grid: {
left: 10,
right: 10,
bottom: 20,
top: 30,
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
},
padding: [5, 10]
},
yAxis: {
axisTick: {
show: false
}
},
legend: {
data: ['expected', 'actual']
},
series: [{
name: 'expected', itemStyle: {
normal: {
color: '#FF005A',
lineStyle: {
color: '#FF005A',
width: 2
}
}
},
smooth: true,
type: 'line',
data: expectedData,
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: 'actual',
smooth: true,
type: 'line',
itemStyle: {
normal: {
color: '#3888fa',
lineStyle: {
color: '#3888fa',
width: 2
},
areaStyle: {
color: '#f3f8ff'
}
}
},
data: actualData,
animationDuration: 2800,
animationEasing: 'quadraticOut'
}]
})
}
}
}
</script>
复制代码
看完这个文件,emm... 一切都在预料之内,还是比较合理的,这个可以借鉴😝,下一步我猜测在
index.vue
中肯定有这个图表想要的数据,还有样式宽高的相关数据
贴出部分 index.vue
代码
views/dashboard/admin/index.vue
复制代码
<el-row style="background:#fff;padding:16px 16px 0;margin-bottom:32px;">
<line-chart :chart-data="lineChartData" />
</el-row>
import LineChart from './components/LineChart'
const lineChartData = {
newVisitis: {
expectedData: [100, 120, 161, 134, 105, 160, 165],
actualData: [120, 82, 91, 154, 162, 140, 145]
},
...
}
复制代码
果然传入了相关的数据,当然还有相关的样式我没有粘贴,大家可以
clone
下来看一下
重点来啦,我认为这部分 resize
封装的真不错;
建议复制到 IDE
中方便阅读,顺序先从方法 methods
看起,我会在注释中加上序号方便查看
views/dashboard/admin/components/mixins/resize.js
复制代码
import { debounce } from '@/utils' // 防抖函数
export default {
data() {
return {
$_sidebarElm: null,
$_resizeHandler: null
}
},
mounted() {
// 2,$_resizeHandler resize 事件,里面代码告诉我们chart如果存在就会调用resize 是为了保证图表显示正常
this.$_resizeHandler = debounce(() => {
if (this.chart) {
this.chart.resize()
}
}, 100)
// 3,这里调用 resize 窗口大小变化时就会resize保证图表显示正常
this.$_initResizeEvent()
// 8,经过层层查看原来这个是侧边栏展开或者缩放时触发 resize 保证图表正常渲染
this.$_initSidebarResizeEvent()
},
// 9,这个钩子下面的两个看方法名字就知道是移除上面的事件用的所以这两个方法就不细说了
beforeDestroy() {
this.$_destroyResizeEvent()
this.$_destroySidebarResizeEvent()
},
// 由于页面使用了 keep-alive 包裹肯定也要只要不刷新页面,第二次进入的时候就不会触发上面的钩子了所以这两个钩子内还要做重复的操作保证 resize 正常
activated() {
// 这里留个彩蛋 😎 看哥们我后面的发挥
this.$_initResizeEvent()
this.$_initSidebarResizeEvent()
},
deactivated() {
this.$_destroyResizeEvent()
this.$_destroySidebarResizeEvent()
},
methods: {
// 1,activated 和 mounted 都调用了
$_initResizeEvent() {
// $_resizeHandler 在 mounted 的第一行就赋值了
window.addEventListener('resize', this.$_resizeHandler)
},
$_destroyResizeEvent() {
window.removeEventListener('resize', this.$_resizeHandler)
},
// 7,这个函数 transitionend 事件触发函数
$_sidebarResizeHandler(e) {
// 为什么要 === width 在触发呢?留个问题下面解决
if (e.propertyName === 'width') {
this.$_resizeHandler()
}
},
// 4,activated 和 mounted 都调用了
$_initSidebarResizeEvent() {
// 5,$_sidebarElm 就是整个侧边栏 至此 data 中的两个数据我们都大概知道是干嘛的了
this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
// 6,如果这个元素存在就监听一个事件调用一个方法
// transitionend 这个是 vue 动画的事件在 transtion 结束后触发
this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
},
$_destroySidebarResizeEvent() {
this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
}
}
}
复制代码
关于上述代码第 6 步骤的解释 戳这里
问题一:第 7 步骤为什么 e.propertyName === 'width'
才会执行 resize
transitionend 戳这里
transitionend 是 CSStransition(如 transform/scale 等)结束的触发事件;看到了这个解释,可以猜想既然是 transitionend 的事件对象,那么会不会是通过 transition 移动了 width 属性呢?
那么咱们就来看看上面第 5 步骤时获取的类名是哪个元素都有什么 css 样式
果然被我们找到了,经过我的测试就是这个属性,大家也可以去试下记得不要改成
transition: all 0.28s;
他是监控你的走向你用all
实际还是 width 发生了改变;
src/styles/sidebar.scss 10行 复制代码
至此关于 dashboard 的图表基本已经看完了,个人认为其他的图表都差不多,作者用 mixin 来处理 resize 这段代码很值得学习;在 src/components/Charts
中也有一段类似的 mixin 对应页面中侧边栏的 Charts 相关页面; 大家可以自行查看,基本都是一样的;
高能预警
本来我也是看了这段代码之后,屁颠屁颠的就借鉴了一波用到自己的项目中是真的香呀😂;不过做完之后发现了问题;毕竟自己还是比较水的,第一反应一定是自己借鉴的不到位;直到我在 vue-element-admin 中复现了这个问题;
当时内心第一个想法,装13的机会来了😆我要提 PR 不要拦我;好那我就先解决这个 bug
经过查看他封装代码的逻辑来看
- 第一次进入页面时候默认是进入 dashboard 页面的所以不会出现这个问题;
mounted
没有问题 - 从其他 tab 进入时触发此问题,一定时
activated
钩子有问题
activated() {
this.$_initResizeEvent()
this.$_initSidebarResizeEvent()
}
复制代码
首先第二个方法是 侧边栏的可以保证和这个没有关系,那就剩下第一个方法了
$_initResizeEvent() {
window.addEventListener('resize', this.$_resizeHandler)
}
复制代码
1, 看我们的 gif 是 dashboard 页面小屏时切换其他页面,然后切换大屏在切换回来
2, 正常流程 dashboard 页面触发 activated
的时候触发窗口的 resize 事件再触发 echarts
的 resize 方法;
3, 而我们的操作步骤是在其他页面经历了 resize 之后再回到 dashboard 页面,dashboard 在activated
的时候没有经历 resize 所以造成了触发 deactivated
钩子的时候图表宽度/高度是多大,那么回来的时候还是多大不会发生变化;
ok !分析的胡说八道;那么就来想想这个问题怎么解决;
说白了就是差个触发 activated
的时候主动 resize echarts
的功能;咱们就来加一下;
methods
下新增 resize 方法
resize() {
const { chart } = this
chart && chart.resize()
}
复制代码
activated
中调用该方法
activated() {
this.resize()
this.$_initResizeEvent()
this.$_initSidebarResizeEvent()
}
复制代码
完美解决;开始提 PR 不会(戳这里)?bug 都改了,这个 13 不装不行了
最后,这种 bug 也算自己侥幸吧;其实这个项目 Charts下面的相关页面就没有这个bug;刚好我借鉴了 dashboard 相关的逻辑,刚好作者在这里忘记加了主动触发 resize 机制,刚好自己碰到了这样的需求;一切都是机缘巧合,其实作者还是很强的,心里也是由衷的佩服,毕竟一个人维护一个 6w+ Star 的项目,也给像我这样的渣渣们提供很好的轮子让我们二次开发;当然啦如果有幸被合并了肯定很好,没有被合并也是意料之中的,不过这对自己也是一种成长,激励自己写出更高质量的代码;