乐趣区

关于javascript:如何使用-render-函数封装高扩展的组件

如何应用 render 函数封装高扩大的组件

后面的文章有提到,vue 官网给出的 render 函数的例子只能体现 render 函数的优雅的一方面,却不能看出其扩展性,明天就来封装一个体现其扩展性的组件。

需要

后盾治理中经常有如下布局的数据展现需要:

像表格又不是表格,像表单又不是表单,实际上样子像表格,出现的数据是一个对象,和 form 的绑定的值一样,我将其称为表单式表格。

款式深的列是题目,浅的列是题目对应的取值,数据往往是服务器返回的,题目往往是定宽的,取值能要各种各样,比方显示一张图片,值为 01,须要显示是与否,有时候须要增加一个批改按钮,让用户能批改某些值,还须要设置某一列逾越几列。

先来看看一个基于 element ui 的实现

不好的实现

在接手的我的项目看到一个实现,先看应用形式

<FormTable :data="lessonPackageArr" :fleldsInfo="lessonPackageInfo" :maxColumn="3" label-width="120px">
  <template #presentedHours="{data}">
    <div class="flex-box between">
      <span>
        {{data.presentedHours}}
      </span>
      <span class="column-btn" @click="editPresentedHours(data)"> 批改 </span>
    </div>
  </template>
  <template #gifts="{data}">
    <div class="flex-box between">
      <span>
        {{data.gifts}}
      </span>
      <span class="column-btn" @click="editPresentedHours(data)"> 批改 </span>
    </div>
  </template>
</FormTable>

lessonPackageInfo 对象如下构造:

// 一个对象,用于配置题目列和题目列对应的字段
// type 指定值的类型,当初组件外部设置可能显示哪些类型的值了
// 对于服务其返回 1 0 须要显示 是否的数,提供一个 map_data 来映射
// column 属性设置跨列
// 须要自定义显示内容 提供 slot
lessonPackageInfo: {orderType: { type: 'option', desc: '课时包类别', map_data: { 1: '首单', 2: '续费', 5: '赠课'} },
    combo: {type: 'text', desc: '套餐名称'},
    presentedHours: {type: 'text', desc: '赠送课时', slot: true},
    price: {type: 'text', desc: '规范价格'},
    gifts: {type: 'text', desc: '赠送礼物', column: 3, slot: true},
  }
  1. props 不够直观,配置项多
  2. 不是齐全数据驱动

为何组件的配置项多不好?

对于这种需要很固定,组件的输出即 props 应该要最小化,组件性能要最大化,尽量给 props 提供默认值,这样能力进步团队的开发效率。

为何不是齐全的数据驱动不好?

这个组件不是齐全数据驱动的,须要自定义显示列是,须要编写模板。

如果须要自定义的列很多,就要写很多模板代码,想要再提取,只能再次封装组件,不提取,模板代码可能会收缩,你可能常常看到动辄 500 行一行的 template?而收缩的模板代码,让组件保护变得艰难,须要 template 和 js 代码之间来回切换。再者,减少一列自定义的数据,起码要批改两个中央。

为何须要齐全的数据驱动?

尽管有 slot 来扩大组件,然而咱们在写业务组件时候应该少用,而是尽量应用数据驱动模板。因为数据是 js 代码,当组件代码收缩时,很容易把 js 代码提取成独自的文件,而想要提取 slot 的代码,只能再封装组件。

三大前端框架的设计理念都是 数据驱动模板,这是它们区别于 jQuery 的重要特色,也是咱们封装业务组件时优先遵循的准则。

看了组件应用的问题,再看组件的代码:

<template>
  <div v-if="tableData.length" class="form-table">
    <div v-for="(data, _) in tableData" :key="_" class="table-border">
      <el-row v-for="(row, index) in rows" :key="index">
        <el-col v-for="(field, key) in row" :key="key" :span="getSpan(field.column)">
          <div v-if="(field.disabled && data[key]) || !field.disabled" class="column-content flex-box between">
            <div class="label" :style="'width:' + labelWidth">
              <span v-if="field.required" class="required">*</span>
              {{field.desc}}
            </div>
            <div class="text flex-item" :title="data[key]">
              <template v-if="key ==='minAge'">
                <span>{{data[key] }}</span>
                -
                <span>{{data['maxAge'] }}</span>
              </template>
              <template v-else-if="key ==='status'">
                <template v-if="field.statusList">
                  <span v-if="data[key] == 0" :class="field.statusList[2]">{{field.map_data[data[key]] }}</span>
                  <span v-else-if="data[key] == 10 || data[key] == 34" :class="field.statusList[1]">
                    {{field.map_data[data[key]] }}
                  </span>
                  <span v-else :class="field.statusList[0]">{{field.map_data[data[key]] }}</span>
                </template>
                <span v-else>{{field.map_data[data[key]] }}</span>
              </template>

              <slot v-else :name="key" v-bind:data="data">
                <TableColContent
                  :dataType="field.type"
                  :metaData="data[key]"
                  :mapData="field.map_data"
                  :text="field.text"
                />
              </slot>
            </div>
          </div>
        </el-col>
      </el-row>
    </div>
  </div>
  <div v-else class="form-table empty"> 暂无数据 </div>
</template>

