與上週的第一篇實踐教程同樣,在這篇文章中,我將繼續從一種常見的功能——表格入手,展現Vue.js中的一些優雅特性。同時也將對filter功能與computed屬性進行對比,說明各自的適用場景,也爲vue2.0版本中即將刪除的部分filter功能作準備。php
仍是先從需求入手,想一想實現這樣一個功能須要注意什麼、大體流程如何、有哪些應用場景。css
表格自己是一種很是經常使用的組件,用於展現一些複雜的數據時表現很好。前端
當數據比較多時,咱們須要提供一些篩選條件,讓用戶更快列出他們關注的數據。vue
除了預設的一些篩選條件,可能還須要一些個性化的輸入搜索功能。git
對於有明顯順序關係的數據,例如排名、價格等,還須要排序功能方便快速倒置數據。github
若是數據量較大,須要分頁展現表格。vue-router
須要注意的是,上述的這些需求其實和大部分數據庫提供的功能是很是一致的,並且因爲數據庫擁有索引等優化方式以及服務器更好的性能,更加適合處理這些需求。不過如今流行的先後端分離,也是但願讓客戶端在合理的範圍內,更多的分擔服務器端的壓力,因此當找到一個平衡時,在前端處理適量的需求是正確的選擇。vuex
接下來就嘗試用vue完成這些需求吧。數據庫
由於這樣一個多功能表格可能會應用在多個項目中,因此設計思路上儘可能將表格相關的內容放在Table.vue組件中,減小耦合,方便複用。segmentfault
爲了更好的對比前端實現以上需求的利與弊,咱們須要一份較大較複雜的測試數據。幸運的是我以前的一個項目中,設計的一份API正好知足這一需求,數據爲魔獸世界競技場的天梯排行API,目前這個API處於開放狀態,接口詳見Myarena介紹。
與上一篇教程相相似,仍是新建一個api文件夾以及一個arena.js用於管理API接口。再在App.vue中引入arena.js,在created階段獲取數據。做爲一個demo,咱們只獲取region爲CN、laddar爲3v3的數據,不過只要將兩個參數經過v-model綁定給對應的表單控件,就能很輕鬆的實現不一樣地區數據的切換。
如以前所說,思路上咱們但願減小table組件與外部環境的耦合,因此咱們給Table.vue設置一個props屬性rows,用於獲取App.vue取回的數據。
在App.vue中註冊table組建時要注意,命名不能用默認的table,因此註冊爲vTable,就能用<v-table>標籤引入table組件了。
目前爲止,咱們的App.vue完成了它全部的功能,代碼以下:
<template> <div class="container"> <v-table :rows="rows"></v-table> </div> </template> <script> import arena from './api/arena' import vTable from './components/Table' export default { components: { vTable }, data () { return { region: 'CN', laddar: '3v3', rows: [] } }, methods: { getLaddar (region, laddar) { arena.getLaddar(region, laddar, (err, val) => { if (!err) { this.rows = val.rows } }) } }, created () { this.getLaddar(this.region, this.laddar) } } </script>
實際的App.vue中還有一個獲取API中的最後更新時間的操做,以及一些css設置,篇幅考慮這裏進行了省略,對完整代碼有興趣的能夠移步文章末尾的Github倉庫。
Table.vue的template中主要爲3部分,分別是用於搜索、篩選和分頁的表單控件、用於排序表格的表頭thead以及用於展現數據的tbody。
首先來完成tbody的部分,基本思路就是用v-for遍歷數據,再經過模板填入,須要注意如下幾個重點:
返回的數據不必定徹底符合要求。例如我但願實現經過勝率排序,但數據中只包含了勝負場數,須要先計算一次。2. 數據中用於表現玩家職業的數據爲classId這個屬性,但在實際項目中我想要用各職業的icon展現職業,因此我在utils.js中實現了各一個classIdToIcon的工具函數,用於映射classId至sprite圖中的background-position。
以上兩點說明咱們最好不要遍歷props得到的rows這一原始數據。所以另建了一個computed屬性players,並在其中完成了前期處理,我把全部的前期處理放在了handleBefore中。
因爲即將使用的各類filters操做比較複雜,因此在handlebefore中進行了console.log('before handle'),方便咱們驗證handlebefore在什麼階段被執行了。
完成佈局以後,目前Table.vue中的重點代碼以下:
<template> <tbody> <tr v-for="player of players :class="player.factionId? 'horde':'alliance'"> <th>{{ player.ranking }}</th> <th>{{ player.rating }}</th> <th> <span class="class" :style="{ backgroundImage: 'url(http://7xs8rx.com1.z0.glb.clouddn.com/class.png)', backgroundPosition: player.classIcon }"></span> {{ player.name }} </th> <th>{{ player.realmName }}</th> <th> <bar :win="player.weeklyWins" :loss="player.weeklyLosses"></bar> </th> <th> <bar :win="player.seasonWins" :loss="player.seasonLosses"></bar> </th> </tr> </tbody> </template> <script> import Bar from './Bar' import { classIdToIcon } from '../assets/utils' export default { components: { Bar }, props: { rows: { type: Array, default: () => { return [] } } }, computed: { players () { this.rows = this.handleBefore(this.rows) return this.rows } }, methods: { handleBefore (arr) { console.log('before handle') if (this.rows[0]) { arr.forEach((item) => { if (item.weeklyWins === 0 && item.weeklyLosses === 0) { item.weeklyRate = -1 } else { item.weeklyRate = item.weeklyWins / (item.weeklyWins + item.weeklyLosses) } if (item.seasonWins === 0 && item.seasonLosses === 0) { item.seasonRate = -1 } else { item.seasonRate = item.seasonWins / (item.seasonWins + item.seasonLosses) } item.classIcon = classIdToIcon(item.classId) }) } return arr } } } </script>
能夠看到,我還引入了一個Bar.vue組件用於展現勝率,這是由於我但願最終的實際效果是這樣的:
一開始我直接在勝率所在的<th>標籤中進行各類操做,但可想而知在進行一些邊界狀況的判斷時,會出現各類含有player.weeklyWins, player.weeklyLosses等長命名變量的三元表達式。
原本是出於便利考慮,卻反而致使代碼難以維護。所以新建了個一個bar組件,將勝負傳入組件中,在bar組件內部用更語義化的方式實現,Bar.vue中模板部分代碼以下:
<template> <div class="clear-fix"> <span v-if="!hasGame || win / total > 0" :style="{ width: 100 * win / total + '%' }" :class="hasGame? '':'no-game'" class="win-bar"> {{ hasGame? (100 * win / total).toFixed(1) + '%':'無場次' }} </span> <span v-if="loss / total > 0" :style="{ width: 100 * loss / total + '%' }" class="loss-bar"> {{ win === 0? '0%':'' }} </span> </div> </template>
更好理解和維護了,不是嗎?
在使用vue的過程當中,須要注意的是框架中許多方法其實在內部最終是異曲同工。
例如咱們能夠直接在元素中執行一些對數據的操做,例如@click="show = !show",一樣的咱們也能夠對事件綁定方法,再在方法中操做數據,例如@click="toggle", toggle () { this.show = !this.show }。還好比咱們能夠用computed屬性和watch屬性實現不少相同的功能,接下來還將用computed去實現和filters相同的功能。
vue設計中的靈活性讓咱們有了更多的可能性,但在學習時,應該以搞明白不一樣方式在不一樣場景中的優劣爲目標,實際運用時選擇最好的那一種。
在例子中,players實際是一個5000條數據的數組,在不作任何處理時,將直接渲染出5000個<tr>,因此先趕忙過濾吧!
對於v-for循環,vue中提供了3中filters過濾數組,分別爲filterBy, orderBy, limitBy,其功能對應了搜索/篩選、排序和分頁,實現分別是使用了Array.filter, Array.sort(), Array.slice()。
這三種filters在使用時很是便利,只要在v-for後用|分離再添加對應的filters便可,這3中filter的具體參數能夠查看官方API,這裏很少作贅述。
須要注意的是,實際的過程是先將被遍歷的數組(例子中的players)依次經過過濾器,再將最後一個過濾器返回的數組進行v-for操做。
所以,filters放置的順序是須要根據需求來調整的,也由於每種過濾器的內部實現效率不一樣,因此在需求優先級不明顯時,應該以效率爲優先。
注意:實際測試時,發現不論怎麼過濾數組,handleBefore方法都沒有再次執行,也就是說players數組並無被改動過。
例如在個人例子中,我但願能夠篩選出名字或者服務器包含了我所輸入內容的玩家,而且將他們按照某種方式排序,最後的結果每頁只顯示20條。
那麼顯然剪切數組永遠應該放在最後一步,而排序和過濾在需求中沒有明顯的優先級。可是大部分狀況下,sort的效率都要低於filter,因此咱們先進行filter,減小數組長度,再sort。
有了這一思路以後,用於v-for的<tr>變爲:
<tr v-for="player of players | filterBy query in 'name' 'realmName' | orderBy sort.key sort.val | limitBy 20 (page-1)*20" :class="player.factionId? 'horde':'alliance'">
這裏直接將各個變量動態化,再經過Table.vue中的input綁定v-model以及表頭thead綁定@click事件來改變篩選的條件,就已經實現了大部分的搜索、過濾、分頁功能。
表頭改變sort排序我是經過如下代碼實現的,方式可能不是太好,特此列出:
<thead> <tr> <th @click="sort = {key: 'ranking', val: -sort.val}">排名</th> <th @click="sort = {key: 'rating', val: -sort.val}">分數</th> <th>資料</th> <th>服務器</th> <th @click="sort = {key: 'weeklyRate', val: -sort.val}">本週戰績</th> <th @click="sort = {key: 'seasonRate', val: -sort.val}">賽季戰績</th> </tr> </thead>
能夠看到,經過vue的filters功能,已經能夠輕鬆完成咱們的大部分功能,代碼量極少。這也是vue2.0前瞻發佈以後,提出廢棄部分filters功能後許多人反應較爲強烈的緣由。
可是如同做者在改動說明中所說,filters對於初學者來講不易理解,而且filters的功能均可以用computed屬性進行更靈活、更好把控的實現。並且在一些複雜條件下,堆疊過濾器會形成一些額外的複雜性以及不方便之處。
那麼何爲複雜條件呢?例如我增添兩個需求,一是按職業篩選玩家,而是篩選出必定分數以上的玩家,那麼後者用filterBy就不太好實現了。
咱們須要將對分數段的過濾放在filters以前進行,但又要注意不破壞players數組自己。在實際完成時,會發現這個過程仍是比較糾結的。
除此以外,咱們還會發現分頁中最重要的一個信息——總頁數咱們獲取不到。由於vue並無把一串過濾管道中產出的最終用於v-for的數組暴露出來,因此咱們沒法得到這個實際被循環的數組的長度。
在實際hack這些需求時,發現很容易與filters的執行順序發生衝突,所以決定從新用computed屬性來實現一遍全部功能,不借助自帶的filters。
固然,在這一段的前半部分中,咱們顯而易見的感覺到了來自filters的便利性。若是需求中filters能夠知足,那麼在1.x版本中使用filters仍是十分明智的!
在Github倉庫中,我用Table.vue.bak文件儲存了以前一段中用filters實現的代碼,方便與咱們接下里的實現進行比較。
首先整理一下用computed屬性來實現的思路:
首先要實現filterBy, orderBy, limitBy這三個filter的功能,上文中已經提到了他們的內部實現,因此分別用Array.filter, Array.sort和Array.slice重寫一遍並不複雜。
說是computed屬性實現,其實也仍是隻有players這個computed屬性,只是在其內部執行了全部的過濾動做,咱們實際是把各類過濾器的邏輯放置在各個method中。
不建議把各個過濾method寫的過於抽象,由於就是內置filters高度抽象致使一些特殊需求沒法實現,因此不妨就以最針對性的方式:一個method對應一種過濾。
在執行各個過濾method時,依然有最初提到的順序帶來的效率問題。由於vue牽一髮而動全身的特性,任何一個過濾條件改變時,全部過濾method都會執行一遍,因此儘快用高效的過濾器縮短數組長度顯得更爲重要。
我嘗試過經過watch屬性實現最小化method調用,但無奈功力不夠沒能實現。同時我也認爲前端處理大量數據的狀況不多見,而且用第4點中的數據進行優化後,執行效率不算過低,因此不必在這個方面作過多糾結。真有性能瓶頸時,從服務器端尋求解決會更簡單。
注意:在實現各類過濾method時,建議閱讀vue中filterBy, orderBy, limitBy三部分的實現源碼,其自己對於數組的操做就有一些優化,很是值得學習。在一些特殊狀況中,例如數組中大量相等值時,過於簡單的sort function會致使執行步數激增,vue中的一些處理都予以了避免。
根據需求目標,我設置瞭如下這些method(順序即爲執行順序):
classFilter:過濾玩家職業,經過item.classId === this.class進行判斷,this.class綁定的是一個select控件。
queryFilter:匹配玩家姓名中的字段,經過item.name.indexOf(this.query)判斷,this.query則綁定一個input控件。
ratingFilter:篩選玩家分數段,經過item.rating >= this.rating進行判斷,this.rating綁定了一個類型爲range的input控件,range的範圍則是用computed屬性進行計算。
sortTable:由於Array.sort進行的步數較多,因此放在數組被上述3個method處理的較短後進行。
paginate:全部過濾操做完畢以後,就能夠進行分頁了。在使用Array.slice()以前,先將數組的長度傳給this.total儲存起來,用於在分頁後計算總的頁數。
除了以上幾個過濾method之外,固然也還有handleBefore方法對數組進行前期處理。可是因爲players每次都會從新計算,因此爲了放止handleBefore被重複執行,應該加上必定的判斷條件,例如handleBefore添加的屬性是否已經存在了等等。
同時,還能夠把一些不須要在過濾以前執行的動做從handleBefore中拿出,例如例子中的classId轉換爲Icon,能夠在過濾以後對最終要展現的數據進行便可,減小一些步數。因此又設置了一個handleAfter方法,用於在分頁完成以後進行後續操做,固然在handleAfter中也可能重複執行,因此若是執行的操做消耗很大,建議一樣添加判斷,避免重複執行。
在例子代碼中,我在每一個方法中都統計了執行的步數,實際結果顯示設置一個合理的過濾順序能夠避免一些性能問題,結果以下:
能夠看出初始化時,在沒有任何過濾的狀況下,sort的步數較高。而一旦添加了一些過濾條件以後,順位靠後的filter和sort的步數都會大幅度減小。
因爲工做比較忙,暫時沒有打算將開頭中展現的MyArena項目重構,不過能夠想象那會是一個很好的用vue製做單頁應用的示例,後續的教程中可能會用來作例子。
本次教程中的例子,專一於展現多功能表格自己
上週是Vue.js開發實踐的第一篇文章,也是我第一次在SF社區的我的專欄裏發表文章,但願可以把平時遇到的一些問題和解決的思路分享給你們,本身也進行一個梳理。
開發實踐這個系列會用一些小例子,展現一些思路,實現一些有用、可複用的常見功能。計劃中,還會有Vue.js實戰系列和Sails.js實戰系列兩個系列的文章。
前者從較完整的項目出發,分析技術選型、vue-router和vuex的使用、多端共用代碼、後期維護等方面的一些考量。後者則是用Sails.js這個框架構建企業級Node.js後端的一些嘗試和心得,包括框架的優缺點、橫向對比以及細節摸索等等。
目前也在關注阿里的開源項目Weex的內測進展,理想中的狀態是用Weex實現項目在移動端App的開發,真正完成JS全棧,不過Weex還沒正式開源,有待觀望,因此只是後期設想,暫時不在計劃內。
文章目前就只發在SF的專欄裏,因此有意見建議都請在文章底部留言。同時因爲以上所說的全部工做都由我一我的在負責,因此文章的更新可能時快時慢,爭取作到一週一篇。