We should separate Structure, Presentation, and Behavior. -- The Golden Rule
2019年上半年,间间断断写了一些页面,也为我的全栈打上了前端这块拼图。
这篇文章,我会先介绍一下我对前端的理解,然后用vue框架写一个自定义的表格的demo。
A Quick Glimpse
在公司里,我做了一些后台管理服务,也开发了一些数据分析工具。后台管理服务对页面的要求不高,python可以用jinja,java(kotlin)可以用ftl,来渲染动态页面,开发快速,简单实用。数据分析工具的页面就会比较复杂,PM也比较看重页面的美观和交互,我用了当下流行的vue框架,页面交互处理起来确实更方便。
我自己在写前端页面的时候,有一个豁然开朗的时间点,关于黄金法则“结构和表现相分离”。了解法则之前,我专注于实现页面价值,在代码的结构上思考的不多,然后页面进行增量和迭代时都痛苦不堪;了解之后,写代码就有了一定的理论指导:
- html和css的分离。让盒子模型一下子非常清晰了,拿到PM的原型图,先分几个大块,结构就基本确定了;
- js和css的分离。让页面事件变得非常清晰,js基本只负责click,input和select等几个用户主动交互的事件;
- css对表现的绝对控制。让苛刻的PM也喜笑颜开,掌握一些基本style (display, position等),就能完全满足PM关于位置、颜色、大小等等的任性要求;
- 还不满足?HTML5中的media tag,加上pixi.js库,多媒体和动画也不在话下。
理论核心 + 不断实践,在应用or业务层就感觉非常棒了。
A Brief Instance
在做数据分析工具的时候,最常写的就是画图和表格。这里做一个table demo,分享一下所思所学。
在开始写代码之前,我们先想一下表格的常见属性。
- 不固定的列数。有五列的表,也有九列的表,表头的名字也经常换,表头有时会需要一些注释;
- 筛选的列。有些列需要能够筛选;
- 排序的列。有些列需要能够排序;
- 个性化的单元格。有的单元格可能需要特殊处理。
所以,我首先把表格分成了header和body,每一个单元格都是一个对象,具有单元格的一些属性。
在渲染数据的过程中,为了响应筛选和排序,采用行列索引的方式依次渲染单元格。为了筛选和排序互不影响,就简单地采用了全部排序的方式。
想清楚了这些问题之后,我们就可以开始写代码了。
说到开始写代码,前端demo代码有一点很有意思,因为即时重启的缘故,不用测试代码就有直观的反馈,让每一行代码都有一个功能点,很棒!
那,就开始吧。
- 直接使用
vue create
创建一个新的项目。
vue create custom-table
# 安装一些必要的依赖,bootstrap,jquery,fontawesome之类的
yarn add bootstrap jquery
# 为了让vue更好的使用全局的jquery,webpack提供的plugin,这里可以简单的创建一个vue.config.js
touch vue.config.js
- 在App.vue中写出表格的结构。
<!-- 表的结构 -->
<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>
// 表的数据
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语法糖的优雅,for和if很好的嵌入,通过数据来改变DOM结构。
也能感受我的设计意图,header中的属性可以决定表头的特殊功能(筛选、排序、注释);body中的属性可以去改变单元格的表现(css、format)。
- 有了清晰的结构,就开始组件补全计划吧。
用FilterElement来说,table父组件中会有若干个filter组件,每一个filter组件输入rowIndex和对应colIndex的value([{rowIndex: rowIndex, value: value}]
或者简单写成[[rowIndex, value]]
),输出经过筛选之后的rowIndex;
同时,为了让多列同时筛选,我们取不同组件输出值的并集。
想清楚这两个细节之后,代码就顺理成章了。
在table父组件中,我们有:
<!-- 子组件之间的数据传递 -->
<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: {
// 获得列的数据
allDataArray: function() {
let tmpArray = [];
for (let i = 0; i < this.columnData.length; i++) {
tmpArray.push(this.columnData[i][1]);
}
return tmpArray;
},
// 去重数据
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: {
// 监听用户的筛选事件
checkValue: function() {
this.$emit("filterRows", this.filteredRowIndexes);
}
}
在写筛选组件时,有很多值得思考的地方:
- 关于去重元素,如果是primitive data,我们可以直接使用Set;那如果是对象,就需要hash去重,还可能要考虑“与”和“或”的关系;
- 无论js、java、还是c,在做对象遍历的时候都没有python那种“自在”的感觉。不过js在性能上好像有这样的关系,
for > for-of > forEach > filter > map > for-in
; - 对象的深、浅拷贝,确实比python需要要花更多的心思;
- 全选 / 全部选 / checkbox的不确定态;
- 完成所有组件之后,demo就完成了。全部代码在我的GIT REPO里,欢迎大家查看。
A Short Summary
前端三板斧,HTML、CSS 和 JS,随着实践也掌握的越来越多。JS的对象和原型,CSS的loader和parser,Vue的生命周期和状态管理,都略知一二。学习会让人开心,但也会让人迷惘,因为在这个焦虑的社会,价值还是太重要了。共勉 ~