<script>
  import TableColContent from '@/components/TableColContent'
  export default {
    name: 'FormTable',
    components: {TableColContent,},
    props: {
      // 数据
      data: {
        required: true,
        type: [Object, Array, null],
      },
      // 字段信息
      fleldsInfo: {
        required: true,
        type: Object,
        // className: {type: "text", desc: "班级名称", column: 3},
      },
      // 最多显示列数
      maxColumn: {
        required: false,
        type: Number,
        default: 2,
      },
      labelWidth: {
        required: false,
        type: String,
        default: '90px',
      },
    },
    data() {return {}
    },
    computed: {tableData() {if (!this.data) {return []
        }
        if (this.data instanceof Array) {return this.data} else {return [this.data]
        }
      },
      rows() {const returnArray = []
        let total = 0
        let item = {}
        for (const key in this.fleldsInfo) {const nextTotal = total + this.fleldsInfo[key].column || 1
          if (nextTotal > this.maxColumn) {returnArray.push(item)
            item = {}
            total = 0
          }
          total += this.fleldsInfo[key].column || 1
          item[key] = this.fleldsInfo[key]
          if (total === this.maxColumn) {returnArray.push(item)
            item = {}
            total = 0
          }
        }
        if (total) {returnArray.push(item)
        }
        return returnArray
      },
    },
    methods: {getSpan(column) {if (!column) {column = 1}
        return column * (24 / this.maxColumn)
      },
    },
  }
</script>

有哪些问题?

  1. 模板有太多的条件判断,不优雅
  2. 自定义显示列,还须要在引入 TableColContent,减少了组件复杂性

TableColContent 外部还是对配置项的 type 进行条件判断

局部代码

<span v-else-if="dataType ==='image'|| dataType ==='cropper'":class="className">
  <el-popover placement="right" title=""trigger="hover">
    <img :src="metaData" style="max-width: 600px;" />
    <img slot="reference" :src="metaData" :alt="metaData" width="44" class="column-pic" />
  </el-popover>
</span>

剖析完以上实现的问题,看看好的实现

好的实现

先看应用形式:

<template>
  <ZmFormTable :titleList="titleList" :data="data" />
</template>
<script>
  export default {
    name: 'Test',
    data() {
      return {data: {}, // 从服务器获取
        titleList: [{ title: '姓名', prop: 'name', span: 3},
          {
            title: '课堂作品',
            prop: (h, data) => {
              const img =
                (data.workPic && (
                  <ElImage
                    style='width: 100px; height: 100px;'
                    src={data.workPic}
                    preview-src-list={[data.workPic]}
                  ></ElImage>
                )) ||
                ''
              return img
            },
            span: 3,
          },
          {title: '作品点评', prop: 'workComment', span: 3},
        ],
      }
    },
  }
</script>

组件阐明:
titleList是组件的列配置,一个数组,元素 title 属性是题目,prop 指定从 data 里取值的字段,span 指定这列值逾越的行数。

prop 反对 string,还反对函数,这是实现自定义显示的形式,当这个函数很大时,可提取到独立的 js 文件中,也能够把整个 titleList 提取独自的 js 文件中。

参数 h 和 data 是如何传递进来的?或者 这函数在哪调用呢?

h 是 createElement 函数,data 是从组件外部的 data,和父组件传入的 data 是同一个值。

当一般函数的第一个参数是 h 是,它就是一个 render 函数。

这种形式应用起来简略多了。

看看外部实现

<template>
  <div class="form-table">
    <ul v-if="titleList.length">
      <!-- titleInfo 是通过转化的 titleList-->
      <li
        v-for="(item, index) in titleInfo"
        :key="index"
        :style="{width: ((item.span || 1) / titleNumPreRow) * 100 +'%'}"
      >
        <div class="form-table-title" :style="`width: ${titleWidth}px;`">
          <Container v-if="typeof item.title ==='function'":renderContainer="item.title":data="data" />
          <span v-else>
            {{item.title}}
          </span>
        </div>
        <div class="form-table-key" :style="`width:calc(100% - ${titleWidth}px);`">
          <Container v-if="typeof item.prop ==='function'":renderContainer="item.prop":data="data" />
          <span v-else>
            {{![null, void 0].includes(data[item.prop] && data[item.prop]) || '' }}
          </span>
        </div>
      </li>
    </ul>
    <div v-else class="form-table-no-data"> 暂无数据 </div>
  </div>
</template>

<script>
  import Container from './container.js'
  export default {
    name: 'FormTable',
    components: {Container,},
    props: {
      titleWidth: {
        type: Number,
        default: 120,
      },
      titleNumPreRow: {
        type: Number,
        default: 3,
        validator: value => {const validate = [1, 2, 3, 4, 5, 6].includes(value)
          if (!validate) {console.error('titleNumPreRow 示意一行有题目字段对, 只能时 1 -- 6 的偶数, 默认 3')
          }
          return validate
        },
      },
      titleList: {
        type: Array,
        default: () => {return []
        },
        validator: value => {
          const validate = value.every(item => {const { title, prop} = item
            return title && prop
          })
          if (!validate) {console.log('传入的 titleList 属性的元素必须蕴含 title  和 prop 属性')
          }
          return validate
        },
      },
      data: {
        type: Object,
        default: () => {return {}
        },
      },
    },
  }
</script>
<!-- 款式不是要害,省略 -->

实现自定义显示的形式,没有应用动静插槽,而是用一个函数组件Container,该组件接管一个 render 函数作为 prop。

export default {
  name: 'Container',
  functional: true,
  render(h, { props}) {return props.renderContainer(h, props.data)
  },
}

在 Container 外部调用 titleList 传入的函数。

装置 npm 体验

打包后有 2.8M,很大啊,预计没有人用,就不优化了。

总结

  1. 封装组件时优先思考数据驱动
  2. 一般函数的第一个参数是 h,就是渲染函数
  3. 可能有一些人不习惯写 JSX, 可兼容两种写法
退出移动版