在 vue 项目使用 echarts 的场景中,如下三点不容忽视:1. 可视化的数据每每是异步加载的;2. 若一个页面存在大量的图表( 尤为当存在关系图和地图时 ),每每会致使该页面的渲染速度很慢并可能在几秒内卡死,产生极差的用户体验。3. 引入 echarts 组件致使编译后的文件过大从而使得首次访问的加载极慢。关于第三点,你们能够参考以前的撰文 优化 Vue 项目编译文件大小。如下针对上述前两点,给出数据异步、延迟渲染的 echarts vue 组件的设计和实现方式,并对实现之中可能存在的问题进行介绍。
组件代码能够访问 Github 查看。
首先,咱们须要把 echarts 使用中公共的部分抽离出来,造成基础组件。javascript
让咱们在 官网 - 5 分钟上手 ECharts 教程中找到使用 echarts 的步骤:css
# 1. 获取一个用于挂在 echarts 的 DOM 元素 let $echartsDOM = document.getElementById('echarts-dom') # 2. 初始化 let myEcharts = echarts.init($echartsDOM) # 3. 设置配置项 let option = {...} # 4. 为 echarts 指定配置 myEcharts.setOption(option)
注:在 Vue 中,首先咱们须要使用 import echarts from 'echarts'
以引入 echarts。html
由上可知,在 echarts 使用中,除第三步设置配置项之外,其余的步骤都是重复的,便可以抽离出来放入组件中统一实现。vue
注:其实 option 配置中也存在能够抽离的部分,好比咱们能够将 echarts 的颜色、散点大小、折线粗细等提取出来统一赋值,以保证 echarts 风格的统一。但因为不一样类型的 ehcarts 图的颜色配置方式不一样,于是实现起来相对繁琐,这里不进行说明,有兴趣的同窗能够自行尝试。java
首先咱们书写一个简单 i-ehcart.vue
,其中,配置项直接复制于官网的教程示例。git
<style scoped> .echarts { width: 100%; height: 100%; } </style> <template> <div> <div class="echarts" id="echarts-dom"></div> </div> </template> <script> import echarts from 'echarts' export default { name: 'echarts', data() { return {} }, mounted() { let $echartsDOM = document.getElementById('echarts-dom') let myEcharts = echarts.init($echartsDOM) let option = { title: { text: 'ECharts 入门示例' }, tooltip: {}, legend: { data: ['销量'] }, xAxis: { data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"] }, yAxis: {}, series: [{ name: '销量', type: 'bar', data: [5, 20, 36, 10, 10, 20] }] } myEcharts.setOption(option) } } </script>
而后在 App.vue
中引入这一组件,并设置 echarts 的宽高:github
<style> .echarts-container{ width: 100%; height: 20rem; } </style> <template> <div id="app"> <i-echart class="echarts-container"></i-echart> </div> </template> <script> import iEchart from './components/i-echart' export default { name: 'app', components: { iEchart } } </script>
刷新页面后,便可看到柱状图。编程
因为咱们须要抽离 option 部分,最好的方式是将其做为组件的属性,即 props
交由调用方配置:json
# i-echart.vue import echarts from 'echarts' export default { name: 'echarts', props: { option: { type: Object, default(){ return {} } } }, data() { return {} }, mounted() { let $echartsDOM = document.getElementById('echarts-dom') let myEcharts = echarts.init($echartsDOM) let option = this.option myEcharts.setOption(option) } }
而后咱们能够将 option 配置抽离到组件调用方,并经过「传参」的方式进行调用:segmentfault
<i-echart :option="option" class="echarts-container"></i-echart>
以前咱们注意到,在 option 参数中,咱们给出了默认值 {}
,即空对象。这样作实际上是有问题的,即在 echarts 中,若是传入的 option 配置对象不含有 series
键,就会抛出错误:
Error: Option should contains series.
默认值处理是须要存在的,即当调用方传入的对象为空或不存在 series
配置时,应在页面上显示一些提示( 对用户友好的提示,而不是对编程人员 ),即避免因报错而形成空白的状况。
此外,当咱们像以前那样给 option 这一参数进行类型限制后,假若调用方传入非对象类型,Vue 会直接抛出错误——这一结果也不是咱们想要的。咱们应该取消类型限制,并在 option 发生变化时进行依次如下判断:
1. 是否为对象; 2. 是否为空对象; 3. 是否包含 series 键; 4. series 是否为数组; 5. series 数组是否为空。
代码实现以下:
function isValidOption(option){ return isObject(option) && !isEmptyObject(option) && hasSeriesKey(option) && isSeriesArray(option) && !isSeriesEmpty(option) } function isObject(option) { return Object.prototype.isPrototypeOf(option) } function isEmptyObject(option){ return Object.keys(option).length === 0 } function hasSeriesKey(option){ return !!option['series'] } function isSeriesArray(option) { return Array.isArray(option['series']) } function isSeriesEmpty(option){ return option['series'].length === 0 }
注:实际上,当判断出 option 为对象后,能够直接进行第三步的判断。
而后,当判断 option 符合上述三种状况时,在页面上显示如「数据为空」之类的提示:
import echarts from 'echarts' export default { name: 'echarts', props: { option: { default(){ return {} } } }, data() { return { myEcharts: null, isOptionAbnormal: false } }, mounted() { let $echartsDOM = document.getElementById('echarts-dom') if(!$echartsDOM) return let myEcharts = echarts.init($echartsDOM) this.myEcharts = myEcharts this.checkAndSetOption() }, watch: { option(option){ this.checkAndSetOption() } }, methods: { checkAndSetOption(){ let option = this.option if(isValidOption(option)){ this.myEcharts.setOption(option) this.isOptionAbnormal = false }else{ this.isOptionAbnormal = true } } } }
这里在书写代码时,有如下几点须要注意:
document.getElementById()
的返回结果为空,不能直接使用 echarts.init()
,不然会抛出错误:Error: Initialize failed: invalid dom
;immediate: true
使得 watch
钩子可以在属性初始化赋值时被触发,但这样作是不合适的。由于这样设置以后,在 option 初始化从而触发 watch 时,用于挂载 echarts 的 DOM 元素还未存在于页面中,从而致使出现 TypeError: Cannot read property 'setOption' of null
的错误。咱们要重点注意 echarts 做用的生命周期,这一点后续还会涉及。 从上面的代码中能够注意到,咱们使用 isOptionAbnormal
标识了传入的 option
值是否符合规定。基于这一标识,咱们能够对 echarts 组件进行优化,当 option 不合法或数据为空时给出提示信息而不是显示空白甚至报错。
首先,咱们修改原组件 i-echart.vue
代码,增长 shadow
层:
<div> <div class="shadow" v-if="isOptionAbnormal"> 数据为空 </div> <div class="echarts" v-if="!isOptionAbnormal" id="echarts-dom"></div> </div>
并为其增长样式:
.shadow { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; font-size: 1rem; color: #8590a6; }
可当咱们把 option 修改成 null 后,展现的样式没有按照预期。「数据为空」的字样被挤到一旁。
经过审查元素,咱们猜想是因为 echarts 实例生成的 svg 并无由于 v-if 而消失( 或是 Vue 自己的处理机制 ),而是上移到了兄弟节点。
可见咱们须要在 echarts 的挂载元素之上再加一层容器 DOM:
<div> <div class="shadow" v-if="isOptionAbnormal"> 数据为空 </div> <div class="wrap-container"> <div class="echarts" v-if="!isOptionAbnormal" id="echarts-dom"></div> </div> </div>
同时对样式进行修改:
.wrap-container, .echarts { width: 100%; height: 100%; } .shadow { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; font-size: 1rem; color: #8590a6; }
这样一来,当 option 不合法时,提示文本确实会出如今合适的位置,但新的问题也出现了:当 option 值由不合法值变为合法值时,echarts 并无被渲染。
这是因为咱们在 option 检测的过程当中,只是进行了 setOption
,而因为咱们使用的 v-if
会在 option 不合法时直接删除 DOM 元素,使得 myEcharts
即 DOM 挂载对象消失,天然 setOption
也没有效果了。
这里有两个方案能够解决:
checkAndSetOption()
函数,使其可以在 option 改变检测时,对页面中是否存在挂载元素也进行检测,当不存在时,从新进行 echarts.init()
并赋值 myEcharts
。即考虑到 option 由「合法到合法」的改变,与「非法到合法」的改变是不一样的这一状况;v-if
改变为 v-show
,并将 echarts 挂载元素与提示信息框的布局改成 absolute。就两者而言,后者显然更易操做,也是咱们所采起的方法。
首先,咱们把 v-if 修改成 v-show,并为根元素添加类以用于调节样式:
<div class="main-container"> <div class="shadow" v-show="isOptionAbnormal"> 数据为空 </div> <div class="wrap-container" v-show="!isOptionAbnormal"> <div class="echarts" id="echarts-dom"></div> </div> </div>
而后进行样式调整:
.main-container{ position: relative; } .wrap-container, .shadow{ position: absolute; } .wrap-container, .echarts { width: 100%; height: 100%; } .shadow { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; font-size: 1rem; color: #8590a6; }
而后,咱们再将 option 由不合法到合法进行修改时,便不会出现没法渲染的状况了。
在实际场景中,用于渲染的数据经常是异步获取的,在异步加载数据之中,咱们可能须要在页面中显示如「正在加载...」的字样来表示加载过程正在进行以提升用户体验。而加载过程就组件而言是没法直接获取的,即须要组件调用方经过某种方式进行控制。
因此,咱们须要使用某一参数用于进行加载信息的显示。与以前不合法提示信息的操做方式相同,咱们使用绝对定位的元素和 isLoading
属性进行处理:
首先,咱们添加 isLoading 属性:
props: { option: { default() { return {} } }, isLoading: { type: Boolean, default: false } },
而后修改 HTML 代码:
<div class="main-container"> <div class="loading" v-show="isLoading"> 数据加载中... </div> <div class="shadow" v-show="!isLoading && isOptionAbnormal"> 数据为空 </div> <div class="wrap-container" v-show="!isLoading && !isOptionAbnormal"> <div class="echarts" id="echarts-dom"></div> </div> </div>
并修改样式:
.main-container{ position: relative; } .wrap-container, .loading, .shadow{ position: absolute; } .wrap-container, .echarts { width: 100%; height: 100%; } .shadow, .loading{ width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; font-size: 1rem; color: #8590a6; }
而后,咱们即可以在组件调用方中,使用 is-loading
来控制了:
<i-echart :option="option" :is-loading="true" class="echarts-container"></i-echart>
组件的最大用处是复用,但当咱们将以前写的组件进行复用时,会发现出现了问题:
<i-echart :option="option" class="echarts-container"></i-echart> <i-echart :option="option" class="echarts-container"></i-echart>
此时,咱们发现页面中并无出现两个 echarts 图,而是只有第一个。经过浏览器审查元素,咱们能够发现,只有第一个组件被正确地挂载了。这是为何呢?
这是由于 echarts 进行 init 挂载时使用的是 DOM 元素的 ID。而在组件中,咱们设置的 ID 是固定的( 注意与 scoped css 进行区分 )。即多个组件的 ID 是相同的,故而只有一个组件会被 echarts 挂载。
那么该如何解决这个问题呢?方法也很简单,只要保持每一个元素得到惟一的 ID 就能够了。而对于惟一 ID,咱们能够经过时间戳和随机数来实现。
修改组件代码,为组件挂载的 DOM 设置随机的 ID:
首先,咱们设置一个随机 ID:
data() { return { randomId: 'echarts-dom' + Date.now() + Math.random() } },
并将其 echarts 元素的 ID 修改成该值:
<div class="echarts" :id="randomId"></div>
而后将 mounted 生命周期中的 DOM 组件 ID 修改成咱们随机生成的值:
mounted() { let $echartsDOM = document.getElementById(this.randomId) ... }
此时,咱们才真正完成了基础组件的构建。
这里指的延迟加载,是 echarts 的渲染只在页面滚动到特定高度的时候才会进行。
因为 echarts 组件渲染须要性能( 尤为是地图、关系图 ),对于存在大量 echarts 的页面,若是在页面加载时所有进行渲染,可能会致使页面卡顿而下降用户体验。于是,咱们须要对 echarts 进行按需加载。
完成这一功能须要如下步骤:
下面咱们就逐步完成这些功能。在此以前,咱们须要添加一个高度足够的占位 DOM,以检测效果:
<div style="height: 50rem;"></div>
咱们可使用 window.onscroll = function(){}
来监听页面的滚动,但这种方式只能同时做用于一个组件。想要在全部组件中生效,咱们须要使用 window.addEventListener('scroll', function(){})
。注意,绑定的生命周期为 mounted
:
mounted: { window.addEventListener('scroll', () => { console.log(this.randomId) }) ... }
注意,这里使用了箭头函数以维持 this
的指向。
接下来,咱们要使用如下方法获取浏览器下边界的绝对位置,用以与以后 DOM 元素的上边界进行对比以判断当前是否应该进行渲染:
window.addEventListener('scroll', () => { let windowHeight = document.documentElement.clientHeight||window.innerHeight let scrollTop = document.documentElement.scrollTop || document.body.scrollTop let windowBottom = +scrollTop + +windowHeight console.log(windowBottom) })
接下来要获取组件的位置。在这以前,咱们要首先解决获取组件 DOM 元素的问题,这里有两种方式:
document.getElementById()
获取;$ref
获取。这里咱们使用第二种方式。
首先,咱们在组件上加入 ref 属性:
<div class="main-container" ref="selfEcharts"> ... </div>
而后,经过如下方式,获取组件自己:
this.$refs.selfEcharts
能够看到,与 ID 不一样,ref 是组件内惟一的( 而不是全局惟一 )。
以后,咱们经过如下方式获取组件的上边缘位置:
this.$refs.selfEcharts.offsetTop
注:这里也可使用 lodash
的 _.get()
来获取 offset
值,以免 Cannot read property of undefined
的错误:
_.get(this.$refs, 'selfEcharts.offsetTop', 0)
基于以上代码,咱们能够经过对比浏览器下边缘及组件的位置,从而控制 setOption 的时机,以达到延迟加载的效果。
咱们把以前的 this.checkAndSetOption()
放入高度判断中:
window.addEventListener('scroll', () => { ... if(windowBottom >= selfTop){ this.checkAndSetOption() } })
注:为了更明显地检测效果,咱们能够在 checkAndSetOption()
上加上 setTimeout
。
你们能够注意到,以上代码存在两个能够优化的部分:
这里咱们引入 lodash,并使用 throttle 来控制滚动监测的触发频率:
首先引入 lodash:
import _ from 'lodash'
而后限制触发间隔为 500 ms:
window.addEventListener('scroll', _.throttle(() => { let windowHeight = document.documentElement.clientHeight||window.innerHeight let scrollTop = document.documentElement.scrollTop || document.body.scrollTop let windowBottom = +scrollTop + +windowHeight let selfTop = _.get(this.$refs, 'selfEcharts.offsetTop', 0) if(windowBottom >= selfTop){ this.checkAndSetOption() } }, 500))
若想用 document.removeEventListener()
解绑事件,首先咱们要抽离事件自己,将匿名函数转为实名函数。
首先,咱们要将检测事件提取到 methods
之中:
methods: { checkAndSetOption() { let option = this.option if (isValidOption(option)) { this.myEcharts.setOption(option) this.isOptionAbnormal = false } else { this.isOptionAbnormal = true } } }
为了保证 addEventListener 和 removeEventListener 时操做的是同一个函数,这里咱们使用 data
添加实名函数:
data() { return { scrollEvent: _.throttle(this.checkPosition, 500) } }
而后在事件绑定中使用这一实名函数:
window.addEventListener('scroll', this.scrollEvent)
以后在检测到窗口滚动到合适高度的时候进行事件解绑:
checkPosition() { ... if (windowBottom >= selfTop) { this.checkAndSetOption() window.removeEventListener('scroll', this.scrollEvent) } },
当咱们回顾本身的代码,能够发现,在实际应用中,实际上是存在问题的。
因为用于渲染 echarts 的数据经常是异步获取的,也就是说,option 可能会在异步调用结束以后更新,从而触发 option 的 watch,进而致使 this.checkOption()
执行,最终使得 setOption 在页面没有滚动到合适位置时就触发了。
为了解决这个问题,咱们应该让 setOption
的过程受制于一个标识位,而该标识位会在页面滚动到合适位置时置为 true,从而杜绝因为 option 更新、触发 watch 而致使的漏洞。
首先,咱们要添加一个新的 data,取名为为 isPositionReady
:
data: { ... isPositionReady: false }
而后,在 checkAndSetOption()
中加入对该标识位的判断:
checkAndSetOption() { ... if(this.isPositionReady !== true) return ... }
最后,在位置检测方法 checkPosition()
中,当达到合适位置时,将该标识位置为 true:
checkPosition() { ... if (windowBottom >= selfTop) { this.isPositionReady = true ... } }
此时,以上漏洞就被修补了。
事实上,以上组件中还有一个漏洞,让咱们改变组件调用方的代码来发现它:
<div id="app"> <i-echart :option="option" class="echarts-container"></i-echart> <div style="height: 50rem;"></div> <i-echart :option="option" class="echarts-container"></i-echart> <i-echart :option="option" class="echarts-container"></i-echart> </div>
刷新页面,咱们发现本来应该渲染的第一个 echarts 组件并无展现出来。也就是说,经过咱们以前的代码,全部 echarts 组件的渲染都必须由页面滚动事件触发。
而对于那些本来就处于页面靠上位置的组件而言,理应在页面加载后就马上渲染而无需等待滚动。修补这个问题也很简单,只要在 mounted
生命周期中进行一次 checkPosition
检测便可:
mounted() { ... this.checkPosition() ... }
自此,一个具备延迟加载功能的 echarts 组件就完成了。接下来,咱们须要对该组件进行进一步优化,以适应更多的场景需求。
这里的重绘指的是 ehcarts 中的 resize()
方法。用于在某些时刻进行 echarts 的调整,包括:
echarts 并不会主动地随着浏览器宽度的改变而调整,须要咱们在页面改变时间中主动触发。实现的方式也很简单,只要按照以前的思路监听 window resize
事件便可。( 注意,这里一样要考虑控制监听频率的问题 ):
window.addEventListener('resize', _.throttle(() => { this.myEcharts.resize() console.log('---') }, 500))
对于一些场景,如含有侧边栏的页面而言,侧边栏收缩时,也须要对 echarts 进行 resize
调整。而此时,浏览器宽高一般是不会变化的。
于是咱们须要有一个机制,可以让组件调用方主动触发以使组件进行 resize
。因为当前版本的 Vue 是不能直接调用组件的方法的,想要作到这一点,咱们可使用如下两种方法:
采用时间戳或随机数赋值组件的属性,在组件调用方检测到侧边栏一类组件状态改变等须要 echarts 组件主动触发 resize
时,从新生成随机数或从新获取时间戳。而在组件中,对属性的变化进行检测,即当属性变化时,执行 resize
:
添加用于触发主动重绘的属性:
props: { resizeSignature: { default: '' } }
添加对该属性的监听,并在变化时执行 resize
:
resizeSignature(){ this.myEcharts.resize() }
此时,只要在调用方改变 resize-signature
便可使 echarts 主动调用 resize
。
在一些场景中,咱们可能须要对 echarts 的点击事件进行捕捉以进行下一步的处理( 如:数据下钻 )。
为了支持这一类场景,咱们须要为 echarts 添加点击监听事件,并将该事件及其参数上抛至组件调用方。
绑定 echarts 点击事件:
mounted () { ... let myEcharts = echarts.init($echartsDOM) myEcharts.on('click', params => { this.echartsClicked(params) }) ... }
向上抛出事件及其参数:
methods: { echartsClicked(params) { this.$emit('echarts-clicked', params) } }
在组件调用方捕捉该事件和参数:
<i-echart :option="option" @echarts-clicked="echartsClicked" class="echarts-container"></i-echart>
methods:{ echartsClicked(params){ console.log(params) } }
对于 echarts 中使用 stack
配置的堆叠图,在堆叠图来回转换中,可能出现样式错误的问题,这是因为使用 setOption(option)
时只会更新相较以前 option 不一样的部分。解决方法是:
echarts.setOption(option) // 修改成: echarts.setOption(option, true)
详情可参考:Github Issue:请问一个柱状图叠加数据刷新问题。
在 echarts 中,对地图的使用仍是比较频繁的。使用地图时,使用地图的 Json 数据进行注册时比较合适的方式。为此,组件中提供了 maps
属性,用于地图数据的注册,如:
<i-echart :option="option" :maps="maps"></i-echart> <script> ... // 'echarts/map/json/china.json' let maps = [ { name: 'china', data: chinaJson }, ... ] ... </script>
在 Vue 中,v-show
使用 display
控制组件的显隐。而当 echart init 的时候,若是其挂载 DOM 的 v-show 处于 false 状态,则其 init 的对象宽高都是 0。即便以后 v-show 状态改变,因为 mounted
生命周期不会再次触发,从而使得 echarts 显示不正常。
为此,咱们须要将 v-show 修改成对 visibility
这一 CSS 的改变:
:style="{visibility: isChartVisible ? 'visible' : 'hidden'}" ... computed: { isChartVisible(){ return !this.isLoading && !this.isOptionAbnormal } }
当咱们经过对某一组件设置 overflow 使得页面总体高度小于等于屏幕高度时,对 window 绑定的滚动事件就失效了:
<div id="#app" style="width:100%; height:100%; overflow:auto"> <div id="scroll" style="width:100%; height:100rem;"></div> </div>
如上,此时 window 及其至 div#app 的子元素都是不会发生 scroll 事件的。若是咱们想要监听滚动事件,只能将其绑定在 div#scroll 元素上:
document.querySelector('#scroll').addEventListener('scroll', function(){})
这也就意味着,对于这种场景,若是在 #scroll 中放置了许多咱们以前完成的 vue-echarts 组件,因为没法正常监听滚动事件,那些不在首屏显现的图表以后也不能正常显示。
为了解决这一问题,咱们须要为组件增长一个参数,使得咱们能够传入可以被监听滚动事件的元素 ID,以便延迟加载效果正常起效:
/** * 用于绑定滚动监听的 DOM 元素的 ID 值,不传递时会使用 window */ scrollDomId: { default: null }
而后咱们须要改动三个地方:
首先,咱们须要获取应该被监听滚动事件的元素:
computed: { /** * 获取可滚动的 DOM 元素 * @returns {Window} */ onScrollDOM () { let scrollDom = window if (this.scrollDomId !== null) { let tempDom = document.querySelector('#' + this.scrollDomId) if (tempDom !== null) { scrollDom = tempDom } } return scrollDom }, ... }
修改滚动监听的绑定:
/** * 对滚动事件进行监控 */ this.onScrollDOM.addEventListener('scroll', this.scrollEvent)
修改位置检测中 scrollTop
值的获取逻辑:
checkPosition () { ... let scrollTop = this.onScrollDOM.scrollTop || document.documentElement.scrollTop || document.body.scrollTop ... },