弹性布局的一次奇妙探索,对布局的掌控力又增加一分

弹性布局(Flex/Grid)现在已经成为了日常开发布局的首选,而我在深入弹性布局的过程中,重构了系统的布局方案,误打误撞实现了苦恼几年的一个布局方案;

于是我开始深挖为什么会产生这个效果,以彻底掌握这个布局的实现,让其为我所有;

布局场景

在我们做后台管理系统中,经常会出现如下的布局场景:

顶部和左侧的没啥好说的,这次的体验出自中间这一块,做后台管理系统的大佬都知道,后台管理系统通常是要一屏显示的,滚动区域只能出现在我截图的中间这一块;

而中间这一块通常也就是我标注的这样,Search区域就是一个查询表单,Content区域就是一个Table表格,Footer区域就是一个分页器;

而为了用户体验分页器通常是希望可以固定在底部的,然后Table的高度响应式发生变化,我们想实现这个布局是非常简单的,如下:

效果是实现了,但是是不是感觉差了点什么?

其实是差一个表头固定,大多数第三方table组件都内置滚动然后固定表头,其实我们希望实现的效果是如下这样的:

其实实现这个效果非常简单,接下来就来看看具体怎么实现;

布局实现

通常我们实现像我上面提到的内容这样的布局可以使用Flex来实现,因为它就是一个流式布局,自上而下:

vue 复制代码
<template>
  <div class="container">
    <div class="search"></div>
    <div class="content"></div>
    <div class="footer"></div>
  </div>
</template>

<style>
  .container {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 10px;
    height: calc(100vh - 80px - 40px);
  }

  .search {
    min-height: 100px;
  }

  .content {
    flex-grow: 1;
    overflow: auto;
  }

  .footer {
    min-height: 50px;
  }

  .search,
  .content,
  .footer {
    display: flex;
    justify-content: center;
    align-items: center;
    background: azure;
    border-radius: 4px;
  }
</style>

像这样我们就实现了最开始第一张图中的主内容区域的布局了,这个时候我们只需要在content区域中写上Table组件并填充数据,就会看到图二的效果;

想要实现图三的效果,我们只需要在.content上面加上两个样就可以实现,如下:

css 复制代码
.content {
    flex-grow: 1;
    overflow: auto;

    display: flex;
    flex-direction: column;
}

我们只需要将.content变成一个弹性盒子,由于这里是Flex,所以还需要改变一下方向;

如果我们使用的是Grid处理方式就又不一样了,对于Grid我们需要使用如下代码:

css 复制代码
.content {
    flex-grow: 1;
    overflow: auto;

    display: grid;
    grid-template-rows: 100%;
}

.content > :first-child {
    height: 100%;
}

Grid的处理方式有点不同,要么显示不对第一个子元素的height设置任何值,包括unset都不行,要么就显示设置一些固定的值,例如%、px这种,也不能使用auto,或许后续会被修复,而Flex则没有这些限制,所以这里更推荐使用Flex布局去实现;

原理环节

当我第一次误打误撞实现这个效果之后,我寝食难安,因为没弄清楚为什么会产生这种结果,担心后续动了布局之后会出现别的bug;

于是我开始尝试将这个效果复现出来,就开始大量查阅资料,接下来就一一将这些原理进行揭秘;

首先在了解原理之前,我们需要弄明白两个东西:

  • 弹性容器(flex container):弹性容器指的是显示设置display的值为flex、inline-flex、grid、inline-grid的容器,在我们这个示例中,弹性容器指的就是.content
  • 弹性项(flex item):弹性项指的是弹性容器下的直系子元素,它们不需要我们做任何设置,只要是在弹性容器下的直系子元素都是弹性项

首先要想实现这个效果,我们需要在弹性容器上将overflow设置为auto这样才有效果;

在我最开始发现这个效果的时候,如果overflow设置为hidden,那么table自带的滚动条会丢失,高度被撑满;

目前只在Chrome下进行过测试,当前Chrome版本为120.0.6,现在overflow设置为hidden也可以实现这样的效果;

通常我们的高度计算,如果一个元素的heigh设置为百分比值,那么高度计算将依赖于这个元素的包含块;

而包含块是什么呢?在规范文档中是这样描述的,containing-block

A rectangle that forms the basis of sizing and positioning for the boxes associated with it. Notably, a containing block is not a box (it is a rectangle), however it is often derived from the dimensions of a box. Each box is given a position with respect to its containing block, but it is not confined by this containing block; it can overflow. The phrase "a box's containing block" means "the containing block in which the box lives," not the one it generates.

In general, the edges of a box act as the containing block for descendant boxes; we say that a box "establishes" the containing block for its descendants. If properties of a containing block are referenced, they reference the values on the box that generated the containing block. (For the initial containing block, values are taken from the root element unless otherwise specified.)

简单翻译如下,并不保证对:

