全栈之路 —— Vue自定义表格组件

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代码有一点很有意思,因为即时重启的缘故,不用测试代码就有直观的反馈,让每一行代码都有一个功能点,很棒!


一个DEMO

那,就开始吧。

  1. 直接使用vue create创建一个新的项目。
vue create custom-table
# 安装一些必要的依赖,bootstrap,jquery,fontawesome之类的
yarn add bootstrap jquery
# 为了让vue更好的使用全局的jquery,webpack提供的plugin,这里可以简单的创建一个vue.config.js
touch vue.config.js
  1. 在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)。

  1. 有了清晰的结构,就开始组件补全计划吧。
    用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的不确定态;
  1. 完成所有组件之后,demo就完成了。全部代码在我的GIT REPO里,欢迎大家查看。

A Short Summary

前端三板斧,HTML、CSS 和 JS,随着实践也掌握的越来越多。JS的对象和原型,CSS的loader和parser,Vue的生命周期和状态管理,都略知一二。学习会让人开心,但也会让人迷惘,因为在这个焦虑的社会,价值还是太重要了。共勉 ~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容

  • 基于Vue的一些资料 内容 UI组件 开发框架 实用库 服务端 辅助工具 应用实例 Demo示例 element★...
    尝了又尝阅读 1,124评论 0 1
  • UI组件 element- 饿了么出品的Vue2的web UI工具套件 Vux- 基于Vue和WeUI的组件库 m...
    小姜先森o0O阅读 9,231评论 0 72
  • UI组件 element- 饿了么出品的Vue2的web UI工具套件 Vux- 基于Vue和WeUI的组件库 m...
    王喂马_阅读 6,391评论 1 77
  • 一:什么是闭包?闭包的用处? (1)闭包就是能够读取其他函数内部变量的函数。在本质上,闭包就 是将函数内部和函数外...
    xuguibin阅读 9,195评论 1 52
  • vue概述 在官方文档中,有一句话对Vue的定位说的很明确:Vue.js 的核心是一个允许采用简洁的模板语法来声明...
    li4065阅读 7,108评论 0 25