We should separate Structure, Presentation, and Behavior. -- The Golden Rule
2019年上半年囱井,間間斷斷寫了一些頁面,也為我的全棧打上了前端這塊拼圖。
這篇文章,我會先介紹一下我對前端的理解前翎,然后用vue框架寫一個自定義的表格的demo杈帐。
A Quick Glimpse
在公司里,我做了一些后臺管理服務(wù)竞思,也開發(fā)了一些數(shù)據(jù)分析工具。后臺管理服務(wù)對頁面的要求不高钞护,python可以用jinja盖喷,java(kotlin)可以用ftl,來渲染動態(tài)頁面难咕,開發(fā)快速课梳,簡單實用距辆。數(shù)據(jù)分析工具的頁面就會比較復(fù)雜,PM也比較看重頁面的美觀和交互暮刃,我用了當(dāng)下流行的vue框架跨算,頁面交互處理起來確實更方便。
我自己在寫前端頁面的時候椭懊,有一個豁然開朗的時間點诸蚕,關(guān)于黃金法則“結(jié)構(gòu)和表現(xiàn)相分離”。了解法則之前氧猬,我專注于實現(xiàn)頁面價值背犯,在代碼的結(jié)構(gòu)上思考的不多,然后頁面進行增量和迭代時都痛苦不堪盅抚;了解之后漠魏,寫代碼就有了一定的理論指導(dǎo):
- html和css的分離。讓盒子模型一下子非常清晰了妄均,拿到PM的原型圖柱锹,先分幾個大塊,結(jié)構(gòu)就基本確定了丰包;
- js和css的分離禁熏。讓頁面事件變得非常清晰,js基本只負(fù)責(zé)click邑彪,input和select等幾個用戶主動交互的事件瞧毙;
- css對表現(xiàn)的絕對控制。讓苛刻的PM也喜笑顏開锌蓄,掌握一些基本style (display, position等)升筏,就能完全滿足PM關(guān)于位置撑柔、顏色瘸爽、大小等等的任性要求;
- 還不滿足铅忿?HTML5中的media tag剪决,加上pixi.js庫,多媒體和動畫也不在話下檀训。
理論核心 + 不斷實踐柑潦,在應(yīng)用or業(yè)務(wù)層就感覺非常棒了。
A Brief Instance
在做數(shù)據(jù)分析工具的時候峻凫,最常寫的就是畫圖和表格渗鬼。這里做一個table demo,分享一下所思所學(xué)荧琼。
在開始寫代碼之前譬胎,我們先想一下表格的常見屬性差牛。
- 不固定的列數(shù)。有五列的表堰乔,也有九列的表偏化,表頭的名字也經(jīng)常換,表頭有時會需要一些注釋镐侯;
- 篩選的列侦讨。有些列需要能夠篩選;
- 排序的列苟翻。有些列需要能夠排序韵卤;
- 個性化的單元格。有的單元格可能需要特殊處理袜瞬。
所以怜俐,我首先把表格分成了header和body,每一個單元格都是一個對象邓尤,具有單元格的一些屬性拍鲤。
在渲染數(shù)據(jù)的過程中,為了響應(yīng)篩選和排序汞扎,采用行列索引的方式依次渲染單元格季稳。為了篩選和排序互不影響,就簡單地采用了全部排序的方式澈魄。
想清楚了這些問題之后景鼠,我們就可以開始寫代碼了。
說到開始寫代碼痹扇,前端demo代碼有一點很有意思铛漓,因為即時重啟的緣故,不用測試代碼就有直觀的反饋鲫构,讓每一行代碼都有一個功能點浓恶,很棒!
那结笨,就開始吧包晰。
- 直接使用
vue create
創(chuàng)建一個新的項目。
vue create custom-table
# 安裝一些必要的依賴炕吸,bootstrap伐憾,jquery,fontawesome之類的
yarn add bootstrap jquery
# 為了讓vue更好的使用全局的jquery赫模,webpack提供的plugin树肃,這里可以簡單的創(chuàng)建一個vue.config.js
touch vue.config.js
- 在App.vue中寫出表格的結(jié)構(gòu)。
<!-- 表的結(jié)構(gòu) -->
<table class="table table-bordered">
<thead>
<th></th>
<th v-for="(cell, colIndex) in header" :key="colIndex">
<span v-text="cell.value"></span>
<info-element v-if="cell.info" :info="cell.info" />
<filter-element v-if="cell.filter" />
<sort-element v-if="cell.sort" />
</th>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in body" :key="rowIndex">
<td v-text="rowIndex"></td>
<td v-for="(cell, colIndex) in row" :key="colIndex">
<cell-element :cell="cell" />
</td>
</tr>
</tbody>
</table>
// 表的數(shù)據(jù)
props: {
header: {
type: Array,
require: true,
default: () => {
return [
{ value: "col1", info: "這是第一列" },
{ value: "col2", filter: true },
{ value: "col3", sort: true }
];
}
},
body: {
type: Array,
require: true,
default: () => {
return [
[{ value: "col1", color: "red" }, { value: "col2", type: "percent"}, { value: "col3", type: "float" }],
[{ value: "col1" }, { value: "col2" }, { value: "col3" }]
];
}
}
}
這里可以感受到vue語法糖的優(yōu)雅瀑罗,for和if很好的嵌入胸嘴,通過數(shù)據(jù)來改變DOM結(jié)構(gòu)莉钙。
也能感受我的設(shè)計意圖,header中的屬性可以決定表頭的特殊功能(篩選筛谚、排序磁玉、注釋);body中的屬性可以去改變單元格的表現(xiàn)(css、format)驾讲。
- 有了清晰的結(jié)構(gòu)蚊伞,就開始組件補全計劃吧。
用FilterElement來說吮铭,table父組件中會有若干個filter組件时迫,每一個filter組件輸入rowIndex和對應(yīng)colIndex的value([{rowIndex: rowIndex, value: value}]
或者簡單寫成[[rowIndex, value]]
),輸出經(jīng)過篩選之后的rowIndex;
同時,為了讓多列同時篩選谓晌,我們?nèi)〔煌M件輸出值的并集掠拳。
想清楚這兩個細(xì)節(jié)之后,代碼就順理成章了纸肉。
在table父組件中溺欧,我們有:
<!-- 子組件之間的數(shù)據(jù)傳遞 -->
<filter-element
v-if="cell.filter"
:column-data="getAllColumnData(colIndex)"
@filterRows="filterRows(colIndex, $event)"
/>
data() {
return {
// key是colIndex, value是fitleredRowIndexes, 用來最后取并集
filterBuffer: {}
}
},
methods: {
// 獲取表格一列的值,和sortElement的方法一樣
getAllColumnData: function(colIndex) {
let tmpArray = [];
for (let rowIndex = 0; rowIndex < this.body.length; rowIndex++) {
tmpArray.push([rowIndex, this.body[rowIndex][colIndex].value]);
}
return tmpArray;
},
// 用filterBuffer來保存某一列的排序索引
filterRows: function(colIndex, filteredRowIndexes) {
this.$set(this.filterBuffer, colIndex, filteredRowIndexes);
}
}
在FilterElement子組件中柏肪,我們有:
<div class="inline-block">
<button
id="filterEle"
class="btn dropdown-toggle like-text-btn"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
/>
<div class="dropdown-menu fit-view" aria-labelledby="filterEle" @click.stop>
<div
class="dropdown-item"
v-for="(column, index) in allDataSet"
:key="index"
>
<input :value="column" v-model="checkValue" type="checkbox" />
<span v-text="column" />
</div>
</div>
</div>
computed: {
// 獲得列的數(shù)據(jù)
allDataArray: function() {
let tmpArray = [];
for (let i = 0; i < this.columnData.length; i++) {
tmpArray.push(this.columnData[i][1]);
}
return tmpArray;
},
// 去重數(shù)據(jù)
allDataSet: function() {
return Array.from(new Set(this.allDataArray));
},
// 篩選后的行索引
filteredRowIndexes: function() {
let tmpArray = [];
for (let i = 0; i < this.columnData.length; i++) {
if (this.checkValue.includes(this.allDataArray[i])) {
tmpArray.push(this.columnData[i][0]);
}
}
return tmpArray;
}
},
watch: {
// 監(jiān)聽用戶的篩選事件
checkValue: function() {
this.$emit("filterRows", this.filteredRowIndexes);
}
}
在寫篩選組件時姐刁,有很多值得思考的地方:
- 關(guān)于去重元素,如果是primitive data烦味,我們可以直接使用Set聂使;那如果是對象,就需要hash去重谬俄,還可能要考慮“與”和“或”的關(guān)系柏靶;
- 無論js、java溃论、還是c屎蜓,在做對象遍歷的時候都沒有python那種“自在”的感覺。不過js在性能上好像有這樣的關(guān)系蔬芥,
for > for-of > forEach > filter > map > for-in
梆靖; - 對象的深控汉、淺拷貝笔诵,確實比python需要要花更多的心思;
- 全選 / 全部選 / checkbox的不確定態(tài)姑子;
- 完成所有組件之后乎婿,demo就完成了。全部代碼在我的GIT REPO里街佑,歡迎大家查看谢翎。
A Short Summary
前端三板斧捍靠,HTML、CSS 和 JS森逮,隨著實踐也掌握的越來越多榨婆。JS的對象和原型,CSS的loader和parser褒侧,Vue的生命周期和狀態(tài)管理良风,都略知一二。學(xué)習(xí)會讓人開心闷供,但也會讓人迷惘烟央,因為在這個焦慮的社會,價值還是太重要了歪脏。共勉 ~