封装思想:灰白表格如何优雅封装

201 阅读6分钟

前言

  • 常网IT源码上线啦!
  • 本篇录入技术选型专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。
  • 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。
  • 接下来想分享一些自己在项目中遇到的技术选型以及问题场景。

生怕体检像X光,把我们平常偷偷熬的夜、应酬喝的酒、解压吃进去的夜宵,都照的一清二楚。

1.jpg

一、前言

最讲封装,还是用组件例子来说吧,不然干巴巴说有点干巴巴。

3.webp

我们就来一个最简单的表格组件来讲。

像这样子的表格效果,在很多详情页的时候都会看到。

可随意配置,独占一行,或者一行两列。

于是,我们来思考一下怎么封装较好。

二、组件封装

我们先给组件一个命名:PaleTable,灰白表格。

那应该怎么开发。

我们看样式,其实他就是一个表格,一个好的组件,往往用最朴实无华的元素开发。

td。

一行几列,可以用colspan属性实现。

代码如下:

一个最简单的小组件就完成了。

  <table class="static-table">
    <tr v-for="(list, key) in data" :key="key">
    <template v-for="(item, itemKey) in list">
      <td class="labels" :key="item.labels">{{ item.labels }}</td>
      <td class="value" :colspan="item.colspan" :width="item.width" :key="itemKey">
        <span >{{ item.value }}</span>
      </td>
    </template>
  </tr>
</table>

props: {
    data: {
      type: Array,
      default: () => [],
    },
  },

父组件传入:

data: [
  // 这是一行
  [
    {
      labels: '标题',
      value: '',
      width: '80%',
      colspan: 3,
    },
  ],
  // 这是一行两列
  [
    {
      labels: '类型',
      value: '',
      width: '40%',
    },
    {
      labels: '时间',
      value: '',
      width: '40%',
    },
  ],
]

三、需求提升

很显然,这只适用于最简单的展示。

而需求往往是不会这么简单就放过我们的,比如,添加附件下载,图片查看,按钮穿插进去。

于是,我们的PaleTable组件得到更强大的提升。

代码:github.com/git-Dignity…

<template>
  <table class="static-table">
    <tr v-for="(list, key) in data" :key="key">
      <template v-for="(item, itemKey) in list">
        <td class="labels" :key="item.labels">{{ item.labels }}</td>
        <td class="value" :colspan="item.colspan" :width="item.width" :key="itemKey">
          // 数组
          <div>
            <div v-for="(file, fileIndex) in item.value" :key="fileIndex"></div>
          </div>

          // 附件
          <div class="file-content" v-if="item.isFile">
            <div class="file-content-block" v-for="(file, fileIndex) in item.value" :key="fileIndex">
              <span class="file-content-block__text" title="点击可下载" @click="clickName(file)">{{ file.originalName }}</span>
              <el-image
                v-if="['png', 'jpg', , 'jpeg', 'gif'].includes(file.extension)"
                style="width: 50px; height: 50px"
                :src="file.src"
                fit="fill"
                :preview-src-list="[file.src]"
                @click="handleClickItem"
              >
              </el-image>

              <div v-else style="height: 50px"></div>
            </div>
          </div>
          // 按钮
          <div v-else-if="item.btn && item.btn.isShow" :style="item.btn.style">
            <span>{{ item.value }}</span>
            <div>
              <i v-if="item.btn.icon" :class="item.btn.icon.class" :style="item.btn.icon.style"></i>
              <el-button :type="item.btn.type || 'text'" size="small" plain @click="$emit('btnClick', item)">{{ item.btn.name }}</el-button>
            </div>
          </div>

          // 最简单的纯文字
          <span v-else>{{ item.value }}</span>
        </td>
      </template>
    </tr>
  </table>
</template>

<script>
export default {
  props: {
    data: {
      type: Array,
      default: () => [],
    },
  },
  methods: {
    // 点击文件名字,进行下载
    clickName(file) {
      this.$emit('click', file)
    },
    // 关闭el-image遮罩层
    handleClickItem() {
      this.$nextTick(() => {
        // 获取遮罩层dom
        let domImageMask = document.querySelectorAll('.el-image-viewer__mask')
        if (!domImageMask.length) {
          return
        }
        domImageMask.forEach((d) => {
          d.addEventListener('click', () => {
            // 点击遮罩层时调用关闭按钮的 click 事件
            d.nextElementSibling.click()
          })
        })
      })
    },
  },
}
</script>
<style scoped lang='scss'>
.static-table {
  width: 100%;
  border-left: 1px solid #e5e5e5;
  border-collapse: collapse;
  color: #333;
  margin-bottom: 20px;
}

.static-table tr td {
  border-bottom: 1px solid #e5e5e5;
  border-right: 1px solid #e5e5e5;
  border-top: 1px solid #e5e5e5;
  padding: 15px 12px;
  background: white;
}

.static-table .labels {
  width: 160px;
  vertical-align: middle;
  font-weight: 400;
  background-color: #f8f8f8;
}

.static-table .el-radio__input.is-disabled.is-checked .el-radio__inner::after {
  background-color: #a3b8dd;
  width: 10px;
  height: 10px;
}

.static-table .el-input.is-disabled .el-input__icon {
  display: none;
}

