基于Vue2、Element开发的自适应栅格组件

514 阅读2分钟

起因

之前使用 Vue3.2 Naive UI 写了一套后台模版,其中 Naive UINGrid 组件使我非常感兴趣,类似于0:24 640:12 1280:8 这种使用比较简单的自适应栅格系统使用起来非常方便,那为什么不在 Vue2 常用的组件库Element 上也整一个呢。

效果展示

queryKey.gif

重点

监听尺寸变化

实际上并不用去在乎 NGrid 是如何实现的,有 0:24 640:12 1280:8 这样一个输入就已经足够了,所以我们的目标就很明确了。

首先,监听父元素的宽度变化,这里我注册了一个指令resize来监听元素的尺寸变化,使用的 ResizeObserver API来监听元素。

export default {}.install = (Vue, options = {}) => {
  Vue.directive('resize', {
    bind(el, binding) {
      const resizeObserber = new ResizeObserver(function(entries) {
        entries.forEach((item, index) => {
          binding.value({
            width: item.contentRect.width,
            height: item.contentRect.height
          });
        });
      });
      resizeObserber.observe(el);
      el.__resizeObserber__ = resizeObserber;
    },
    unbind(el) {
      el.__resizeObserber__.unobserve(el);
    }
  });
};

这样,我们就可以在想要监听的dom上通过v-resize触发对应的方法。

组件封装

既然是栅格系统,我们当然是选择 Element 中的 el-row el-col 组件,为了保持其一致的使用,我们也将我们的组件封装为Grid GridItem 组件。

Grid组件

Grid 组件仅需要对 el-row 组件进行封装,添加 v-resize 即可。

<div v-resize="onResize">
    <el-row v-bind="$attrs" v-on="$listeners">
          <slot></slot>
    </el-row>
</div>

为了保持 el-row 原有的属性,通过 $attrs$listeners 透传属性及方法。

onResize 方法接收到组件的 width 信息,通过 provide 向孙子组件传递,为了保持 width 的响应性,使用方法传递;

我们在使用的过程中,大部分情况下可能希望在父组件设置 span 之后,大部分子组件自动使用父组件的 span,额外的情况在单独对 GridItem 设置,那么我们在 provide 中新增一个 pSpan 属性,以供后代组件使用。

export default {
    provide() {
        return {
            pSpan: this.span,
            width: () => this.width
        }
    },
    props: {
        span: {
            type: [String, Number],
            default: () => '0:24 640:12 768:12 1280:8 1920:6'
        }
    },
    data() {
        return {
            width: ''
        }
    },
    methods: {
        onResize({ width }) {
            this.width = width;
        }
    }
}

GridItem组件

GridItem 组件主要负责对之前字符串的解析,依据 span 传入的值以及监听节点的 width 来计算出相应的 span 值,交给 el-col 渲染,以达到对应屏幕断点下的显示差异,其功能并不复杂。

<template>
  <el-col
    v-bind="$attrs"
    :span="handlerSpan"
    v-on="$listeners"
  >
    <slot></slot>
  </el-col>
</template>
<script>
export default {
  name: 'GridItem',
  props: {
    span: {
      type: [String, Number],
      default: () => ''
    }
  },
  inject: {
    pSpan: {
      default: () => 8
    },
    width: {
      default: () => 0
    }
  },
  computed: {
    handlerSpan() {
      if (!this.span && !isNaN(+this.pSpan)) {
        return +this.pSpan;
      }
      if (this.span && !isNaN(+this.span)) {
        return +this.span;
      }
      try {
        const spanMapArray = (this.span || this.pSpan).split(' ')
          .map((item) => {
            return item.split(':');
          })
          .sort((a, b) => b[0] - a[0])
          .find((item) => this.width() >= +item[0]);
        return +spanMapArray[1] || 8;
      }
      catch (e) {
        console.log('error');
      }
      return 8;
    }
  }
};
</script>

实现效果

最终的效果就如开头的图示,通过如下的代码

<div class="detail-demo bg-white p-small rounded-mini">
    <NxGrid>
        <NxGridItem>1</NxGridItem>
        <NxGridItem>2</NxGridItem>
        <NxGridItem>3</NxGridItem>
        <NxGridItem>4</NxGridItem>
        <NxGridItem>5</NxGridItem>
    </NxGrid>
</div>

得到这样的结果。

queryKey.gif

而实际上如果将该组件作为一个基础组件,配合之前文章提到的 Form 组件,或者是 Detail 组件,都可以轻易的满足产品或者UI的要求,在对应的宽度下显示出更合理的效果。

结语

该想法比较简单,实现也很简单,也还没有遇到各种实际业务上的困难,如果有什么想法,请指正。