它是一种矩形,它构成了与它相关的盒子的大小和定位,需要注意的是,包含块不是盒子(它是矩形),但是它通常是从盒子派生而来的,每个盒子都有一个相对于其包含块的位置,但它不受该包含块的限制,它可能会溢出,这句话"盒子的包含块"的意思是"盒子所在的包含块",而不是它的生成的。

通常,盒子的边缘充当盒子的包含块,我们说一个盒子为它的后代"建立"了包含块,如果参考了包含块的特性,则这些特性将参照生成包含块的盒子上的值。(对于初始的包含块,除非另有规定,否则值取自根元素)

通过这一段描述,我们可以明白为什么在一个dom上设置heigh: 100%会失效,例如有如下的结构:

html 复制代码
<div style="height: 100px">
    <div class="parent">
        <div class="child"></div>
    </div>
</div>

<style>
    .parent {
        
    }
    
    .child {
        height: 100%;
        background: azure;
    }
</style>

我们没有在.parent上显示设置任何高度,这样就导致.child的包含块(矩形,盒子(.parent)的边缘充当盒子(.child)的包含块)的高度计算是依赖于.child的内容高度;

那么.childheight: 100%就等于.child的内容高度,也等于.parent的内容高度;

如果.parent显示设置了高度,如果使用的是百分比值,那么.parent的高度计算就依赖于他的包含块,也就是第一个div,它设置了height: 100px,那么.parentheight: 100%就等于100px;

然后映射到.child上,这个时候.child的高度就也会是100px

了解了这个好像对我们上面的现象解释并不合理,因为elementtable组件下的滚动容器,不知道跨越了多少个标签了,讲道理它的高度计算如果不慢慢翻还真不知道是依赖谁;

不过没关系,我们继续规范文档,它里面有这样的一个规范min-percentage-contribution

Then, unless otherwise specified, when calculating the used sizes and positions of the containing block's contents:

  • If the cyclic dependency was introduced due to a block-axis size other than a minimum size on the containing block (i.e. a block-size or max-block-size in most layout modes, or a flex-basis in flex layout) that causes it to depend on the size of its contents, the box's percentage is not resolved and instead behaves as auto.

NOTE: Grid items and flex items do allow percentages to resolve in this case.

  • Otherwise, the percentage is resolved against the containing block's size. (The containing block's size is not re-resolved based on the resulting size of the box; the contents might thus overflow or underflow the containing block).

简单翻译如下:

除非另有规定,否则在计算包含块内容的使用大小和位置时:

  • 如果循环依赖是由于包含块上的块轴大小(即大多数布局模式中的块大小或最大块大小,或者在flex布局中的flex-basis)引起的,该大小使其依赖于其内容的大小,则该盒子的百分比不会被解析,而是表现为auto。

注意:grid item 和 flex item 确实允许在这种情况下解析百分比。

  • 否则,百分比将根据包含块的大小解析。(包含块的大小不会根据盒子的结果大小重新解析;因此,内容可能会溢出或不足包含块)。

描述很绕口,但是实际上很简单,直接上代码:

html 复制代码
<div class="flex-container">
    <div class="parent flex-item">
        <div class="child"></div>
    </div>
</div>

<style>
    .flex-container {
        display: flex;
        flex-direction: column;
        height: 100px;
        background: beige;
    }

    .parent {
        background: aliceblue;
        padding: 10px;
        box-sizing: border-box;
    }

    .parent.flex-item {
        flex-grow: 1;
        flex-basis: 50px;
    }

    .child {
        height: 100%;
        background: azure;
    }
</style>

由于视频转GIF会丢帧,同时会也失真,所以看着会不是很清楚,这里建议感兴趣的大佬复制代码,运行起来直接看效果。

可以看到非常有趣的现象,.child的高度可以随着.flex-item的高度变化而变化,在使用flex-growflex item进行伸缩的时候,.child百分比高度也会随之变化;

而直接使用flex-basis定义一个固定的高度,.child.child百分比高度计算也会依赖于flex-basis定义的高度;

这里还有一个非常有趣的现象,就是.child百分比高度计算虽然依赖于flex-basis的结果,但是直接关系是content-box的高度,哪怕我定义了box-sizing: border-box,所以包含块的边界应该是content-box开始计算的;

所以上面直接使用flex-basis: 50px的时候,.childheight: 100%最终渲染的结果是30px,因为减去上下padding: 10px的高度;

同理使用flex-grow进行伸缩的时候,flex item撑满了flex containerflex-basis计算的最终值这个时候是100px,.childheight: 100%最终渲染的结果是80px也是因为减去了padding: 10px的高度;

这一现象对应着上述规范的第一条,如果包含块的快轴(默认是高度)大小是由flex布局中的flex-basis引起的,grid itemflex item确实允许在这种情况下解析百分比;

那么:该大小使其依赖于其内容的大小,则该盒子的百分比不会被解析,而是表现为auto

这段话是什么意思呢?这个就用下面这张截图来解释:

可以看图中的效果,如果flex-basis的值不是一个确定的值,而是依赖于内容进行计算,那么该盒子的百分比不会被解析,而是表现为auto

