與上篇實踐教程一樣,在這篇文章中,我將繼續(xù)從一種常見的功能――表格入手,展示Vue.js中的一些優(yōu)雅特性。同時也將對filter功能與computed屬性進行對比,說明各自的適用場景,也為vue2.0版本中即將刪除的部分filter功能做準備。
需求分析
還是先從需求入手,想想實現(xiàn)這樣一個功能需要注意什么、大致流程如何、有哪些應(yīng)用場景。
需要注意的是,上述的這些需求其實和大部分數(shù)據(jù)庫提供的功能是非常一致的,而且由于數(shù)據(jù)庫擁有索引等優(yōu)化方式以及服務(wù)器更好的性能,更加適合處理這些需求。不過現(xiàn)在流行的前后端分離,也是希望讓客戶端在合理的范圍內(nèi),更多的分擔服務(wù)器端的壓力,所以當找到一個平衡時,在前端處理適量的需求是正確的選擇。
接下來就嘗試用vue完成這些需求吧。
完成Table.vue
因為這樣一個多功能表格可能會應(yīng)用在多個項目中,所以設(shè)計思路上盡量將表格相關(guān)的內(nèi)容放在Table.vue組件中,減少耦合,方便復用。
獲取測試數(shù)據(jù)
為了更好的對比前端實現(xiàn)以上需求的利與弊,我們需要一份較大較復雜的測試數(shù)據(jù)。幸運的是我之前的一個項目中,設(shè)計的一份API正好滿足這一需求,數(shù)據(jù)為魔獸世界競技場的天梯排行API,目前這個API處于開放狀態(tài),接口詳見Myarena介紹。
與上一篇教程相類似,還是新建一個api文件夾以及一個arena.js用于管理API接口。再在App.vue中引入arena.js,在created階段獲取數(shù)據(jù)。作為一個demo,我們只獲取region為CN、laddar為3v3的數(shù)據(jù),不過只要將兩個參數(shù)通過v-model綁定給對應(yīng)的表單控件,就能很輕松的實現(xiàn)不同地區(qū)數(shù)據(jù)的切換。
引入table.vue組件
如之前所說,思路上我們希望減少table組件與外部環(huán)境的耦合,所以我們給Table.vue設(shè)置一個props屬性rows,用于獲取App.vue取回的數(shù)據(jù)。在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設(shè)置,篇幅考慮這里進行了省略,對完整代碼有興趣的可以移步文章末尾的Github倉庫。
基礎(chǔ)布局
Table.vue的template中主要為3部分,分別是用于搜索、篩選和分頁的表單控件、用于排序表格的表頭thead以及用于展示數(shù)據(jù)的tbody。
首先來完成tbody的部分,基本思路就是用v-for遍歷數(shù)據(jù),再通過模板填入,需要注意以下幾個重點:
完成布局之后,目前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>標簽中進行各種操作,但可想而知在進行一些邊界情況的判斷時,會出現(xiàn)各種含有player.weeklyWins, player.weeklyLosses等長命名變量的三元表達式。本來是出于便利考慮,卻反而導致代碼難以維護。因此新建了個一個bar組件,將勝負傳入組件中,在bar組件內(nèi)部用更語義化的方式實現(xiàn),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的過程中,需要注意的是框架中許多方法其實在內(nèi)部最終是殊途同歸。
例如我們可以直接在元素中執(zhí)行一些對數(shù)據(jù)的操作,例如@click="show = !show",同樣的我們也可以對事件綁定方法,再在方法中操作數(shù)據(jù),例如@click="toggle", toggle () { this.show = !this.show }。還比如我們可以用computed屬性和watch屬性實現(xiàn)很多相同的功能,接下來還將用computed去實現(xiàn)和filters相同的功能。
vue設(shè)計中的靈活性讓我們有了更多的可能性,但在學習時,應(yīng)該以搞明白不同方式在不同場景中的優(yōu)劣為目標,實際運用時選擇最好的那一種。
用filters實現(xiàn)需求
在例子中,players實際是一個5000條數(shù)據(jù)的數(shù)組,在不做任何處理時,將直接渲染出5000個<tr>,所以先趕緊過濾吧!
對于v-for循環(huán),vue中提供了3中filters過濾數(shù)組,分別為filterBy, orderBy, limitBy,其功能對應(yīng)了搜索/篩選、排序和分頁,實現(xiàn)分別是使用了Array.filter, Array.sort(), Array.slice()。
這三種filters在使用時非常便利,只要在v-for后用|分離再添加對應(yīng)的filters即可,這3中filter的具體參數(shù)可以查看官方API,這里不多做贅述。
需要注意的是,實際的過程是先將被遍歷的數(shù)組(例子中的players)依次通過過濾器,再將最后一個過濾器返回的數(shù)組進行v-for操作。
因此,filters放置的順序是需要根據(jù)需求來調(diào)整的,也因為每種過濾器的內(nèi)部實現(xiàn)效率不同,所以在需求優(yōu)先級不明顯時,應(yīng)該以效率為優(yōu)先。
注意:實際測試時,發(fā)現(xiàn)不論怎么過濾數(shù)組,handleBefore方法都沒有再次執(zhí)行,也就是說players數(shù)組并沒有被改動過。
例如在我的例子中,我希望可以篩選出名字或者服務(wù)器包含了我所輸入內(nèi)容的玩家,并且將他們按照某種方式排序,最后的結(jié)果每頁只顯示20條。那么顯然剪切數(shù)組永遠應(yīng)該放在最后一步,而排序和過濾在需求中沒有明顯的優(yōu)先級。但是大部分情況下,sort的效率都要低于filter,所以我們先進行filter,減少數(shù)組長度,再sort。
有了這一思路之后,用于v-for的<tr>變?yōu)椋?/p>
<trv-for="player of players| filterBy query in 'name' 'realmName'| orderBy sort.key sort.val| limitBy 20 (page-1)*20":class="player.factionId? 'horde':'alliance'">
這里直接將各個變量動態(tài)化,再通過Table.vue中的input綁定v-model以及表頭thead綁定@click事件來改變篩選的條件,就已經(jīng)實現(xiàn)了大部分的搜索、過濾、分頁功能。
表頭改變sort排序我是通過以下代碼實現(xiàn)的,方式可能不是太好,特此列出:
<thead> <tr> <th @click="sort = {key: 'ranking', val: -sort.val}">排名</th> <th @click="sort = {key: 'rating', val: -sort.val}">分數(shù)</th> <th>資料</th> <th>服務(wù)器</th> <th @click="sort = {key: 'weeklyRate', val: -sort.val}">本周戰(zhàn)績</th> <th @click="sort = {key: 'seasonRate', val: -sort.val}">賽季戰(zhàn)績</th> </tr></thead>可以看到,通過vue的filters功能,已經(jīng)可以輕松完成我們的大部分功能,代碼量極少。這也是vue2.0前瞻發(fā)布之后,提出廢棄部分filters功能后許多人反應(yīng)較為強烈的原因。但是如同作者在改動說明中所說,filters對于初學者來說不易理解,并且filters的功能都可以用computed屬性進行更靈活、更好把控的實現(xiàn)。而且在一些復雜條件下,堆疊過濾器會造成一些額外的復雜性以及不方便之處。
那么何為復雜條件呢?例如我增添兩個需求,一是按職業(yè)篩選玩家,而是篩選出一定分數(shù)以上的玩家,那么后者用filterBy就不太好實現(xiàn)了。我們需要將對分數(shù)段的過濾放在filters之前進行,但又要注意不破壞players數(shù)組本身。在實際完成時,會發(fā)現(xiàn)這個過程還是比較糾結(jié)的。
除此之外,我們還會發(fā)現(xiàn)分頁中最重要的一個信息――總頁數(shù)我們獲取不到。因為vue并沒有把一串過濾管道中產(chǎn)出的最終用于v-for的數(shù)組暴露出來,所以我們無法獲得這個實際被循環(huán)的數(shù)組的長度。
在實際hack這些需求時,發(fā)現(xiàn)很容易與filters的執(zhí)行順序發(fā)生沖突,因此決定重新用computed屬性來實現(xiàn)一遍所有功能,不借助自帶的filters。
當然,在這一段的前半部分中,我們顯而易見的感受到了來自filters的便利性。如果需求中filters可以滿足,那么在1.x版本中使用filters還是十分明智的!
用computed屬性完成需求
在Github倉庫中,我用Table.vue.bak文件儲存了之前一段中用filters實現(xiàn)的代碼,方便與我們接下里的實現(xiàn)進行比較。
首先整理一下用computed屬性來實現(xiàn)的思路:
注意:在實現(xiàn)各種過濾method時,建議閱讀vue中filterBy, orderBy, limitBy三部分的實現(xiàn)源碼,其本身對于數(shù)組的操作就有一些優(yōu)化,非常值得學習。在一些特殊情況中,例如數(shù)組中大量相等值時,過于簡單的sort function會導致執(zhí)行步數(shù)激增,vue中的一些處理都予以了避免。
根據(jù)需求目標,我設(shè)置了以下這些method(順序即為執(zhí)行順序):
在例子代碼中,我在每個方法中都統(tǒng)計了執(zhí)行的步數(shù),實際結(jié)果顯示設(shè)置一個合理的過濾順序可以避免一些性能問題,結(jié)果如下:

