vue.js實現表格排序篩選功能介紹
需求分析:
還是先從需求入手,想想實現這樣一個功能需要注意什麼、大致流程如何、有哪些應用場景。
(1).表格本身是一種非常常用的元件,用於展示一些複雜的資料時表現很好。
(2).當資料比較多時,我們需要提供一些篩選條件,讓使用者更快列出他們關注的資料。
(3).除了預設的一些篩選條件,可能還需要一些個性化的輸入搜尋功能。
(4).對於有明顯順序關係的資料,例如排名、價格等,還需要排序功能方便快速倒置資料。
(5).如果資料量較大,需要分頁展示表格。
需要注意的是,上述的這些需求其實和大部分資料庫提供的功能是非常一致的,而且由於資料庫擁有索引等優化方式以及伺服器更好的效能,更加適合處理這些需求。不過現在流行的前後端分離,也是希望讓客戶端在合理的範圍內,更多的分擔伺服器端的壓力,所以當找到一個平衡時,在前端處理適量的需求是正確的選擇。接下來就嘗試用vue完成這些需求吧。
完成Table.vue:
因為一個多功能表格可能會應用在多個專案中,所以設計思路上儘量將表格相關的內容放在Table.vue元件中,減少耦合,方便複用。
獲取測試資料:
為了更好的對比前端實現以上需求的利與弊,我們需要一份較大較複雜的測試資料。幸運的是我之前的一個專案中,設計的一份API正好滿足這一需求,資料為魔獸世界競技場的天梯排行API,目前這個API處於開放狀態,介面詳見Myarena介紹。
與上一篇教程相類似,還是新建一個api資料夾以及一個arena.js用於管理API介面。再在App.vue中引入arena.js,在created階段獲取資料。作為一個demo,我們只獲取region為CN、laddar為3v3的資料,不過只要將兩個引數通過v-model繫結給對應的表單控制元件,就能很輕鬆的實現不同地區資料的切換。
引入table.vue元件:
如之前所說,思路上我們希望減少table元件與外部環境的耦合,所以我們給Table.vue設定一個props屬性rows,用於獲取App.vue取回的資料。
在App.vue中註冊table組建時要注意,命名不能用預設的table,所以註冊為vTable,就能用<v-table>標籤引入table元件了。
目前為止,我們的App.vue完成了它所有的功能,程式碼如下:
[HTML] 純文字檢視 複製程式碼<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遍歷資料,再通過模板填入,需要注意以下幾個重點:
(1).返回的資料不一定完全符合要求。例如我希望實現通過勝率排序,但資料中只包含了勝負場數,需要先計算一次。2. 資料中用於表現玩家職業的資料為classId這個屬性,但在實際專案中我想要用各職業的icon展示職業,所以我在utils.js中實現了各一個classIdToIcon的工具函式,用於對映classId至sprite圖中的background-position。
(2).以上兩點說明我們最好不要遍歷props獲得的rows這一原始資料。因此另建了一個computed屬性players,並在其中完成了前期處理,我把所有的前期處理放在了handleBefore中。
(3).由於即將使用的各種filters操作比較複雜,所以在handlebefore中進行了console.log('before handle'),方便我們驗證handlebefore在什麼階段被執行了。
完成佈局之後,目前Table.vue中的重點程式碼如下:
[HTML] 純文字檢視 複製程式碼<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(<img id="aimg_kmYZy" onclick="zoom(this, this.src, 0, 0, 0)" class="zoom" file="http://7xs8rx.com1.z0.glb.clouddn.com/class.png" onmouseover="img_onmouseoverfunc(this)" lazyloadthumb="1" border="0" alt="">)', 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中模板部分程式碼如下:
[HTML] 純文字檢視 複製程式碼<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設計中的靈活性讓我們有了更多的可能性,但在學習時,應該以搞明白不同方式在不同場景中的優劣為目標,實際運用時選擇最好的那一種。
用filters實現需求:
在例子中,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>變為:
[JavaScript] 純文字檢視 複製程式碼<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排序我是通過以下程式碼實現的,方式可能不是太好,特此列出:
[HTML] 純文字檢視 複製程式碼<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還是十分明智的。
用computed屬性完成需求:
(1).在Github倉庫中,我用Table.vue.bak檔案儲存了之前一段中用filters實現的程式碼,方便與我們接下里的實現進行比較。
首先整理一下用computed屬性來實現的思路:
首先要實現filterBy, orderBy, limitBy這三個filter的功能,上文中已經提到了他們的內部實現,所以分別用Array.filter, Array.sort和Array.slice重寫一遍並不複雜。
(2).說是computed屬性實現,其實也還是隻有players這個computed屬性,只是在其內部執行了所有的過濾動作,我們實際是把各種過濾器的邏輯放置在各個method中。
(3).不建議把各個過濾method寫的過於抽象,因為就是內建filters高度抽象導致一些特殊需求無法實現,所以不妨就以最針對性的方式:一個method對應一種過濾。
(4).在執行各個過濾method時,依然有最初提到的順序帶來的效率問題。因為vue牽一髮而動全身的特性,任何一個過濾條件改變時,所有過濾method都會執行一遍,所以儘快用高效的過濾器縮短陣列長度顯得更為重要。
(5).我嘗試過通過watch屬性實現最小化method呼叫,但無奈功力不夠沒能實現。同時我也認為前端處理大量資料的情況很少見,並且用第4點中的資料進行優化後,執行效率不算太低,所以沒必要在這個方面做過多糾結。真有效能瓶頸時,從伺服器端尋求解決會更簡單。
注意:在實現各種過濾method時,建議閱讀vue中filterBy, orderBy, limitBy三部分的實現原始碼,其本身對於陣列的操作就有一些優化,非常值得學習。在一些特殊情況中,例如陣列中大量相等值時,過於簡單的sort function會導致執行步數激增,vue中的一些處理都予以了避免。
根據需求目標,我設定了以下這些method(順序即為執行順序):
(1).classFilter:過濾玩家職業,通過item.classId === this.class進行判斷,this.class繫結的是一個select控制元件。
(2).queryFilter:匹配玩家姓名中的欄位,通過item.name.indexOf(this.query)判斷,this.query則繫結一個input控制元件。
(3).ratingFilter:篩選玩家分數段,通過item.rating >= this.rating進行判斷,this.rating繫結了一個型別為range的input控制元件,range的範圍則是用computed屬性進行計算。
(4).sortTable:因為Array.sort進行的步數較多,所以放在陣列被上述3個method處理的較短後進行。
(5).paginate:所有過濾操作完畢之後,就可以進行分頁了。在使用Array.slice()之前,先將陣列的長度傳給this.total儲存起來,用於在分頁後計算總的頁數。
(6).除了以上幾個過濾method以外,當然也還有handleBefore方法對陣列進行前期處理。但是由於players每次都會重新計算,所以為了放止handleBefore被重複執行,應該加上一定的判斷條件,例如handleBefore新增的屬性是否已經存在了等等。
同時,還可以把一些不需要在過濾之前執行的動作從handleBefore中拿出,例如例子中的classId轉換為Icon,可以在過濾之後對最終要展示的資料進行即可,減少一些步數。所以又設定了一個handleAfter方法,用於在分頁完成之後進行後續操作,當然在handleAfter中也可能重複執行,所以如果執行的操作消耗很大,建議同樣新增判斷,避免重複執行。
在例子程式碼中,我在每個方法中都統計了執行的步數,實際結果顯示設定一個合理的過濾順序可以避免一些效能問題,結果如下:
可以看出初始化時,在沒有任何過濾的情況下,sort的步數較高。而一旦新增了一些過濾條件之後,順位靠後的filter和sort的步數都會大幅度減少。
相關文章
- JS實現前臺表格排序功能JS排序
- Element-UI Table 實現篩選資料功能UI
- Spread表格元件For JAVA功能介紹—表格相關操作元件Java
- wpf ObservableCollection篩選功能
- Vue.js介紹Vue.js
- js實現的陣列自定義排序介紹JS陣列排序
- 排序系統的主選單及功能實現排序
- day01-專案介紹&功能實現
- node.js實現爬蟲功能介紹Node.js爬蟲
- excel如何篩選出自己想要的部分 excel表格如何篩選特定內容Excel
- Flutter實現自定義篩選框Flutter
- 實現微信搖一搖功能簡單介紹
- Excel表格排序功能設定Excel排序
- 改造 layui 表格元件實現多重排序UI元件排序
- 說說如何基於 Vue.js 實現表格元件Vue.js元件
- 陣列多重篩選條件排序方法陣列排序
- 簡單介紹SpringMVC RESTFul實現列表功能SpringMVCREST
- 原型設計之校友圈APP功能實現介紹原型APP
- 【Vue】el-table 簡易表格可篩選列Vue
- excel高階篩選怎麼做 表格的高階篩選怎麼設定條件Excel
- 選擇排序java實現排序Java
- 基於vue.js實現樹形表格的封裝Vue.js封裝
- WPS表格技巧:分類彙總與自動篩選結合實現分類快速求和
- 在文字框輸入關鍵詞可以實現篩選功能程式碼例項
- FineReport11 報表技巧02- 實現類Excel表頭篩選功能Excel
- RxFluxArchitecture框架介紹1-基本功能實現UX框架
- redis-cli 實用功能介紹Redis
- php實現 氣泡排序,插入排序,選擇排序PHP排序
- 選擇排序(python)實現排序Python
- OutputStreamWriter介紹&程式碼實現和InputStreamReader介紹&程式碼實現
- 用Axure實現對時間段的篩選
- avalon繫結實現checkbox全選簡單介紹
- 海量資料“一鍵篩選”,比Excel還好用的篩選功能,更便捷了!Excel
- 原生js實現商品排序功能JS排序
- angualr實現滑鼠拖拽排序功能排序
- 簡單介紹Android自定義View實現時鐘功能AndroidView
- 選擇排序和插入排序(C++實現)排序C++
- 常用的php列表多條件篩選功能PHP