所以这个时候.child百分比高度最终结果是auto而不会去计算具体一个值出来,auto的表现结果就是根据内容大小进行计算;

flex-basis的值不是一个确定的值,其实就是指代的所有的关键字,关键字可以去看文档:flex-basis#syntax

我截图出来的部分全都是根据内容进行计算的,所以这些关键字都会导致flex item百分比高度最终结果是auto,不过前提是该flex itemflex-grow值不为0

如果我们继续在加一层来看看会发生什么,来看下面这个截图:

上面这个示例就非常简单了,.child可以看做是.grandson包含块,所以.grandson的高度计算依赖于.child

.child的高度计算在上面已经说过了,由于我修改了一些样式,.flex-container的高度设置成了200px,又对.child添加了边框便于观察,所以.child的高度计算结果是178px

.grandson的高度计算依赖于.child,所以.grandson的高度计算结果是178px

.grandson的高度计算结果是178px,所以.grandsonheight: 100%最终渲染的结果是158px,由于我对.grandson添加了padding: 10px,所以最终渲染的结果是178px,最后会溢出容器(包含块);

这里的表现形式就已经和普通的盒子没什么区别了,到这里我们就将这一块的原理都研究清楚了,下面是上面截图的验证代码:

html 复制代码
<div class="flex-container">
    <div class="parent flex-item">
        <div class="child">
            <div class="grandson">grandson</div>
        </div>
    </div>
</div>

<style>
    .flex-container {
        display: flex;
        flex-direction: column;
        height: 200px;
        background: beige;
    }

    .parent {
        background: aliceblue;
        padding: 10px;
        box-sizing: border-box;
    }

    .parent.flex-item {
        flex-grow: 1;
        flex-basis: 100px;
        border: 1px solid #000;
    }

    .child {
        height: 100%;
        background: azure;
        padding: 10px;
        box-sizing: border-box;
        border: 1px solid #000;
    }

    .grandson {
        height: 100%;
        background: aquamarine;
        padding: 10px;
    }
</style>

深挖为什么能对 Element-plus 的 Table 组件生效

了解原理之后,我们就可以来看看为什么这个布局能对element-plustable组件生效了,并且可以自动计算出table的高度;

可以看到因为我们将el-table的包装的元素.content设置为弹性容器,这种行为导致el-table作为他的直系子元素,就自然而然的变成了flex item

我们再来看看el-table的样式,很明显他这里没有设置flex-growflex-basis,但是依然可以达到这个效果;

这是因为flex布局可以控制flex item的最终结果的属性除了上述两个以外,还有一个flex-shrink,默认情况下,flex-shrink的值是1

这也对应着初始值flex: 0 1 auto的结果,flex属性的第一个值对应着flex-grow,第二个值对应着flex-shrink,第三个值对应着flex-basis

虽然flex-growflex-basis按照默认值来说并不满足我们上面原理环节中提到的规则,但是原理环节并没有介绍到flex-shrink也可以起作用;

所以这里还要补上一点,如果flex itemflex-shrink值为0,那么该盒子的百分比不会被解析,而是表现为auto

完整的规则:如果flex itemflex-grow值为0,并且flex-shrink值为0,同时flex-basis的值由内容决定,那么该盒子的百分比不会被解析,而是表现为auto

flex-basis的值如果是auto(默认值就是auto),如果flex item显示设置了height属性,那么flex-basis的值会参考height属性;

可以理解为设置了height等同于设置了flex-basis的值,但是设置了flex-basis的值不等同于设置了height,因为flex-basis优先级更高;

接着看el-table下的直系子元素正好设置了height: 100%,也就完美的继承了el-table高度,并且可以响应式发生变化;

同时它也是一个flex container,这样它的直系子元素也都是flex item,而出现滚动区域的直系子元素也显示设置了flex: 1,非常契合我们最开始提到规则:

后续的一些布局就没必要看了,因为Element-plus有自己的滚动组件,滚动行为是由滚动组件控制的,这里我说的滚动区域并不是出现滚动条的地方,而且后续的一些元素全都显示设置了height: 100%

到这里我们也就挖完了这个布局为什么可以对Element-plusTable组件生效,所以说我是误打误撞实现这个布局,也是运气使然,正巧它将我需要的条件都完成了,我只需要将包装元素变成弹性盒子就好。

总结

这是一次非常有意思的探索,我们可以看到弹性布局的强大,弹性盒子有非常多的独立于普通盒子的规则,有时候会出现一些奇奇怪怪的情况不要着急,给它摸透那么它将成为你的致胜法宝;

同时也不要放弃任何一次探索的机会,如果不抓紧机会,或许下次想起来就已经失之交臂了,例如我这次正巧就是因为这个布局的需求,才让我有了这次的探索;

相关推荐
古木20192 分钟前
前端面试宝典
前端·面试·职场和发展
轻口味2 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王2 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发2 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀3 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪3 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef5 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6415 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻5 小时前
Vue(四)
前端·javascript·vue.js