這幾天公司里邊有一個(gè)項(xiàng)目曲掰,叫做日控制臺(tái)蛔琅,該項(xiàng)目是在webview下的一個(gè)webapp胎许,使用vue構(gòu)建,項(xiàng)目中要求使用許多自定義的圖表罗售」家ぃ考察了許多圖表組件之后,發(fā)現(xiàn)echarts是所有表庫(kù)中寨躁,最靈活穆碎,特效最好看的一種。
一职恳、構(gòu)建基礎(chǔ)公共組件
1. 實(shí)現(xiàn)基礎(chǔ)功能
在echart官網(wǎng)上搜索到所禀,如何使用
# 1. 獲取一個(gè)用于掛在 echarts 的 DOM 元素
let $echartsDOM = document.getElementById('echarts-dom')
# 2. 初始化
let myEcharts = echarts.init($echartsDOM)
# 3. 設(shè)置配置項(xiàng)
let option = {...}
# 4. 為 echarts 指定配置
myEcharts.setOption(option)
使用echart的步驟也就這幾部,就是先獲取到承載echart實(shí)例的dom實(shí)例话肖,然后調(diào)用init()方法初始化圖標(biāo)實(shí)例北秽,最后調(diào)用setOption()方法傳入配置項(xiàng)
這幾步都要在vue的mounted方法下實(shí)現(xiàn).
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)
}
}
注:在 Vue 中,首先我們需要使用 import echarts from 'echarts' 以引入 echarts最筒。
二、組件化
思路很簡(jiǎn)單蔚叨,就是將業(yè)務(wù)上用到的圖表床蜘,比如柱狀圖、折線圖蔑水,通通封裝成組件邢锯,然后再main app文件中調(diào)用,通過(guò)分析搀别,可以通過(guò)傳props丹擎,來(lái)改變setOption()方法中的對(duì)象,達(dá)到封裝不同圖表組件的目的歇父。
<ocEcharts class="echarts-container" :options="ocoptions" />
將之前的options轉(zhuǎn)移到main app中的data對(duì)象之中蒂培。
注:echarts圖表要求承載的容器具有固定的寬高才能正常顯示
.echarts-container{
width: 100%;
height: 20rem;
}
1. 組件優(yōu)化-props的series校驗(yàn)
如果在傳入組件的props中傳入了空對(duì)象,就會(huì)發(fā)現(xiàn)榜苫,圖表會(huì)拋出一個(gè)錯(cuò)誤护戳,即
Error: Option should contains series.
原因就是傳入的 option 配置對(duì)象不含有 series 鍵,所以垂睬,默認(rèn)值處理是需要存在的媳荒,即當(dāng)調(diào)用方傳入的對(duì)象為空或不存在 series 配置時(shí)抗悍,應(yīng)在頁(yè)面上顯示一些提示( 對(duì)用戶友好的提示,而不是對(duì)編程人員 )钳枕,即避免因報(bào)錯(cuò)而造成空白的情況缴渊。
此外,當(dāng)我們像之前那樣給 option 這一參數(shù)進(jìn)行類型限制后鱼炒,倘若調(diào)用方傳入非對(duì)象類型衔沼,Vue 會(huì)直接拋出錯(cuò)誤——這一結(jié)果也不是我們想要的。我們應(yīng)該取消類型限制田柔,并在 option 發(fā)生變化時(shí)進(jìn)行依次以下判斷:
// 1. 是否為對(duì)象俐巴;
export function isObject(option) {
return Object.prototype.isPrototypeOf(option)
}
// 2. 是否為空對(duì)象;
export function isEmptyObject(option) {
return Object.keys(option).length === 0
}
// 3. 是否包含 series 鍵硬爆;
export function hasSeriesKey(option) {
return !!option['series']
}
// 4. series 是否為數(shù)組欣舵;
export function isSeriesArray(option) {
return Array.isArray(option['series'])
}
// 5. series 數(shù)組是否為空。
export function isSeriesEmpty(option) {
return option['series'].length === 0
}
export function isValidOption(option) {
return isObject(option) && !isEmptyObject(option)
&& hasSeriesKey(option)
&& isSeriesArray(option) && !isSeriesEmpty(option)
}
然后在組件中引入最后的isValidOption方法作為判斷缀磕,我們先使用一個(gè)watch監(jiān)聽options的變化
watch: {
options(options){
this.checkAndSetOption()
}
},
methods: {
checkAndSetOption(){
let options = this.options
if(isValidOption(options)){
this.myEcharts.setOption(options)
this.isOptionAbnormal = false
}else{
this.isOptionAbnormal = true
}
}
}
這里的checkAndSetOption方法是判斷傳入的option是否合法缘圈,如果合法,就執(zhí)行setOption袜蚕,isOptionAbnormal變量是監(jiān)控頁(yè)面是否顯示options非法的flag糟把。
同樣的dom樣式上的改變,也要使用v-show來(lái)判斷isOptionAbnormal是否要渲染圖表和渲染錯(cuò)誤信息牲剃。
<div>
<div class="shadow" v-show="isOptionAbnormal">數(shù)據(jù)為空</div>
<div class="oc_echarts_container">
<div class="echarts" id="echarts-dom" v-show="!isOptionAbnormal" />
</div>
</div>
.oc_echarts_container, .echarts {
width: 100%;
height: 100%;
}
.shadow {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-size: 1rem;
color: #8590a6;
}
2. 增強(qiáng)組件功能 - 數(shù)據(jù)加載提示
<div class="loading" v-show="isLoading">
數(shù)據(jù)加載中...
</div>
在組件內(nèi)部需要一個(gè)外部的props遣疯,isLoading,但是這里需要注意凿傅,在 Vue 中缠犀,v-show 使用 display 控制組件的顯隱。而當(dāng) echart init 的時(shí)候聪舒,如果其掛載 DOM 的 v-show 處于 false 狀態(tài)辨液,則其 init 的對(duì)象寬高都是 0。即使之后 v-show 狀態(tài)改變箱残,由于 mounted 生命周期不會(huì)再次觸發(fā)滔迈,從而使得 echarts 顯示不正常。所以需要使用css的visibility來(lái)控制顯隱被辑。
computed: {
isChartVisible() {
return !this.isLoading && !this.isOptionAbnormal
}
},
<div class="oc_echarts_container">
<div class="echarts" id="echarts-dom" :style="{visibility: isChartVisible ? 'visible' : 'hidden'}" />
</div>
3. 組件復(fù)用-隨機(jī)ID
echarts 進(jìn)行 init 掛載時(shí)使用的是 DOM 元素的 ID燎悍。而在組件中,我們?cè)O(shè)置的 ID 是固定的( 注意與 scoped css 進(jìn)行區(qū)分 )敷待。如果多個(gè)組件的 ID 是相同的间涵,只有一個(gè)組件會(huì)被 echarts 掛載。
所以我們要設(shè)定一個(gè)隨機(jī)的randomId,賦值到承載echarts圖表的dom元素的id中
<div class="oc_echarts_container">
<div class="echarts" :id="randomId" :style="{visibility: isChartVisible ? 'visible' : 'hidden'}" />
</div>
三榜揖、延遲加載
延遲加載是組件的一個(gè)優(yōu)化勾哩,在業(yè)務(wù)的開發(fā)中可以看到抗蠢,一個(gè)頁(yè)面往往有著許多的圖表,圖標(biāo)伴隨著許多異步請(qǐng)求和canvas渲染思劳,如果一次性渲染所有的圖表會(huì)導(dǎo)致許多的性能問(wèn)題迅矛。這里想到的一個(gè)解決方案就是延遲加載。
用通俗的話講就是潜叛,頁(yè)面滾動(dòng)到哪張圖表就去渲染哪一張圖表秽褒。
完成這一功能需要以下步驟:
- 監(jiān)聽頁(yè)面滾動(dòng)事件;
- 滾動(dòng)事件中獲取 echarts 的位置威兜;
- 在頁(yè)面當(dāng)前位置達(dá)到 echarts 位置的時(shí)候進(jìn)行 echarts 的初始化销斟。
1. 監(jiān)聽頁(yè)面滾動(dòng)
如果要監(jiān)聽頁(yè)面滾動(dòng),需要用到dom的監(jiān)聽器椒舵,addEventListener('scroll', [funciton])蚂踊。這樣就能為每一個(gè)組件附上監(jiān)聽事件。
2. 獲取當(dāng)前滾動(dòng)下邊界和組件的上邊界
這一個(gè)步驟需要封裝成一個(gè)函數(shù)笔宿,checkPosition()
checkPosition() {
let windowHeight = document.documentElement.clientHeight||window.innerHeight
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
let windowBottom = scrollTop + windowHeight
const selfTop = _.get(this.$refs, 'selfEcharts.offsetTop', 0);
if(windowBottom >= selfTop) {
this.isPositionReady = true
this.checkAndSetOption()
window.removeEventListener('scroll', this.scrollEvent)
}
},
- windowBottom(滑動(dòng)的下邊界) = scrollTop(屏幕當(dāng)前滑動(dòng)的距離) + windowHeight(窗口的高度)
- selfTop(當(dāng)前組件的頂部位置)
當(dāng)?shù)谝粋€(gè)變量大于第二個(gè)變量時(shí)犁钟,就認(rèn)為滑動(dòng)到了該圖表組件,就開開啟加載
3. 初始化
data() {
return {
myEcharts: null,
isOptionAbnormal: false,
randomId: 'echarts-dom' + Date.now() + Math.random(),
scrollEvent: _.throttle(this.checkPosition, 500), // 滑動(dòng)事件
isPositionReady: false, //控制數(shù)據(jù)異步與頁(yè)面滾動(dòng)先后順序的flag
}
},
mounted() {
let $echartsDOM = document.getElementById(this.randomId)
if(!$echartsDOM) return
this.myEcharts = echarts.init($echartsDOM)
// 第一次未滑動(dòng)的時(shí)候
this.checkPosition()
//滑動(dòng)之后的監(jiān)聽
window.addEventListener('scroll', this.scrollEvent)
},
checkPosition方法不僅要在scroll監(jiān)聽事件中調(diào)用泼橘,在組件第一次渲染的時(shí)候也要調(diào)用一次進(jìn)行初始化涝动,不然,組件無(wú)法正常渲染圖表炬灭。
4.節(jié)流
組件代碼經(jīng)過(guò)測(cè)試之后發(fā)現(xiàn)醋粟,如果滾動(dòng)過(guò)于快速,會(huì)不停的調(diào)用checkPosition(),關(guān)鍵是這個(gè)方法會(huì)不停的去setOption()重归,所以以上代碼均采用了lodash的throttle節(jié)流方法昔穴,在500毫秒內(nèi)只允許調(diào)用一次。
5. 解綁監(jiān)聽事件
解綁事件提前,組件中有設(shè)定監(jiān)聽器,如果該圖表已經(jīng)被加載了泳唠,那么這個(gè)監(jiān)聽器就沒(méi)有作用了狈网。
window.removeEventListener('scroll', this.scrollEvent)
這段代碼就是解綁監(jiān)聽器,多多少少會(huì)優(yōu)化一下速度吧笨腥。因?yàn)樵黾颖O(jiān)聽和解綁監(jiān)聽的時(shí)間函數(shù)要求一致拓哺,所以在data中新增了scrollEvent,順便把節(jié)流函數(shù)一起加了上去脖母。
6. 請(qǐng)求異步控制setOption
由于用于渲染 echarts 的數(shù)據(jù)常常是異步獲取的士鸥,也就是說(shuō),option 可能會(huì)在異步調(diào)用結(jié)束之后更新谆级,從而觸發(fā) option 的 watch烤礁,進(jìn)而導(dǎo)致 this.checkOption() 執(zhí)行讼积,最終使得 setOption 在頁(yè)面沒(méi)有滾動(dòng)到合適位置時(shí)就觸發(fā)了。
為了解決這個(gè)問(wèn)題脚仔,我們應(yīng)該讓 setOption 的過(guò)程受制于一個(gè)標(biāo)識(shí)位勤众,而該標(biāo)識(shí)位會(huì)在頁(yè)面滾動(dòng)到合適位置時(shí)置為 true,從而杜絕由于 option 更新鲤脏、觸發(fā) watch 而導(dǎo)致的漏洞们颜。
首先,我們要添加一個(gè)新的 data猎醇,取名為為 isPositionReady窥突,然后,在 checkAndSetOption() 中加入對(duì)該標(biāo)識(shí)位的判斷:最后硫嘶,在位置檢測(cè)方法 checkPosition() 中阻问,當(dāng)達(dá)到合適位置時(shí),將該標(biāo)識(shí)位置為 true
checkPosition() {
// ....
if(windowBottom >= selfTop) {
this.isPositionReady = true
// ....
}
},
checkAndSetOption() {
// ....
if(!this.isPositionReady) return
//....
}
四音半、echarts重繪
這里的重繪指的是 ehcarts 中的 resize() 方法则拷。用于在某些時(shí)刻進(jìn)行 echarts 的調(diào)整,包括:
- 組件寬度設(shè)置為百分比曹鸠,瀏覽器寬度發(fā)生變化時(shí)煌茬;
- 頁(yè)面收縮元素狀態(tài)改變,如側(cè)邊欄收縮導(dǎo)致內(nèi)容區(qū)寬度變化彻桃;
1.頁(yè)面寬度改變
繼續(xù)增加監(jiān)聽器就能完成
window.addEventListener('resize', _.throttle(() => {
this.myEcharts.resize()
console.log('---')
}, 500))