vue中多数情况下使用template封装组件逻辑清晰结构简单。
但是在某些情况下,比如非常简单的组件 vue render简单解析或者template无法解决某些场景下,。使用完全javascirpt能力就显得尤为重要。

使用render场景

平时会对ElementUI中的部分组件。el-table等进行封装。以前于增加开发效率。对于el-table使用template 封装在多级表头的情况下。是无法对多级表头的body内容进行自定义模板的。因为需要递归二级及以上el-table-column。所以就造成封装组件会直接引用封装的递归组件。这样就造成具名插槽无法使用。这个情形下使用render 来解决就很nice~~

render简单了解

  • render 函数重要参数createElement
 render: function (createElement) {
    return createElement('h1', this.blogTitle)
 }

渲染结果:

 <h1>{{ blogTitle }}</h1>

createElement 了解

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一个 HTML 标签名、组件选项对象,或者
  // resolve 了上述任何一种的一个 async 函数。必填项。
  'div',

  // {Object}
  // 一个与模板中 attribute 对应的数据对象。可选。
  {
    // (详情见下一节)
  },

  // {String | Array}
  // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)

模板中 attribute 对应的数据对象

{
  // 与 `v-bind:class` 的 API 相同,
  // 接受一个字符串、对象或字符串和对象组成的数组
  'class': {
    foo: true,
    bar: false
  },
  // 与 `v-bind:style` 的 API 相同,
  // 接受一个字符串、对象,或对象组成的数组
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 普通的 HTML attribute
  attrs: {
    id: 'foo'
  },
  // 组件 prop
  props: {
    myProp: 'bar'
  },
  // DOM property
  domProps: {
    innerHTML: 'baz'
  },
  // 事件监听器在 `on` 内,
  // 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
  // 需要在处理函数中手动检查 keyCode。
  on: {
    click: this.clickHandler
  },
  // 仅用于组件,用于监听原生事件,而不是组件内部使用
  // `vm.$emit` 触发的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
  // 赋值,因为 Vue 已经自动为你进行了同步。
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 作用域插槽的格式为
  // { name: props => VNode | Array<VNode> }
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
  // 如果组件是其它组件的子组件,需为插槽指定名称
  slot: 'name-of-slot',
  // 其它特殊顶层 property
  key: 'myKey',
  ref: 'myRef',
  // 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
  // 那么 `$refs.myRef` 会变成一个数组。
  refInFor: true
}

render中操作$attrs$listener

el-table中为了便捷往往只需要写上部分需要的属性或者事件。其他的使用$attrs$listener来让开发人员自定义去使用

  • template写法
<div class="ele-table_container">
    <el-table
      :data="tableList"
      :max-height="tableHeight"
      :ref="tableRef"
      border
      fit
      stripe
      style="width: 100%"
      :sum-text="sumText"
      highlight-current-row
      v-bind="$attrs"
      v-on="$listeners"
    >
      // ...省略
    </el-table>
  • render 写法
