起因
之前使用 Vue3.2 Naive UI 写了一套后台模版,其中 Naive UI 中 NGrid 组件使我非常感兴趣,类似于0:24 640:12 1280:8 这种使用比较简单的自适应栅格系统使用起来非常方便,那为什么不在 Vue2 常用的组件库Element 上也整一个呢。
效果展示
重点
监听尺寸变化
实际上并不用去在乎 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>
得到这样的结果。
而实际上如果将该组件作为一个基础组件,配合之前文章提到的 Form 组件,或者是 Detail 组件,都可以轻易的满足产品或者UI的要求,在对应的宽度下显示出更合理的效果。
结语
该想法比较简单,实现也很简单,也还没有遇到各种实际业务上的困难,如果有什么想法,请指正。