.el-static-table thead tr th {
  color: #333;
  background-color: #f8f8f8;
  font-weight: normal;
}

.static-table .el-checkbox__input.is-disabled.is-checked .el-checkbox__inner:after {
  border-color: #a3b8dd;
}

.file-content {
  display: flex;
  align-items: center;
  &-block {
    padding: 10px;
    border: 1px solid #f3f3f3;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 10px;
    &__text {
      margin-right: 10px;
      cursor: pointer;
    }
  }
}
</style>

这么一个强大的组件就封装好了。

我们来处理一下数据。

四、数据处理

细心的你,想必也发现了,我们的value是直接写死在数组里面的。

4.webp

那,我们需要动态的设置value属性。

一般来说,我们拿到后端的数据,都是对象结构。

而我们上面定义的是没有name(后端返回的key)

怎么确定一个二维数组的位置?

那就是行和列。

假设我们已经有了,setArrItem函数,先不用管这个函数是怎么实现的。

let map = {
  feedbackTitle: [0, 0],
  type: [1, 0],
  time: [1, 1],
}

setArrItem(map, 'value', this.testData, this.form)

解释一下:setArrItem函数接收第一个参数,是映射关系,map对象里面的key,对应着后端返回的对象的key,后面的数组是我们传入PaleTable的数组的行和列

第二个参数:修改this.testData的value属性。

第三个参数:传入PaleTable的数组

第四个参数:后端返回的对象

这样子根据现有的数据结构就可以动态设置value渲染。

接下来,我们看看setArrItem函数怎么实现

/**
 * 设置数组的值
 *
 * @param {Object} map
 * @param {String} value
 * @param {Array} arr
 * @param {Object} obj
 * @example
 * let map = { metaDataSubjectName: [0, 0] };
 * let arr = [ [ { value: 1 } ] ]
 * let obj = { metaDataSubjectName: 1 }
 * setArrItem(map, 'value', arr, obj)
 * 遍历map拿到 [0, 0] 的值,设置arr的属性,arr[0[0]]赋值为obj['metaDataSubjectName']的值
 */
const setArrItem = (map, value, arr, obj) => {
  for (let key in map) {
    if (Object.hasOwnProperty.call(map, key)) {
      const item = getArrItem(arr, map[key])
      item[value] = obj[key]  // 设置value
    }
  }
}

map是一个对象,forin遍历取值。

18行的getArrItem函数,主要是从arr数组中,拿到我们想要更新value的对象。

如:

{
  labels: '受理人',
  value: '',
  width: '40%',
}

看看getArrItem的实现。

/**
 * 获取数组中的值
 *
 * @param {*} data
 * @param {*} valueArr
 * @return {*}
 * @example
 * getArrItem([{a: 1}], [0, 'a']) ==> 1
 */
const getArrItem = (data, valueArr) => {
  if (valueArr.length == 0) {
    return data
  }

  data = data[valueArr.shift()]
  return getArrItem(data, valueArr)
}

第一个参数data:是我们的arr

第二个参数是数组:行和列

valueArr.shift():取出行,data就可以精确到第几行,接着递归调用自己。

第二次遍历,valueArr还有一个列,不走if。

继续valueArr.shift():取出列,data就可以精确到第几列,接着递归调用自己。

此时valueArr为空,返回data,此时获取到arr的第几行第几列的对象了。

完美实现~

五、优化

思考一下,这样子处理,会不会觉得麻烦?

是否有更好的实现?

如果testData数组里面的字段顺序换了一下,那动态设置value的map里面的数组[0,1]也要跟着换,有点麻烦。

那怎么优化好?

其实可以在testData的每个对象里面添加key,即后端返回的key,这样子就对应上了,我们只需要循环的时候去查找就好。

那我一开始为什么不这样子做?

目前做法:相当于已经帮我们关系映射好了,就少了一层查找,也就是少了一层for循环。

而如果把key加在testData数组里面,会增加循环的次数。

那是否一开始数组就不要设置二维数组?

可以,设置一维数组,全部平铺化,就根xlsx表格一样,记录行和列去映射,但对直观性就不是很好。

无法一眼就看出来。这也是为什么一开始要设计成二维数组。(有二维数组,处理值的时候可以打平化)

因为我们的PaleTable需要到。

其实还有很多可能比较好的处理方式,看个人的抉择。

至此撒花~

后记

我们在实际项目中或多或少遇到一些奇奇怪怪的问题。

自己也会对一些写法的思考,为什么不行🤔,又为什么行了?

最后,祝君能拿下满意的offer。

我是Dignity_呱,来交个朋友呀,有朋自远方来,不亦乐乎呀!深夜末班车

👍 如果对您有帮助,您的点赞是我前进的润滑剂。

以往推荐

决定我的offer:问了我3个websocket的问题

几分钟写一个插件

前端哪有什么设计模式(12k+)

为什么没人用mixin(7k+)

小小导出,我大前端足矣!

靓仔,说一下keep-alive缓存组件后怎么更新及原理?

面试官问我new Vue阶段做了什么?

前端仔,快把dist部署到Nginx上

多图详解,一次性啃懂原型链(上万字)

Vue-Cli3搭建组件库

Vue实现动态路由(和面试官吹项目亮点)

VuePress搭建项目组件文档

原文链接

juejin.cn/post/749789…