render(h) {
    // ele-table的基础信息
    // 设置el-table options
    const tableOptions = {
      style: {
        width: "100%"
      },
      props: {
        ...this.$attrs,
        ...this.$props,
        data: this.tableList,
        cellClassName: "ele-table_cell",
        headerCellClassName: "ele-table_header--cell",
        border: true,
        fit: true,
        stripe: true,
        sumText: this.sumText,
        highlightCurrentRow: true
      },
      on: {
        ...this.$listeners
      },
      ref: this.tableRef
    };
    return h(
      "div",
      {
        class: "ele-table_container"
      },
      [
        h("el-table", tableOptions]
    );
  },

render 中 $slots、$scopedSlots、scopedSlots

$slots

场景一:一个为div。并包裹一个slot插槽的组件

  • template
<div>
  <slot></slot>
</div>
  • 使用render
 render(h){
   h("div",this.$slots.default)
 }

场景二:一个为div。并包裹一个slot名字为btn的具名插槽的组件,

  • template
<div>
  <slot name="btn"></slot>
</div>
  • 使用render
 render(h){
   h("div",this.$slots.btn)
 }
 

$scopedSlots

场景一:一个为div。并包裹一个slot名字为btn的具名插槽且包含数据的作用域插槽组件,

  • template
<div>
  <slot name="btn" :item="{text:'我是render slot'}"></slot>
</div>
  • 使用render
 render(h){
   h("div",this.$scopedSlots.btn({
	  item:{ text:'我是render slot' }
   }))
 }

场景二:一个为div。并包裹一个slot包含数据的作用域插槽组件,

  • template
<div>
  <slot :item="{text:'我是render slot'}"></slot>
</div>
  • 使用render
 render(h){
   h("div",this.$scopedSlots.default({
	  item:{ text:'我是render slot' }
   }))
 }

scopedSlots

有个组件A:

<div>
  <slot name="btn" :item="{text:'我是render slot'}"></slot>
</div>

现在需要对A进行封装为组件B 。

  • template
<div class="ele-b">
  <A #btn="{item}">
     <slot name="b" :item-b="{text:'这是B组件slot'}"></slot>
  </A>
</div>
  • render
 render(h){
   h("div",{
      class:"ele-b"
   },[
      h("A",{
         scopedSlots:props=>{
           return h('span',this.$scopedSlots.b({
              'item-b':'这是B组件slot'
           }))
         }
      })
   ])
 }

总结

$slot 一般用于生成非作用域的插槽
即组件中

<slot></slot><slot name="xxx"></slot>

$scopedSlots: 一般生成作用域的插槽

即组件中

<slot :xx="xxx"></slot><slot name="xxx" :xx="xxxx"></slot>

options中的scopedSlots一般用于使用组件,在slot内进行下一步操作

完整代码

<script>
import { summaryMethod, spanMethod } from "./utils.js";
import renderPage from "./we-table-page.js";
import renderColumnCheckBox from "./we-checkbox-column";
import { columnOptions, renderColumn } from "./we-table-column";
import commonCss from "@/styles/common.scss";
import Sortable from "sortablejs";
export default {
  name: "Table",
  props: {
    // 表头数据
    headers: {
      /**
       * prop:行数据字典
       * label:显示的表头
       * width:对应列宽
       * headerAlign:表头对齐方式
       * align:表数据对齐方式
       * fixed:是否固定列
       * sortable:是否排序
       * min-width:对应最小列宽
       */
      type: Array,
      default: () => {
        return [];
      }
    },
    // 表体数据
    list: {
      type: [Array, String],
      default: () => {
        return [];
      }
    },
    // 是否开启根据计算可视范围内,固定表头
    // FIXME 非window 改变无法监听!
    hasResize: {
      type: Boolean,
      default: false
    },
    // 是否增加拖动列.同时开启之后增加row-key.否则会出现渲染问题。
    dragDisabled: {
      type: Boolean,
      default: true
    },
    // 是否开启checkbox
    selection: {
      type: Boolean,
      default: false
    },
    // 仅对 type=selection 的列有效,类型为 Boolean,为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key)
    reserveSelection: {
      type: Boolean,
      default: false
    },
    // 表格ref。
    tableRef: {
      type: String,
      default: "we-table"
    },
    sumText: String,
    // 操作列固定
    btnFixed: {
      type: [Boolean, String],
      default: "right"
    },
    // 操作列名
    btnLabel: {
      type: String,
      default: "操作"
    },
    // 分页
    // 是否开启分页
    isPage: {
      type: Boolean,
      default: true
    },
    // 当前页数
    page: {
      type: Number,
      default: 1
    },
    // 每页显示条目个数
    rows: {
      type: Number,
      default: 15
    },
    // 总条目数
    total: {
      type: Number,
      default: 0
    },
    // 每页显示个数选择器的选项设置
    pagerCount: Number,
    // 是否开启多表头的合计。一般来说一级表头,为了性能不去开启。
    // TODO 为了性能后面直接提供模板?少量多级性能几乎忽略不计,是否合适待定。
    levelColumnsSummary: {
      type: Boolean,
      default: false
    },
    // 合并行设置
    spanOption: {
      type: Object,
      default: () => {
        return {
          spanIndex: [], // 合并列数。默认为空,为空则表示不合并
          spanKey: "id" // 数据合并唯一标识。默认以id作为合并标识
        };
      }
    }
  },
  computed: {
    tableList() {
      if (this.list == "success") {
        return [];
      } else {
        return this.list;
      }
    }
  },
  render(h) {
    // ele-table的基础信息
    // 设置el-table props
    const tableOptions = {
      style: {
        width: "100%"
      },
      props: {
        ...this.$attrs,
        ...this.$props,
        data: this.tableList,
        cellClassName: "ele-table_cell",
        headerCellClassName: "ele-table_header--cell",
        border: true,
        fit: true,
        stripe: true,
        sumText: this.sumText,
        highlightCurrentRow: true
      },
      on: {
        ...this.$listeners
      },
      ref: this.tableRef
    };
    return h(
      "div",
      {
        class: "ele-table_container"
      },
      [
        h("el-table", tableOptions, [
          renderColumnCheckBox.call(this, h),
          ...this.headers.map(item => {
            return h("el-table-column", columnOptions.call(this, h, item), [
              renderColumn.call(this, h, item)
            ]);
          })
        ]),
        renderPage.call(this, h)
      ]
    );
  },
  mounted() {
    if (this.hasResize) {
      this.resizeTable();
    }
    this.dragTable();
  },
  methods: {
    //  多级表头获取真实子节点数据
    getLevelColumns(columns) {
      let newColumns = [];
      columns.forEach(i => {
        if (i.child && i.child.length > 0) {
          newColumns = [...newColumns, ...this.getLevelColumns(i.child)];
        } else {
          newColumns.push(i);
        }
      });
      return newColumns;
    },
    resizeTable() {
      this.$nextTick(function () {
        const mainBodyPadding = commonCss.mainBodyPadding.split("p")[0];
        // 可视高度减去padding和分页组件的高度
        const tableDom = this.$refs[this.tableRef].$el;
        const parentDom = tableDom.parentNode.parentNode;
        this.tableHeight =
          window.innerHeight -
          this.$refs[this.tableRef].$el.offsetTop -
          (Number(mainBodyPadding) + 45);
        let _this = this;
        parentDom.onresize = function (e) {
          console.log(e);
          _this.tableHeight =
            window.innerHeight -
            _this.$refs[_this.tableRef].$el.offsetTop -
            (Number(mainBodyPadding) + 45);
        };
      });
    },
    dragTable() {
      const el = this.$refs[this.tableRef].$el.querySelectorAll(
        ".el-table__body-wrapper > table > tbody"
      )[0];
      this.sortable = Sortable.create(el, {
        disabled: this.dragDisabled,
        setData: function (dataTransfer) {
          dataTransfer.setData("Text", "");
        },
        onEnd: ({ newIndex, oldIndex }) => {
          const currRow = this.list.splice(oldIndex, 1)[0];
          this.list.splice(newIndex, 0, currRow);
          // 对于某些情况,需要拖拽后的时候,则同时影响父组件list数据
          this.$emit("update:list", this.list);
        }
      });
    },
    handleSizeChange(val) {
      this.$emit("update:rows", val);
      this.$emit("page-rows-change");
    },
    handlePageChange(val) {
      this.$emit("update:page", val);
      this.$emit("page-rows-change");
    },
    summaryMethod(param) {
      // 合计行
      return summaryMethod(
        param,
        this.headers,
        this.sumText,
        this.levelColumns
      );
    },
    spanMethod(param) {
      if (this.spanOption.spanIndex.length > 0) {
        return spanMethod(param, this.spanOption, this.list);
      }
    },
    /**
     * @description: 用于多选表格,切换某一行的选中状态,如果使用了第二个参数,则是设置这一行选中与否
     * @param {Number} row 行数据
     * @param {Boolean} selectType 是否勾选
     */
    toggleRowSelection(row, isSelected = true) {
      this.$refs[this.tableRef].toggleRowSelection(row, isSelected);
    },
    /**
     * @description 用于多选表格,清空用户的选择
     */
    clearSelection() {
      this.$refs[this.tableRef].clearSelection();
    },
    /**
     * @param {Number} row 行数据
     * @param {*} index
     * @description 用于处理在type selection时,是否可选。所以返回的list数据要清洗数据添加isChecked字段
     */
    selectable(row) {
      return row.isChecked == undefined || row.isChecked == true ? true : false;
    }
  }
};
</script>

使用

    <Table
          ref="cTable"
          :is-page="false"
          :headers="detailHeaders"
          :list="editTable"
          :page.sync="page"
          :rows.sync="rows"
          @page-rows-change="pageChange"
          @selection-change="selectionChange"
        >
          <template #productCode="{ row }">
            <el-input v-model="row.productCode"></el-input>
          </template>
          <template #bdemandQty="{ row }">
            <el-input v-model="row.bdemandQty"></el-input>
          </template>
          <template #sdemandQty="{ row }">
            <el-input v-model="row.sdemandQty"></el-input>
          </template>
          <template #bpric="{ row }">
            <el-input v-model="row.bprice"></el-input>
          </template>
          <template #sprice="{ row }">
            <el-input v-model="row.sprice"></el-input>
          </template>
          <template #zp="{ row }">
            <el-checkbox v-model="row.zp"></el-checkbox>
          </template>
        </Table>
        export default{
          data(){
            return {
                  detailHeaders: [
        {
          label: "产品编码",
          prop: "productCode",
          width: 180
        },
        {
          label: "产品名称",
          prop: "productName"
        },
        {
          label: "条码",
          prop: "barCode"
        },
        {
          label: "需求数量",
          prop: "bdemandQty",
          child: [
            {
              label: "大包装",
              prop: "bdemandQty"
            },
            {
              label: "小包装",
              prop: "sdemandQty"
            }
          ]
        },
        {
          label: "销售数量",
          prop: "borderedQty",
          child: [
            {
              label: "大包装",
              prop: "borderedQty"
            },
            {
              label: "小包装",
              prop: "sorderedQty"
            }
          ]
        },
        {
          label: "销售价格",
          prop: "bprice",
          child: [
            {
              label: "大包装",
              prop: "bprice"
            },
            {
              label: "小包装",
              prop: "sprice"
            }
          ]
        },
        {
          label: "可供量",
          prop: "usableBigQty",
          child: [
            {
              label: "大包装",
              prop: "usableBigQty"
            },
            {
              label: "小包装",
              prop: "usableSmallQty"
            }
          ]
        },
        {
          label: "金额",
          prop: "amount"
        },
        {
          label: "计量单位",
          prop: "bigUom",
          child: [
            {
              label: "大",
              prop: "bigUom"
            },
            {
              label: "小",
              prop: "smallUom"
            }
          ]
        },
        {
          label: "换算",
          prop: "convert"
        },
        {
          label: "赠品",
          prop: "zp"
        }
      ],
            }
          }
        }

效果

在这里插入图片描述

千里之行
始于足下

Logo

前往低代码交流专区

更多推荐