可以看出初始化時,在沒有任何過濾的情況下,sort的步數(shù)較高。而一旦添加了一些過濾條件之后,順位靠后的filter和sort的步數(shù)都會大幅度減少。
DEMO地址
由于工作比較忙,暫時沒有打算將開頭中展示的MyArena項目重構(gòu),不過可以想象那會是一個很好的用vue制作單頁應(yīng)用的示例,后續(xù)的教程中可能會用來做例子。
本次教程中的例子,專注于展示多功能表格本身
寫作計劃
上周是Vue.js開發(fā)實踐的第一篇文章,也是我第一次在SF社區(qū)的個人專欄里發(fā)表文章,希望能夠把平時遇到的一些問題和解決的思路分享給大家,自己也進行一個梳理。
開發(fā)實踐這個系列會用一些小例子,展示一些思路,實現(xiàn)一些有用、可復用的常見功能。計劃中,還會有Vue.js實戰(zhàn)系列和Sails.js實戰(zhàn)系列兩個系列的文章。
前者從較完整的項目出發(fā),分析技術(shù)選型、vue-router和vuex的使用、多端共用代碼、后期維護等方面的一些考量。后者則是用Sails.js這個框架構(gòu)建企業(yè)級Node.js后端的一些嘗試和心得,包括框架的優(yōu)缺點、橫向?qū)Ρ纫约凹毠?jié)摸索等等。
目前也在關(guān)注阿里的開源項目Weex的內(nèi)測進展,理想中的狀態(tài)是用Weex實現(xiàn)項目在移動端App的開發(fā),真正完成JS全棧,不過Weex還沒正式開源,有待觀望,所以只是后期設(shè)想,暫時不在計劃內(nèi)。
文章目前就只發(fā)在SF的專欄里,所以有意見建議都請在文章底部留言。同時由于以上所說的所有工作都由我一個人在負責,所以文章的更新可能時快時慢,爭取做到一周一篇。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持武林網(wǎng)。
新聞熱點
疑難解答