在 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 ... },