阅读视图

发现新文章,点击刷新页面。

【Fantastic-admin 技术揭秘】妙用Vue动态组件,让客制化更容易

《Fantastic-admin 技术揭秘》系列将带你了解 Fantastic-admin 这款框架各种功能的设计与实现。通过了解这些技术细节,你不光可以更轻松地使用 Fantastic-admin 这款框架,也可以在其他项目中使用这些技术。

你可以点击 这里 查看本系列的所有文章,也欢迎你在评论区留言告诉我你感兴趣的内容,或许下一篇文章就会带你揭秘其中的奥秘。

需求分析

作为一款面向开发者的后台系统框架,一直在尽可能满足各种业务场景的需求,但也确实没办法做到将各种业务需求都集成进去。既然很多功能框架无法直接满足,那就得思考如何降低客制化的成本,让开发者可以更轻松的进行扩展。

image.png

比如有这样一个需求,需要在左侧导航区域 Logo 下方增加一个组织切换的按钮,大部分开源的后台系统的解决方案就是找到导航对应的文件 → 阅读源码 → 编写业务代码 → 测试功能,这就涉及到了几个难点:

  • 需要阅读并修改系统源码。对开发者能力有要求,不求能看懂源码,但至少不能改出bug
  • 系统功能和业务功能的代码耦合。维护成本增加,后续维护的人也得重复第1点,还得区分出哪些是业务代码
  • 系统稳定性。任何直接对系统源码的修改,都无法100%保证绝对没问题,除非你完全熟悉系统源码

实现方案

使用Vue的动态组件,加上Vue插槽的设计理念,就可以很巧妙的解决这个问题。

只需在对应位置提供预留的“插槽”(这里的“插槽”实际上是Vue动态组件),然后按照约定的文件名去创建组件,然后在组件内就可以写业务代码,这个组件就会出现在对应的“插槽”位置上。

核心代码如下:

// src/slots/index.ts

import { pascalCase } from 'scule'

type Slots =
  'header-start' | 'header-after-logo' | 'header-after-menu' | 'header-end' |
  'main-sidebar-top' | 'main-sidebar-after-logo' | 'main-sidebar-after-menu' | 'main-sidebar-bottom' |
  'sub-sidebar-top' | 'sub-sidebar-after-logo' | 'sub-sidebar-after-menu' | 'sub-sidebar-bottom' |
  'tabbar-start' | 'tabbar-end' |
  'toolbar-start' | 'toolbar-end' |
  'free-position'

function tryLoadComponent(name: Slots) {
  const componentMap = import.meta.glob('./*/index.vue', { eager: true })
  const path = `./${pascalCase(name as unknown as string)}/index.vue`
  const component = componentMap[path as keyof typeof componentMap]
  if (!component) {
    return {
      default: defineComponent({
        name: 'SlotsInvalidComponent',
        render: () => null,
      }),
    }
  }
  return component
}

export function useSlots(name: Slots) {
  const component = tryLoadComponent(name)
  return defineComponent((component as any).default)
}

然后在需要的地方使用:

<component :is="useSlots('main-sidebar-after-logo')" />

这样一个基于插槽理念的动态组件就完成了,当需要使用这个插槽的时候,只需要创建 src/slots/MainSidebarAfterLogo/index.vue 文件即可。

Fantastic-admin 中,提供了许多这样的插槽,比如:

实现原理

阅读上面的核心代码,就可以发现其实现原理并不复杂,主要就是通过 import.meta.glob 动态导入 Vue 组件,然后通过 useSlots 函数返回组件。

同时需要注意下,当组件文件不存在时,会返回一个空的组件,这样不会影响到布局。

聊一下CSS中的标准流,浮动流,文本流,文档流

在网络上关于CSS的文章中,有时候能听到“标准流”,“浮动流”,“定位流”等等词语,还有像“文档流”,“文本流”等词,这些流是什么意思?它们是CSS中的一些布局方案和特性。今天我们就来聊一下CSS中的这些流。这篇文章将重点详细描述浮动流。

简述

  • 文档流,普通流,标准流,常规流等:这么多名词实际上指的都是文档流,即元素在HTML中的位置顺序,决定了它在页面中的位置顺序。分为块级元素和行内元素两种。
  • 文本流:文本流指的文档中元素(例如字符)的位置顺序,即从左到右,从上到下的顺序形式。
  • 浮动流:浮动流是使用CSS浮动属性作为布局方式。
  • 定位流:定位流是使用CSS定位属性作为布局方式。

看了简述还是不清楚各种流的区别与关联,比如文档流和文本流看起来差不多,究竟有什么不同?CSS浮动和定位为什么要多加一个“流”字?下面我们一一解答下。

预置CSS

下面的文档中会出现大量的重复CSS代码,这里提前进行声明。后面的所有示例都预先加载了这部分CSS代码。

.left { /* 左浮动 */
  float: left;
}
.right { /* 右浮动 */
  float: right;
}
.red { /* 红 */
  background: red;
}
.yellow { /* 黄 */
  background: yellow;
}
.green { /* 绿 */
  background: green;
}
.blue { /* 蓝 */
  background: blue;
}
.gray { /* 灰 */
  background: gray;
}
.pink { /* 粉 */
  background: pink;
}
.brown { /* 棕 */
  background: brown;
}
.maroon { /* 褐色 */
  background: maroon;
}
.purple { /* 紫色 */
  background: purple;
}
.fuchsia { /* 紫红 */
  background: fuchsia;
}
.lime { /* 黄绿 */
  background: lime;
}
.olive { /* 橄榄绿 */
  background: olive;
}
.navy { /* 藏青 */
  background: navy;
}
.teal { /* 青 */
  background: teal;
}
.aqua { /* 水绿 */
  background: aqua;
}
.orange { /* 橙 */
  background: orange;
}
.clear-left {
  clear: left;
}
.clear-right {
  clear: right;
}
.clear-both {
  clear: both;
}

文档流

文档流又叫做普通流,标准流,常规流等等,它的英文名是“normal flow”,是HTML默认的布局形式。在未指定使用其它布局时,使用的就是文档流的布局。在文档流中分为两种元素:块级元素和行内元素。

块级元素:常见的块级元素有div和p标签等。块级元素会默认占满横向全部宽度,在文档流中从上到下垂直排列。块级元素的左边缘与父元素的左边缘是重合的。

行内元素:常见的行内元素有span和a标签等。行内元素在文档流中的宽度为实际内容的宽度,在水平方向从左到右排列。

<html>
  <body>
    <div class="border div-common"></div>
    <div class="border div-common div-width"></div>
    <div class="border div-common div-width"></div>
    你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS!
    我是JZ。你好CSS! 我是JZ。
    <span class="border">
      我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS!
    </span>
  </body>
  <style>
    .border { border: 1px solid red; }
    .div-common { height: 50px; }
    .div-width { width: 100px; }
  </style>
</html>

float-1.png

通过例子可以看到,每个块级元素独占一行,从上到下排列。在未设置宽度时,默认占满横向全部宽度;即使设置了宽度且剩余空间足够,也是独占一行。行内元素则从左到右排列,如果一行不够,则从下一行左边开始继续。

文本流

文本流是指的文本字符从左到右,从上到下的输出顺序。只看说明,感觉文本流和文档流看起来像是一种东西,但事实上是不一样的,我们看一个例子。

<html>
  <body>
    <div class="container">
      <div class="div-common"></div>
      你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS!
      我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。
    </div>
    <div class="container">
      <div class="div-common left"></div>
      你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS!
      我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。
    </div>
    <div class="container">
      <div class="div-common div-pos"></div>
      你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS!
      我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。
    </div>
  </body>
  <style>
    .container { margin-bottom: 50px; }
    .div-common {
      height: 50px;
      width: 100px;
      border: 1px solid red;
    }
    .left { float: left; }
    .div-pos { position: absolute; }
  </style>
</html>

float-2.png

这里列举了三种情况(例子中包含浮动流和定位流,我们后面会单独介绍):

  1. 第一种情况就是正常的文档流,块级元素单独占一行,字符文本也单独一行从左到右排列。
  2. 我们对块级元素设置了左浮动。下面的字符文本跑到同一行展示了,因此浮动脱离了文档流。但是字符文本没有覆盖到块级元素上面,因此没有脱离文本流。
  3. 我们对块级元素设置了绝对定位。可以看到下面的字符文本不止跑到同一行展示了,还出现在块级元素的下面,因此脱离了文档流,也脱离了文本流。

CSS的float属性

关于浮动流,首先来描述一下CSS的float属性。float属性即是控制浮动流的主要属性,一共有三个值。向哪个方向浮动,即是将那个元素放到其容器的哪一侧。

  • left: 左浮动
  • right: 右浮动
  • none: 不浮动

基本特性

<html>
  <body>
    <div class="div-common">
      你好CSS! 我是JZ。你好CSS! 我是JZ。你好
      <img class="img-common" src="1.jpg" />
      <span>
        CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。
      </span>
    </div>
    <div class="div-common">
      你好CSS! 我是JZ。你好CSS! 我是JZ。你好
      <img class="img-common left" src="1.jpg" />
      CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。
    </div>
    <div class="div-common">
      你好CSS! 我是JZ。你好CSS! 我是JZ。你好
      <img class="img-common right" src="1.jpg" />
      CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。
    </div>
  </body>
  <style>
    .div-common { margin-bottom: 40px; }
    .img-common {
      width: 40px;
      height: 40px;
    }
  </style>
</html>

float-3.png

文本中间有一个图片元素,在不设置浮动时,图片在文本的中间,在设置了左或右浮动后,图片到了左侧或者右侧。还可以看到,原本图片是占一行,但设置了浮动后,实现了文字环绕图片展示。

<html>
  <body>
    <div class="div-common">
      你好CSS! 我是JZ。你好CSS! 我是JZ。你好
      <img class="img-common left" src="1.jpg" />
      <img class="img-common left" src="1.jpg" />
      <img class="img-common left" src="1.jpg" />
      CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。
    </div>
    <div class="div-common">
      你好CSS! 我是JZ。你好CSS! 我是JZ。你好
      <img class="img-common right" src="1.jpg" />
      <img class="img-common right" src="1.jpg" />
      <img class="img-common left" src="1.jpg" />
      CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。你好CSS! 我是JZ。
    </div>
  </body>
  <style>
    .div-common {
      margin-bottom: 40px;
    }
    .img-common {
      width: 40px;
      height: 40px;
    }
  </style>
</html>

float-4.png

如例子,可以将多个元素设置浮动,浮动到同一侧的元素会并排放置,即碰到另一个浮动的元素就停止。

块级元素浮动

不止行内元素,块级元素实际上也是可以浮动的。我们举例看一下:

<html>
  <body>
    <div class="div-common" style="height: 140px">
      <div class="size-common">1</div>
      <div class="size-common">2</div>
      <div class="size-common">3</div>
    </div>
    <div class="div-common">
      <div class="size-common left">1</div>
      <div class="size-common left">2</div>
      <div class="size-common right">3</div>
    </div>
    <div class="div-common">
      CSS! 我是JZ。你好CSS! 我是JZ。
      <div class="size-common left">1</div>
      <div class="size-common left">2</div>
      <div class="size-common right">3</div>
      <div class="size-common right">4</div>
    </div>
  </body>
  <style>
    .div-common {
      height: 60px;
      padding-top: 10px;
      border: 1px dotted blue; 
    }
    .size-common {
      width: 40px;
      height: 40px;
      border: 1px solid red; 
    }
  </style>
</html>

float-5.png

从例子中可以看到,当未设置浮动时,块级元素根据文档流的特点,从上到下排列。当设置浮动之后,块级元素聚到了一行,左右浮动排列。如果同时存在未浮动的行内元素,则行内元素在中间展示。

display属性变化

其实不仅如此,原本的行内元素在设置了浮动后,就变成了块级元素。即float属性会修改display属性的计算值(图源MDN):

float-6.png

<html>
  <body>
    <div class="div-common">
      CSS! 我是JZ。你好CSS! 我是JZ。
      <div class="size-common left">1</div>
      <img class="size-common left" src="1.jpg" />
      <span class="left">左浮动</span>
      <div class="size-common left">2</div>
      <div class="size-common right">3</div>
    </div>
  </body>
  <style>
    .div-common {
      height: 60px;
      padding-top: 10px;
      border: 1px dotted blue; 
    }
    .size-common {
      width: 40px;
      height: 40px;
      border: 1px solid red; 
    }
  </style>
</html>

float-7.png

可以看到span元素的display原本是inline,但设置了浮动之后,计算值就变为block了,img元素的现象也是一样的。

浮动流与块级元素

上面仅仅描述了浮动的基本特点,其实浮动的特点还有很多。我们从浮动流与块级元素的角度,再看看浮动有什么其它特点。

浮动流与块级文档流

首先来看一下纯块级元素在浮动流中的表现。

<html>
  <body>
    <div class="div-common">
      <div class="one"></div>
      <div class="two"></div>
      <div class="three"></div>
      <div class="four"></div>
    </div>
    <div class="div-common">
      <div class="one left"></div>
      <div class="two left"></div>
      <div class="three right"></div>
      <div class="four right"></div>
    </div>
    <div class="div-common" style="margin-top: 80px">
      <div class="one"></div>
      <div class="two left"></div>
      <div class="three"></div>
      <div class="four"></div>
    </div>
    <div class="div-common">
      <div class="one"></div>
      <div class="two right"></div>
      <div class="three"></div>
      <div class="four"></div>
    </div>
    <div class="div-common">
      <div class="one"></div>
      <div class="two left"></div>
      <div class="three left"></div>
      <div class="four"></div>
    </div>
    <div class="div-common" style="margin-top: 20px">
      <div class="one"></div>
      <div class="two right"></div>
      <div class="three right"></div>
      <div class="four"></div>
    </div>
  </body>
  <style>
    .div-common {
      margin-bottom: 10px;
      border: 1px dotted blue;
    }
    .one {
      background: red;
      height: 20px;
      width: 30px;
    }
    .two {
      background: yellow;
      height: 50px;
      width: 40px;
    }
    .three {
      background: green;
      height: 30px;
      width: 50px;
    }
    .four {
      background: blue;
      height: 40px;
      width: 60px;
    }
  </style>
</html>

float-8.png

这里有按先后次序放置的四个元素,分别是第一个红,第二个黄,第三个绿,第四个蓝色。蓝色虚线指的是外部容器的框。我们看看它们在不同场景下的表现:

  • 第一行:未设置浮动,四个元素按照文档流从上到下展示。
  • 第二行:四个元素全部设置浮动,四个元素排成了一行。在子元素全部为浮动时,父级元素的高度会变为0,子元素无法撑起父元素的高度,造成塌陷。具体的解决方案我们会在后面章节描述。
  • 第三行:第一个红元素是正常文档流,第二个黄元素设置了左浮动;因此第二个元素的位置是在第一个元素下方。由于黄元素是浮动不占文档流位置,因此第三个绿元素和第四个蓝元素从上到下依次排列。
  • 第四行: 在第三行的基础上,把第二个黄元素设置为右浮动,不挡住其它元素,可以看到非浮动的1,3,4元素组成了一个正常的文档流。
  • 第五行: 第一个元素是正常文档流。第二个黄,第三个绿元素设置了左浮动,因此在第一个元素下方横向排列。第四个元素未浮动,因此与第一个元素一起组成正常文档流。
  • 第六行: 在第三行的基础上,把第二个黄,第三个绿元素设置为右浮动,不挡住其它元素。

通过这几个例子,可以看到浮动与文档流的关系。浮动元素前面如果有块级元素,那么浮动元素会在块级元素的下方。但是浮动元素本身脱离了文档流,因此不占空间,下方的非浮动块级元素可能会被浮动元素盖住。所有非浮动元素会形成一个文档流。

块级元素与浮动超过一行(单侧)

当浮动的块级元素超过一行时,会发生什么现象呢?我们来看一下单侧浮动的例子。

<html>
  <body>
    <div class="div-common">
      <div class="one left"></div>
      <div class="three left"></div>
      <div class="four left"></div>
      <div class="two left"></div>
      <div class="three left"></div>
      <div class="two left"></div>
    </div>
    <div class="div-common" style="margin-top: 110px">
      <div class="one left"></div>
      <div class="two left"></div>
      <div class="three left"></div>
      <div class="four left" style="width: 100px"></div>
    </div>
    <div class="div-common" style="margin-top: 80px">
      <div class="one left"></div>
      <div class="two left"></div>
      <div class="three left"></div>
      <div class="four left"></div>
    </div>
    <div class="div-common" style="margin-top: 100px">
      <div class="one left" style="height: 100px"></div>
      <div class="two left"></div>
      <div class="three left"></div>
      <div class="four left"></div>
    </div>
    <div class="div-common" style="margin-top: 120px">
      <div class="one left"></div>
      <div class="two left"></div>
      <div class="three left" style="height: 50px"></div>
      <div class="four left"></div>
    </div>

    <div class="div-common" style="margin-top: 120px">
      <div class="one left" style="height: 100px"></div>
      <div class="two left" ></div>
      <div class="three left" style="height: 50px"></div>
      <div class="four left"></div>
    </div>
    <div class="div-common" style="margin-top: 120px">
      <div class="one left" style="height: 100px"></div>
      <div class="two left" ></div>
      <div class="three left"></div>
      <div class="four left"></div>
      <div class="two left" style="height: 10px"></div>
    </div>
    <div class="div-common" style="margin-top: 120px">
      <div class="one left"></div>
      <div class="two left"></div>
      <div class="three left"></div>
      <div class="four left"></div>
      <div class="one left"></div>
      <div class="two left"></div>
      <div class="three left"></div>
      <div class="four left"></div>
    </div>
    <div class="div-common" style="margin-top: 150px">
      <div class="one left"></div>
      <div class="two left"></div>
      <div class="three left"></div>
      <div class="one left"></div>
      <div class="two left"></div>
      <div class="one left"></div>
      <div class="one left"></div>
      <div class="two left"></div>
      <div class="two left"></div>
    </div>
  </body>
  <style>
    .div-common {
      margin-bottom: 10px;
      border: 1px dotted blue;
    }
    .one {
      background: red;
      height: 20px;
      width: 90px;
    }
    .two {
      background: yellow;
      height: 50px;
      width: 120px;
    }
    .three {
      background: green;
      height: 40px;
      width: 150px;
    }
    .four {
      background: blue;
      height: 40px;
      width: 180px;
    }
  </style>
</html>

float-9.png

这些例子中为了更容易换行,元素比较宽,且数量比较多,每一个元素都设置的左浮动。例子比较复杂,我们一个一个来分析:

  • 第一个例子:正常浮动,超过一行之后,从第二行左边继续开始浮动。且第二行的垂直位置是前一行最低的位置。
  • 第二个例子:第三个绿元素的高度比第二个矮一点,第四个元素与第三个高度一样,且一行可以完整放置,因此横向排列。
  • 第三个例子:与第二个例子类似,但是第四个蓝元素更宽,导致水平一行肯定放不开。但是注意第四个蓝元素并没有去开一个新行,而是在前一个绿元素下方继续放置。
  • 第四个例子:与第三个例子类似,但是第一个红元素高度很高,能纵向同时容纳第二个黄元素与第三个绿元素。但因为水平有空间,因此第三个绿元素并没有纵向放置。第四个蓝元素虽然水平一行肯定放不开,而且前面的红元素右侧还有大片空闲区域,但是依旧放置在第三个绿元素下方。
  • 第五个例子:与第三个例子类似,但是第二个黄元素与第三个绿元素高度相同。因此第三个绿元素下方没有空闲区域了,因此第四个绿元素只好从最左侧开始新的一行浮动了。
  • 第六个例子:第一个红元素非常高,第二三个元素高度相同,且第四个蓝元素太宽,无法放置在第一行。由于前一个第二三个元素高度相同,因此第四个蓝元素无法放置在第三个下方,因此它向前寻找,找到了第一个元素下方还有位置。
  • 第七个例子:与第六个例子类似,但第四个蓝元素可以放置在第三个元素下方,同时增加了第五个黄元素。第五个黄元素太宽,无法放置在第四个绿元素右侧。第五个黄元素同时高度非常低,三四五元素加起来都比红元素高度低,因此第五个黄元素向前寻找位置,最终在第一个红元素右侧放置。注意看虽然第五个黄元素上方还有空位可以容纳它自己,但是它不在前一个元素右侧的时候,它的位置纵向需要在前一个元素下方。
  • 第八个例子:第四个蓝元素在第三个绿元素下方放置,且依旧属于浮动的第一行。第五个红元素的水平位置和第四个蓝元素对齐,虽然上方还有位置可以完整放置红元素,但红元素还是没有过去。同样的第二行的第一个黄元素上方有位置,但纵向还是从上一行最低的位置下方开始。
  • 第九个例子:上面部分示例的综合场景。

从上面的例子可以看到浮动在换行场景时的一些规律:

  1. 如果一行后方有位置,那么优先水平放置。
  2. 如果一行后方没有位置,且前一个元素下方有“空位”,就优先放置在前一个元素下方。如果下方没有空位,但是更前的元素下方有空位,这个元素会向前寻找。
  3. 后一个元素的位置如果在前一个元素右侧,那么纵向位置可以水平对齐。如果后一个元素的位置如果在前一个元素左侧,那么那么纵向位置必须在前一个元素最低位置的下方。
  4. 如果实在找不到位置,那就开启新的一行浮动。新一行浮动的纵向位置开始于前一行所有元素的最下方。

块级元素与浮动超过一行(双侧)

可以看到,在单侧浮动的元素排列就已经比较复杂了,如果左侧和右侧同时出现浮动,且超过一行,又会出现怎样的现象呢?首先看个简单的例子。

<html>
  <body>
    <div class="div-common">
      <div class="div1 left red"></div>
      <div class="div1 left yellow"></div>
      <div class="div1 right green"></div>
      <div class="div1 right blue"></div>
      <div class="div1 left gray"></div>
      <div class="div1 right pink"></div>
    </div>
    <div class="div-common" style="margin-top: 110px">
      <div class="div1 left red"></div>
      <div class="div1 left yellow"></div>
      <div class="div1 left gray"></div>
      <div class="div1 right pink"></div>
      <div class="div1 right green"></div>
      <div class="div1 right blue"></div>
    </div>
    <div class="div-common" style="margin-top: 110px">
      <div class="div1 left red"></div>
      <div class="div1 left yellow"></div>
      <div class="div1 left gray"></div>
      <div class="div1 right pink"></div>
      <div class="div1 left green"></div>
      <div class="div5 right blue"></div>
    </div>
    <div class="div-common" style="margin-top: 110px">
      <div class="div3 left red"></div>
      <div class="div2 left yellow"></div>
      <div class="div2 left gray"></div>
      <div class="div1 right pink"></div>
      <div class="div1 left green"></div>
      <div class="div2 right blue"></div>
    </div>
    <div class="div-common" style="margin-top: 120px">
      <div class="div3 left red"></div>
      <div class="div2 left yellow"></div>
      <div class="div2 left gray"></div>
      <div class="div1 right pink"></div>
      <div class="div1 left green"></div>
      <div class="div2 right blue"></div>
      <div class="div2 left pink"></div>
      <div class="div2 left gray"></div>
      <div class="div2 right yellow"></div>
      <div class="div2 left green"></div>
    </div>
  </body>
  <style>
    .div-common {
      margin-bottom: 10px;
      border: 1px dotted blue;
    }
    .div1 {
      height: 40px;
      width: 100px;
    }
    .div2 {
      height: 40px;
      width: 150px;
    }
    .div3 {
      height: 100px;
      width: 50px;
    }
    .div4 {
      height: 200px;
      width: 50px;
    }
    .div5 {
      height: 40px;
      width: 50px;
    }
  </style>
</html>

float-10.png

  • 首先看第一与第二个例子:同样的6个元素,只不过顺序和左右浮动不同。这里可以看到换行的规律:按照元素在HTML中出现的顺序在页面中排列,如果第一行无法容纳下一个元素,那么就从第二行开始继续浮动。
  • 第三个例子在的第五个绿元素因为第一行位置不够,因此开启了第二行左浮动。第六个蓝元素是右浮动,虽然第一行有位置可以放置,但由于上一个元素已经开启了第二行(虽然是左浮动),因此也在第二行展示了。
  • 然后是第四个例子,第一个红元素高度非常高,因此其余的左右浮动元素都在它的右侧排列,在内部换行,但是都属于大的第一行。再看第五个例子,第一个红元素的右侧足够放置三行元素的排列。尤其看第三行,最上面的部分高度在红元素内,但是下面的高度已经超过红元素了。再看最后一个绿元素,当右侧位置不够时,终于开启了大的第二行浮动。
<html>
  <body>
    <div class="div-common">
      <div class="div1 left red"></div>
      <div class="div4 right yellow"></div>
      <div class="div2 left gray"></div>
      <div class="div3 right pink"></div>
      <div class="div2 left green"></div>
    </div>
    <div class="div-common" style="margin-top: 150px">
      <div class="div1 left red"></div>
      <div class="div4 right yellow"></div>
      <div class="div2 left gray"></div>
      <div class="div3 right pink"></div>
      <div class="div2 right green"></div>
    </div>
    <div class="div-common" style="margin-top: 150px">
      <div class="div1 left red"></div>
      <div class="div4 right yellow"></div>
      <div class="div2 left gray"></div>
      <div class="div3 right pink"></div>
      <div class="div2 right green"></div>
      <div class="div2 right blue"></div>
      <div class="div1 left brown"></div>
    </div>
    <div class="div-common" style="margin-top: 160px">
      <div class="div5 right red"></div>
      <div class="div1 left yellow"></div>
      <div class="div3 left gray"></div>
      <div class="div6 right green"></div>
      <div class="div7 left pink"></div>
      <div class="div8 left brown"></div>
    </div>
    <div class="div-common" style="margin-top: 160px">
      <div class="div5 right red"></div>
      <div class="div1 left yellow"></div>
      <div class="div3 left gray"></div>
      <div class="div6 right green"></div>
      <div class="div7 left pink"></div>
      <div class="div8 right brown"></div>
    </div>
  </body>
  <style>
    .div-common {
      margin-bottom: 10px;
      border: 1px dotted blue;
    }
    .div1 {
      height: 50px;
      width: 100px;
    }
    .div2 {
      height: 50px;
      width: 150px;
    }
    .div3 {
      height: 40px;
      width: 100px;
    }
    .div4 {
      height: 40px;
      width: 300px;
    }
    .div5 {
      height: 100px;
      width: 150px;
    }
    .div6 {
      height: 40px;
      width: 150px;
    }
    .div7 {
      height: 80px;
      width: 100px;
    }
    .div8 {
      height: 100px;
      width: 50px;
    }
  </style>
</html>

float-11.png

再看更复杂一点的例子。

  • 首先第一个例子,第一个红元素比第二个高一点,导致第三个灰元素左浮动时靠在红元素的右侧。而第四个绿元素比第三个灰元素低一点,因此第五个绿元素左浮动时靠在灰元素的右侧,形成了类似于台阶的样式。第二个例子把最后的绿元素右浮动,可以看到和左浮动在同一水平位置。
  • 第三个例子在第二个的基础上增加了右浮动的蓝元素。虽然是右浮动,但因为绿元素前面位置水平被灰元素挡住了,因此位置靠下了,这时候蓝元素左侧反而没有左浮动的元素了,这时候放置一个左浮动的棕元素,它反而靠在最前了。
  • 第四第五个例子非常类似,区别在于最后的棕元素是左浮动还是右浮动。棕元素的前一个元素是左浮动的粉元素,但是棕元素的位置上方恰好空出了一块位置,可以容纳棕元素。但是棕元素不能比前一个元素的水平位置更高,因此上方空出了一块位置。这个不管对左浮动还是右浮动都有效。

通过这几个例子可以看到,双侧浮动和单侧浮动的换行以及位置规律是一样的,单侧的规则双侧也是可以生效的。但由于双侧浮动情况更多,因此会有更多看起来奇怪的位置排列现象。

不同父元素的浮动流

上面尝试的例子都集中在一个父元素里面,如果在不同的父元素中浮动,会发生什么现象呢?

<html>
  <body>
    <div class="div-common">
      <div class="div1 left red"></div>
      <div class="div1 left yellow"></div>
    </div>
    <div class="div-common">
      <div class="div1 right gray"></div>
      <div class="div1 right pink"></div>
    </div>
    <div class="div-common" style="margin-top: 80px">
      <div class="div2 left red"></div>
      <div class="div1 right yellow"></div>
      <div class="div1 right pink"></div>
    </div>
    <div class="div-common" style="margin-top: 10px">
      <div class="div1 left blue"></div>
      <div class="div2 left gray"></div>
      <div class="div1 left green"></div>
    </div>
    <div class="div-devide" style="margin-top: 180px"></div>
    <div>
      <div class="div1 left red"></div>
      <div class="div1 left yellow"></div>
    </div>
    <div>
      <div class="div1 right gray"></div>
      <div class="div1 right pink"></div>
    </div>
    <div class="div-devide" style="margin-top: 100px"></div>
    <div>
      <div class="div2 left red"></div>
      <div class="div1 right yellow"></div>
      <div class="div1 right pink"></div>
    </div>
    <div style="margin-top: 10px">
      <div class="div1 left blue"></div>
      <div class="div2 left gray"></div>
      <div class="div1 left green"></div>
    </div>
  </body>
  <style>
    .div-common { border: 1px dotted blue; }
    .div-devide {
      border: 1px dotted brown;
    }
    .div1 {
      height: 50px;
      width: 100px;
    }
    .div2 {
      height: 50px;
      width: 150px;
    }
  </style>
</html>

float-12.png

这几个例子与前几个有一些区别:这些都是两个父级div组成了一个例子,父级div之间没有设置margin。前两个例子父级div有border,后两个例子没有。

  • 第一个例子:两个父级div由于内部元素全部浮动,因此不占空间。所以内部元素在垂直位置上居然是重叠的。即第二个父级div的元素浮动不会因为第一个父级div元素中出现浮动而在第二行排列。但由于父级div存在border,因此垂直位置并不是完全一致的,而是有很小的高度差。
  • 第三个例子:在第一个例子的基础上去掉了父级div的border。这样我们发现不同父级的浮动元素在垂直方向上位置完全一致。
  • 第二个例子:在第一个例子的基础上,增加了第二个父级div的margin-top,这样两个父级的元素浮动在第一行就能有明显区分,而且多了几个元素。我们看第五个灰元素,它自身属于第二个父级,放置的时候属于第一个父级的右浮动元素挡住了它的位置。虽然所属的父元素不同,但是灰元素依然避开了粉元素,在下方放置了。最后一个绿元素因为第一行位置不够,在第二行重新开始浮动。
  • 第四个例子:在第二个例子的基础上,去掉了父级div的border。现象与第二个例子基本一致。

从这几个例子可以看出,虽然浮动元素所属的父级不同,浮动流的规律也是适用的;即在同一个浮动流中浮动。

浮动流与同高度行内元素

上面我们讨论了很多块级元素在浮动流中的现象,现在再讨论一下浮动流与行内元素的特点。由于行内元素场景更多,这里仅讨论下非可替换与行内块元素,以及元素高度固定的场景。这里以span元素为例,其它元素的现象也是一致的。

单个行内元素浮动

首先来看一下单个行内浮动元素的表现。

<html>
  <body>
    <div class="div-common">
      <span class="red">第1个</span><span class="yellow">第2个</span
      ><span class="green">第3个</span><span class="blue">第4个</span
      ><span class="gray">第5个</span>
    </div>
    <div class="div-common">
      <span class="red">第1个</span><span class="left yellow">第2个左</span
      ><span class="green">第3个</span><span class="blue left">第4个左</span
      ><span class="gray">第5个</span>
    </div>
    <div class="div-common">
      <span class="red">第1个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="left yellow">第2个个个个个左</span>
    </div>
    <div class="div-common">
      <span class="red"
        >第1个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="left yellow">第2个个个个个左</span>
    </div>
    <div class="div-common">
      <span class="red"
        >第1个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="left yellow">第2个个个个个左</span>
    </div>
    <div class="div-common">
      <span class="red">第1个个个个个个个个个个个个个个个</span
      ><span class="yellow">第2个个个个个个个个个</span
      ><span class="left green">第3个个个个个左</span>
    </div>
    <div class="div-common">
      <span class="red">第1个个个个个个个个个个个</span
      ><span class="yellow">第2个个个个个个个个个个个个个个个个个个个个</span
      ><span class="left green">第3个个个个个左</span>
    </div>
    <div class="div-common">
      <span class="red">第1个个个个个个个个个个个个个个个个个</span
      ><span class="yellow">第2个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green"
        >第3个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="left gray">第4个个个个个个个个个个个</span>
    </div>
    <div class="div-common">
      <span class="red">第1个个个个个个个个个个个个个个个个个</span
      ><span class="yellow">第2个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green"
        >第3个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="gray">第4个个个个个个个</span><span class="left pink">第5个个个个个个个个</span>
    </div>
  </body>
  <style>
    .div-common {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
  </style>
</html>

float-13.png

  • 第一个例子:五个行内元素,没有设置浮动
  • 第二个例子:第二个和第四个元素设置了左浮动,这两个元素跑到最左边了。同样都是浮动元素时,按照它们的在原HTML中的位置排列。
  • 第三个例子:只有两个元素,第二个元素浮动。但第一个元素太长,使得第一行无法容纳下完整的第二个元素,因此跑到第二行最左边浮动。注意此时第一行虽然有位置,但是浮动元素并未利用。(非浮动时则会利用)
  • 第四个例子:只有两个元素,第二个元素浮动。但第一个元素太长,超过了一行。因此第二个元素跑到第二行最左边浮动。注意此时第一个元素看起来向被浮动元素“断成两截”的样子。
  • 第五个例子:第一个元素太长了,自己延伸到第三行了,因此把第二个元素挤到第三行浮动。
  • 第六个例子:前两个元素使得第一行位置不足了,因此跑到第二行最左边浮动。注意此时第一行虽然有位置,但是浮动元素并未利用。(非浮动时则会利用)
  • 第七个例子:前两个元素超过了一行,第三个元素跑到第二行最左边浮动。最后一个元素像是被浮动元素“断成两截”的样子。
  • 第八个例子:三个元素延伸到第三行了,因此把第四个元素挤到第三行浮动。
  • 第九个例子:四个元素延伸到第三行了,第五个元素挤到第三行浮动。但注意第四个元素自身比较短,是肯定在第三行展示的,因此这里不止截断了第三个长元素,第一个元素还在同一行的第五个后面。

这里能总结出单个元素单侧浮动的一点规律:浮动元素会在当前行向一侧浮动。但如果浮动元素在浮动前的位置跨行,则在它最下方所在行浮动。即原来浮动元素可能在第二行和第三行,则浮动后会在第三行浮动。我们做一下更多的实验,看看总结的规律是否正确。

单个元素很长

这里试一下单个元素很长的场景,包括单个很长的非浮动元素与浮动元素:

<html>
  <body>
    <div class="div-common">
      <span class="red"
        >第1个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="yellow">第2个</span>
    </div>
    <div class="div-common">
      <span class="red"
        >第1个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="yellow left">第2个</span>
    </div>
    <div class="div-common">
      <span class="red"
        >第1个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="yellow">第2个</span>
    </div>
    <div class="div-common">
      <span class="red"
        >第1个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="yellow left">第2个</span>
    </div>
    <div class="div-common">
      <span class="yellow">第1个</span
      ><span class="red"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      >
    </div>
    <div class="div-common" style="margin-bottom: 70px">
      <span class="yellow">第1个</span
      ><span class="red left"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      >
    </div>
    <div class="div-common">
      <span class="yellow">第1个</span
      ><span class="red"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green">第3个</span><span class="blue">第4个</span>
    </div>

    <div class="div-common">
      <span class="yellow">第1个</span
      ><span class="red"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green left">第3个</span><span class="blue">第4个</span>
    </div>

    <div class="div-common" style="margin-bottom: 90px">
      <span class="yellow">第1个</span
      ><span class="red left"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green">第3个</span><span class="blue">第4个</span>
    </div>

    <div class="div-common">
      <span class="yellow">第1个</span
      ><span class="red left"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green">第3个</span><span class="blue">第4个</span
      ><span class="gray"
        >第5个个个个个个个个个个个个个个个个个个个个个个个</span
      >
    </div>

    <div class="div-common">
      <span class="red left"
        >第1个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="yellow">第2个</span><span class="green">第3个</span
      ><span class="blue">第4个</span
      ><span class="gray">第5个个个个个个个个个个个</span>
    </div>
  </body>
  <style>
    .div-common {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
  </style>
</html>

float-14.png

上面的每个例子共同特征是都有一个超过一行的红元素。我们一个一个来分析:

  • 第一个例子:未设置浮动,与第二个例子做对比用。
  • 第二个例子:第二个黄元素设置了浮动,浮动前它在红元素之后,浮动后截断了红元素,跑到第二行最左侧了。
  • 第三个例子:与第一个例子类似,未设置浮动。只不过红元素更长了,使得第二个黄元素在第二行换行。
  • 第四个例子:第三个例子中的黄元素设置了浮动,可以看到浮动前黄元素横跨第二行第三行,浮动后只在第三行展示。
  • 第五个例子:第一个黄元素与第二个长红元素横跨第一行第二行,均未设置浮动。
  • 第六个例子:第五个例子中的红元素设置了浮动,在设置浮动后红元素从第二行开始展示,一直持续到第三行,且红色背景的覆盖范围持续到第三行结束。
  • 第七个例子:一共四个元素,均未设置浮动。其中第二个红元素很长。
  • 第八个例子:第三个绿元素设置了浮动,它本来就在第二行,因此跑到了第二行最左侧,截断了红元素。
  • 第九个例子:一共四个元素,其中第二个红元素很长,且设置了浮动。可以看到第二个红元素在第二行第三行展示,和第六个例子一致。
  • 第十个例子:在第九个例子的基础上增加了第五个灰元素,较长且未设置浮动。第二个红元素右上方的空白被灰元素填充了,且剩下灰元素部分被浮动的红元素阶段,到了第四行展示。
  • 第十一个例子:第十个例子中的红元素跑到了第一位,其它元素都没有占用红元素在第二行剩下的空间,而是全部在第三行展示。

上面的例子看似有点奇怪,其实比较容易理解。虽然我们的元素是span,但浮动后就变成了块级元素,其中的文本也在块级元素内展示。因此浮动前这些文本可能横跨两行,浮动后便统一在一行展示了。即使是避免不了换行的长元素,也是在块级元素内部换行。也正因为是块级元素,因此存在换行的长元素的未被文本覆盖的有背景的位置,也属于块级内部,别的文本是不能占用这个位置来展示的。

至于同一个span元素中的文本是可以被浮动元素截断,导致换行甚至间隔几行来展示的。另外如果浮动的元素在最下方,父元素的宽高是不计算最下方的浮动元素的。但如果浮动元素在最上方,父元素的宽高却将它包含在内。

多行内元素单侧浮动

上面的例子基本都都是单个元素浮动的场景,这一部分我们来看下多个元素同时浮动的例子。

<html>
  <body>
    <div class="div-common">
      <span class="yellow">第1个</span><span class="red">第2个</span
      ><span class="green">第3个</span><span class="blue">第4个</span
      ><span class="gray">第5个</span>
    </div>
    <div class="div-common">
      <span class="yellow">第1个</span><span class="red left">第2个</span
      ><span class="green">第3个</span><span class="blue left">第4个</span
      ><span class="gray">第5个</span>
    </div>
    <div class="div-common">
      <span class="yellow">第1个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span><span class="red left">第2个</span
      ><span class="green">第3个</span><span class="blue left">第4个</span
      ><span class="gray">第5个</span>
    </div>
    <div class="div-common">
      <span class="yellow">第1个个个个个个个个个个个个个个个个个个个个个个个个个个</span><span class="red left">第2个</span
      ><span class="green">第3个</span><span class="blue left">第4个</span
      ><span class="gray">第5个</span>
    </div>
    <div class="div-common">
      <span class="yellow">第1个个个个个个个个个个个个个个个个个个个个个个个个个</span><span class="red left">第2个</span
      ><span class="green">第3个</span><span class="blue left">第4个</span
      ><span class="gray">第5个</span>
    </div>
    <div class="div-common">
      <span class="yellow">第1个个个个个个个个个个个个个个个个个个个个个个</span><span class="red left">第2个</span
      ><span class="green">第3个</span><span class="blue left">第4个</span
      ><span class="gray">第5个</span>
    </div>
    <div class="div-common">
      <span class="yellow">第1个个个个个个个个个个个个个个个个个个个个</span><span class="red left">第2个</span
      ><span class="green">第3个</span><span class="blue left">第4个</span
      ><span class="gray">第5个</span>
    </div>
    <div class="div-common">
      <span class="yellow">第1个个个个个个个个个个个个个个个个</span><span class="red left">第2个</span
      ><span class="green">第3个</span><span class="blue left">第4个</span
      ><span class="gray">第5个</span>
    </div>
  </body>
  <style>
    .div-common {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
  </style>
</html>

float-15.png

这是一组相互关联的例子,每个例子都是五个元素。从第二个例子开始,都是第二个和第四个元素浮动。从第三个例子开始,第一个元素变长,但是长度逐渐减小。

  • 第一个例子:五个元素没有浮动,做对照使用。
  • 第二个例子:第二和第四个元素左浮动,浮动顺序为在HTML中的元素顺序。
  • 第三个例子:第一个元素超长,导致元素其它元素全部在第二行。浮动的第二和第四个元素也在第二行展示。效果和上一节的例子一致。
  • 第四个例子:第一个元素缩短,长度不足占满一行,但是第一行又不够完整的展示第二个元素(浮动为了块级元素,不能像行内元素一样换行),因此第三个元素的一部分放到了第一行。
  • 第五个例子:继续缩短第一个元素,使得第一行可以容纳第二个元素,因此第二个元素在第一行左浮动。
  • 第六个例子:继续缩短第一个元素,使得第一行可以容纳第二和第三个元素,但第四个位置不够,此时第五个元素在第一行和第二行跨行展示。
  • 第七个例子:继续缩短第一个元素,使得第一行可以容纳第二,第三,第四个元素。此时第二和第四个元素都在第一行左浮动。
  • 第八个例子:继续缩短第一个元素,此时所有元素都在第一行展示,类似第二个例子。

这个例子体现出的行内浮动规则在前面已经说过了,但是通过不断地缩短第一个元素,能看出一个有趣的现象:当前行无法容纳浮动元素时,浮动元素会在下一行展示。但是排在后面的非浮动元素却不受限制,可以跑到浮动的前一行展示。

如果排到后面的浮动元素可以在当前行容纳下,那么这个元素会不会排到前面展示呢?根据上面块级元素的规律,我认为不会。我们再看几个例子实验下:

<html>
  <body>
    <div class="div-common">
      <span class="yellow">第1个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="red left">第2个个个个个个个</span
      ><span class="green left">第3个</span><span class="blue left">第4个</span
      ><span class="gray">第5个</span>
    </div>
    <div class="div-common" style="margin-bottom: 150px">
      <span class="yellow">第1个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="red left"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green left">第3个</span
      ><span class="blue left"
        >第4个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="gray left">第5个个个</span>
    </div>
    <div class="div-common">
      <span class="yellow">第1个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="red left"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green left">第3个</span
      ><span class="blue left"
        >第4个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="gray left">第5个个个</span
      ><span class="pink"
        >第6个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      >
    </div>
    <div class="div-common">
      <span class="yellow">第1个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="red left"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green left">第3个</span
      ><span class="blue left"
        >第4个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="gray left">第5个个个</span
      ><span class="pink"
        >第6个个个个个个个个个个个个个个个个个个</span
      >
    </div>
  </body>
  <style>
    .div-common {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
  </style>
</html>

float-16.png

  • 第一个例子:第一个黄元素比较长,导致第二个红元素虽然是左浮动,但第一行容纳不下,因此在第二行左浮动。第三个第四个元素虽然第一行的空间足够容纳下,但它们是左浮动,必须在第二个红元素下方或者右侧,因此只能在第二行红元素后面放置。
  • 第二个例子:第二到第五个元素全部左浮动。其中第二和第四个元素长度超过了一行。可以看到所有浮动的元素都单独一行,因为所有的元素都没办法和前面的元素组合成单独一行。而且虽然第一行有空位可以容纳第三个元素,而且第二个元素后面(第三行)也有空位,但是由于块级元素的性质以及浮动元素不能出现在前一个浮动元素的“前面”,因此第三个元素依然独立一行展示。第五个元素同理。
  • 第三个例子:增加了第六个非浮动元素,可以看到它在各个浮动元素造成的空白中补足(除了被块级元素占据的空白)。
  • 第四个例子:缩短了第六个元素的长度,使其只到第四行。

可以看到第二个例子中父级元素的的边框只右第一行,第三个例子中父级元素的边框持续到了最后一行,第四个例子中父级元素的边框缩短到了第四行。这说明行内元素构成的浮动中,依然是非浮动元素的高度决定了它的高度。通过这些例子可以看到,块级元素浮动规律和文本行内元素构成的浮动是一致的。例如后一个左浮动元素必须在前一个左浮动元素的“下方或者右侧”(右浮动同理)。

多行内元素双侧浮动

了解了单侧多元素浮动,再来看一下双侧多元素浮动的场景。与单侧一样,我们也构造一组相互关联的例子:

<html>
  <body>
    <div class="div-common">
      <span class="red">第1个</span><span class="yellow">第2个</span
      ><span class="green">第3个</span><span class="blue">第4个</span
      ><span class="gray">第5个</span><span class="pink">第6个</span
      ><span class="brown">第7个</span>
    </div>
    <div class="div-common">
      <span class="red right">第1个</span><span class="yellow">第2个</span
      ><span class="green right">第3个</span><span class="blue">第4个</span
      ><span class="gray left">第5个</span><span class="pink">第6个</span
      ><span class="brown left">第7个</span>
    </div>
    <div class="div-common">
      <span class="red right"
        >第1个个个个个个个个个个个个个个</span
      ><span class="yellow">第2个</span><span class="green right">第3个</span
      ><span class="blue">第4个</span><span class="gray left">第5个</span
      ><span class="pink">第6个</span><span class="brown left">第7个</span>
    </div>
    <div class="div-common">
      <span class="red right"
        >第1个个个个个个个个个个个个个个个个</span
      ><span class="yellow">第2个</span><span class="green right">第3个</span
      ><span class="blue">第4个</span><span class="gray left">第5个</span
      ><span class="pink">第6个</span><span class="brown left">第7个</span>
    </div>
    <div class="div-common">
      <span class="red right"
        >第1个个个个个个个个个个个个个个个个个个</span
      ><span class="yellow">第2个</span><span class="green right">第3个</span
      ><span class="blue">第4个</span><span class="gray left">第5个</span
      ><span class="pink">第6个</span><span class="brown left">第7个</span>
    </div>
    <div class="div-common">
      <span class="red right"
        >第1个个个个个个个个个个个个个个个个个个个</span
      ><span class="yellow">第2个</span><span class="green right">第3个</span
      ><span class="blue">第4个</span><span class="gray left">第5个</span
      ><span class="pink">第6个</span><span class="brown left">第7个</span>
    </div>
    <div class="div-common">
      <span class="red right"
        >第1个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="yellow">第2个</span><span class="green right">第3个</span
      ><span class="blue">第4个</span><span class="gray left">第5个</span
      ><span class="pink">第6个</span><span class="brown left">第7个</span>
    </div>
    <div class="div-common">
      <span class="red right"
        >第1个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="yellow">第2个</span><span class="green right">第3个</span
      ><span class="blue">第4个</span><span class="gray left">第5个</span
      ><span class="pink">第6个</span><span class="brown left">第7个</span>
    </div>
    <div class="div-common">
      <span class="red right"
        >第1个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="yellow">第2个</span><span class="green right">第3个</span
      ><span class="blue">第4个</span><span class="gray left">第5个</span
      ><span class="pink">第6个</span><span class="brown left">第7个</span>
    </div>
  </body>
  <style>
    .div-common {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
  </style>
</html>

float-17.png

  • 第一个例子:未设置浮动,做对比用。
  • 第二个例子:第一个和第三个元素设置了右浮动,第五个和第七个元素设置了左浮动。可以看到,在HTML中顺序靠前的元素在浮动中的顺序更靠前。
  • 第三个例子:第一个元素长度增加,使得第一行放不开所有元素。这里第七个元素被挤到了第二行。由于第七个元素是浮动元素,因此他没有跨行展示。
  • 第四个例子:第一个元素长度继续增加。使得第六个元素无法完整放在第一行,由于它不是浮动元素,因此跨行展示。
  • 第五个例子:第一个元素长度继续增加,第六个元素全部放置在第二行。
  • 第六个例子:第一个元素长度继续增加,使得第五个元素无法完全放置在第一行。注意由于第五个元素是左浮动,不能跨行展示,因此在完全第二行展示。但是这样造成第一行又剩下一点空间被第六个元素填充了。
  • 第七个例子:第一个元素长度继续增加,第五六七个元素都在第二行展示,第四个元素由于空间不够跨行展示。
  • 第八个例子:第一个元素长度继续增加,第二行完整展示第四个元素。
  • 第九个例子:第一个元素长度继续增加,超过一行。触发了块级元素独立成两行,其它元素都在第三行展示。

当双侧浮动时,元素的展示与换行规律也是一样的,浮动元素的出现位置按照HTML的位置顺序出现。对于浮动元素来说,位置更靠后的元素的所在行不可能在位置更靠前的元素所在行的前面。但非浮动元素由于可以补缺的原因,位置靠后的元素是可以出现在更前面的行的。我们再来看几个例子:

<html>
  <body>
    <div class="div-common">
      <span class="red left">第1个个个个个个个个个个个个</span
      ><span class="yellow right">第2个个个个个个个个个个个个个</span
      ><span class="green left">第3个个个个个个个个个个个个个个个</span
      ><span class="blue right">第4个个个个个个个个</span
      ><span class="gray left">第5个个个个个个个个个</span
      ><span class="pink right">第6个个个个个个个个个个个</span
      ><span class="brown">第7个个个个个个个个个个个个个个个个个个个</span>
    </div>
    <div class="div-common" style="margin-bottom: 100px">
      <span class="brown">第0个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="red left">第1个个个个个个个个个个个个</span
      ><span class="yellow right">第2个个个个个个个个个个个个个</span
      ><span class="green left">第3个个个个个个个个个个个个个个个</span
      ><span class="blue right">第4个个个个个个个个</span
      ><span class="gray left">第5个个个个个个个个个</span
      ><span class="pink right">第6个个个个个个个个个个个</span>
    </div>
    <div class="div-common">
      <span class="red left"
        >第1个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="yellow right"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green left">第3个个个个个个个个个个个个</span
      ><span class="blue right"
        >第4个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="gray left">第5个个个个个个个个个个个个</span
      ><span class="pink right">第6个个个个个个个个个个</span
      ><span class="brown"
        >第7个个个个个个个个个个个个个个个个个个个个个个个个个</span
      >
    </div>
    <div class="div-common">
      <span class="red left"
        >第1个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="yellow right"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green left">第3个个个个个个个个个个个个</span
      ><span class="blue right"
        >第4个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="gray left">第5个个个个个个个个个个个个</span
      ><span class="pink right">第6个个个个个个个个个个</span
      ><span class="brown"
        >第7个个个个个个个个个个个个个个个个个个个个个个个个个</span
      >
    </div>
  </body>
  <style>
    .div-common {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
  </style>
</html>

float-18.png

  • 第一个例子:前六个元素分别左右浮动,致使前三行都有少量空白。第七个元素未浮动,因此从第一行开始填补每一行的空白区域。
  • 第二个例子:第0个元素未浮动,第一到六个分别左右浮动。可以看到第一行的未浮动元素没有向下填充下面行的空白,下面的浮动元素也没有向上侵占未浮动元素的空间。
  • 第三个例子:浮动元素长度增加了,第二个元素和第四个元素都超过了一行。但是第四个元素依然在空白处填补。
  • 第四个例子:第三个例子未改动。但是使用鼠标选中了第一到第六个元素中间的部分文本。可以看到第7个元素虽然在中间穿插填补,但并未被选中。实际上选中顺序还是按照HTML的顺序,不是按照页面上呈现的顺序。

第一与第二个例子中六个浮动元素是一样的,区别在于未浮动元素的位置。第一个例子在最后,元素向上填补了空缺。第二个例子在最前,元素没有向下填补浮动元素造成的空缺。

浮动流中块级元素与同高度行内元素

前面描述的行内元素都是同高度的场景,但行内元素和块级元素都存在高度不同的场景。我们首先从简单的块级元素与同高度行内元素的组合场景开始看起。

仅块级元素浮动

<html>
  <body>
    <div class="wrapper">
      <div class="red block"></div>
      <span class="yellow">第1个</span><span class="green">第2个</span>
      <div class="brown block"></div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <div class="red block left"></div>
      <span class="yellow">第1个</span><span class="green">第2个</span>
      <div class="brown block"></div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <div class="red block left"></div>
      <span class="yellow">第1个</span><span class="green">第2个</span>
      <div class="brown block left"></div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <div class="red block"></div>
      <span class="yellow">第1个</span><span class="green">第2个</span>
      <div class="brown block left"></div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <div class="red block"></div>
      <span class="yellow">第1个</span><span class="green">第2个</span>
      <div class="brown block left"></div> <div class="maroon block left"></div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <div class="red block"></div>
      <span class="yellow">第1个</span><span class="green">第2个</span>
      <div class="brown block left"></div><div class="maroon block left"></div>
      <div class="purple block"></div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
  </body>
  <style>
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
    .block {
      width: 100px;
      height: 40px;
    }
  </style>
</html>

float-19.png

这是一组较简单的例子,没有换行,所有块级元素的高度一致而且都是左浮动。我们来分析一下:

  • 第一个例子:块级元素和行内元素间隔放置,没有设置浮动,做对比用。可以看到块级元素独立一行,在块级元素之间的行内元素们也是独立一行。
  • 第二个例子:红块级元素设置浮动。虽然浮动元素脱离了文档流不占空间,但是没有脱离“文本流”,行内元素还是会为它空出位置,因此行内元素在它同一行的后面显示。至于棕色的块级元素,没有浮动,所以应该在上面行内元素的下一行展示。由于红浮动元素并不占空间,因此和红元素部分重叠展示。
  • 第三个例子:两个块级元素同时浮动,到了一行。行内元素因此也在后面展示了。注意第二和第三个行内元素中间有空,这是因为它们中间原有一个块级元素,虽然浮动走了,但是两个行内元素并不紧挨,因此中间会出现空格。
  • 第四个例子:棕块级元素浮动。上面的红元素继续独立一行,棕块级元素则出现在了浮动元素之前。
  • 第五个例子:在棕块级元素浮动的基础上,增加了浮动的褐块级元素。可以看到横向展示了。
  • 第六个例子:在浮动的褐块级元素的后面增加了一个非浮动的紫色块级元素,可以看到如第二个例子一样,紫色元素为上面的行内元素空出一行,然后与棕色元素部分重叠展示。最后的行内元素则独立一行展示。

通过行内元素和块级元素的对比,我们可知在前一个块级元素浮动后,后面的非浮动块级元素依然会独立一行展示。但是后面的行内元素却紧贴着浮动块级元素同行展示。这也是浮动脱离文档流但没有脱离“文本流”的标志。

<html>
  <body>
    <div class="wrapper">
      <div class="red block"></div>
      <span class="yellow">第1个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      >
      <div class="brown block"></div>
      <span class="gray">第3个个个个个个个个个个个个个个个</span
      ><span class="pink">第4个个个个个个个个个个个个个</span>
    </div>
    <div class="wrapper">
      <div class="red block left"></div>
      <span class="yellow"
        >第1个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      >
      <div class="brown block"></div>
      <span class="gray">第3个个个个个个个个个个个个个个个</span
      ><span class="pink">第4个个个个个个个个个个个个个</span>
    </div>
    <div class="wrapper">
      <div class="red block left"></div>
      <span class="yellow"
        >第1个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      >
      <div class="brown block left"></div>
      <span class="gray">第3个个个个个个个个个个个个个个个</span
      ><span class="pink">第4个个个个个个个个个个个个个</span>
    </div>
    <div class="wrapper">
      <div class="red block"></div>
      <span class="yellow"
        >第1个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      >
      <div class="brown block left higher"></div>
      <div class="purple block left"></div>
      <span class="gray">第3个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="pink">第4个个个个个个个个个个个个个个个个个个个个个</span>
    </div>
    <div class="wrapper">
      <div class="red block"></div>
      <span class="yellow"
        >第1个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      >
      <div class="brown block left"></div>
      <div class="purple block left higher"></div>
      <span class="gray">第3个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="pink">第4个个个个个个个个个个个个个个个个个个个个个</span>
    </div>
  </body>
  <style>
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
    .block {
      width: 100px;
      height: 40px;
    }
    .higher {
      height: 100px;
    }
  </style>
</html>

float-20.png

这里又有几个例子,主要展示了换行和块级元素高度不同的场景。我们一个一个分析下:

  • 第一个例子:块级元素和行内元素间隔放置,没有设置浮动,做对比用。可以看到行内元素正常换行。
  • 第二个例子:红块级元素设置浮动。可以看到行内元素在红元素右侧展示,且因为红元素可以容纳两行,因此在右侧分行放置。行内元素的第三行因为空间不够,独立成行。
  • 第三个例子:两个块级元素都设置了浮动。与上面行内元素没有换行的场景不同,这次两个元素分成两行浮动了。这是因为浮动元素后面的块级元素太长,导致第一个浮动元素后面没有空间放置第二个浮动元素了,只能新起一行。
  • 第四个例子:红块级元素没有浮动。后面行内元素有三行,再后面的两个块级元素都设置了浮动。注意看行内元素的第三行,前面是两个左浮动的块级元素。因此,行内元素的最后一行后面如果有左浮动元素,该浮动元素会跑到这一行的最左边。这与我们讨论行内浮动元素时的规律一致。注意看两个浮动元素的高度不一致,先高后矮。后面的行内元素,先在紫色的块级元素后面一行一行展示,然后又在下面更高的棕色元素后一行一行展示。
  • 第五个例子:与上一个例子不一样的是,两个浮动元素先矮后高。右侧的浮动元素并没有到棕色元素下面的空白区域展示,而是一直在右侧浮动。这与我们前面看到的浮动块级元素的规律类似。

仅行内元素浮动

<html>
  <body>
    <div class="wrapper">
      <div class="red block"></div>
      <span class="yellow">第1个</span><span class="green">第2个</span>
      <div class="brown block"></div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <div class="red block"></div>
      <span class="yellow left">第1个</span><span class="green">第2个</span>
      <div class="brown block"></div>
      <span class="gray">第3个</span><span class="pink left">第4个</span>
    </div>
    <div class="wrapper">
      <div class="red block"></div>
      <span class="yellow left">第1个</span
      ><span class="green"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="gray">第3个</span><span class="pink left">第4个</span>
      <div class="brown block"></div>
    </div>
    <div class="wrapper">
      <div class="red block"></div>
      <span class="yellow">第1个</span
      ><span class="green left"
        >第2个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="gray">第3个</span><span class="pink left">第4个</span>
      <div class="brown block"></div>
    </div>
  </body>
  <style>
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
    .block {
      width: 100px;
      height: 40px;
    }
  </style>
</html>

float-21.png

  • 第一个例子:块级元素和行内元素间隔放置,没有设置浮动,做对比用。
  • 第二个例子:第一个和第四个文本左浮动。可以看到文本元素浮动仅在文本元素所在的行内部进行,不会影响块级元素。
  • 第三个例子:所有文本放在一起,且第二个文本超长,第一个和第四个文本左浮动。可以看到由于第二个元素跨行,所以第四个元素跑到文本第二行的最左边。
  • 第四个例子:第二个和第四个文本浮动。第二个文本由于超长因此独立两行展示,第四个文本没有侵占第二个文本内的空间。注意我们没有看到棕色块级元素,因为它被浮动元素遮盖了(注意看蓝色边框是留出棕色块级元素位置的)。浮动元素由于脱离文档流不占用空间,因此把下面的非浮动元素遮盖了。

在这个场景下,规律与上面纯行内元素的浮动场景一致,因此这里不过多讨论了。另外注意“第四个例子”中,非浮动的文本元素会给浮动的块级元素留出空间(即文本流),但是浮动的文本元素却没有给非浮动的块级元素留出空间。关于这一点我们再看一下明确的对比:

<html>
  <body>
    <div class="wrapper">
      <div class="red block"></div>
      <span class="yellow">第1个个个个个个个个个个</span>
    </div>
    <div class="wrapper">
      <div class="red block left"></div>
      <span class="yellow">第1个个个个个个个个个个</span>
    </div>
    <div class="wrapper">
      <span class="yellow">第1个个个个个个个个个个</span>
      <div class="red block"></div>
    </div>
    <div class="wrapper">
      <span class="yellow left">第1个个个个个个个个个个</span>
      <div class="red block"></div>
    </div>
  </body>
  <style>
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
    .block {
      width: 100px;
      height: 40px;
    }
  </style>
</html>

float-22.png

  • 第一个例子:块级元素在上方,行内元素在下方,未浮动做对比用。
  • 第二个例子:块级元素浮动,下方的行内元素到了与块级元素同行的位置,且给块级元素留出了位置。
  • 第三个例子:行内元素在上方,块级元素在下方,未浮动做对比用。
  • 第四个例子:行内元素浮动,下方的块级元素位置上移。文本元素没有给块级元素留出位置,反而遮挡了下方的块级元素。

块级元素与行内元素同时浮动

<html>
  <body>
    <div class="wrapper">
      <div class="red block"></div>
      <span class="yellow">第1个</span><span class="green">第2个</span>
      <div class="brown block"></div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <div class="red block left"></div>
      <span class="yellow left">第1个</span><span class="green">第2个</span>
    </div>
    <div class="wrapper">
      <span class="yellow left">第1个</span><span class="green">第2个</span>
      <div class="red block left"></div>
    </div>
    <div class="wrapper">
      <div class="red block"></div>
      <span class="yellow">第1个</span><span class="green left">第2个</span>
      <div class="brown block left"></div>
      <span class="gray">第3个</span><span class="pink left">第4个</span>
    </div>
    <div class="wrapper">
      <div class="red block"></div>
      <span class="yellow">第1个</span><span class="green left">第2个个个个个个个个个个个个个个个个个</span>
      <div class="brown block left"></div>
      <span class="gray">第3个</span><span class="pink left">第4个</span>
    </div>
    <div class="wrapper">
      <div class="red block"></div>
      <span class="yellow">第1个</span><span class="green left">第2个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span>
      <div class="brown block left"></div>
      <span class="gray">第3个</span><span class="pink left">第4个</span>
    </div>
  </body>
  <style>
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
    .block {
      width: 100px;
      height: 40px;
    }
  </style>
</html>

float-23.png

  • 第一个例子:未设置浮动,做对比用。
  • 第二个例子:红块级元素与第一个文本元素设置了浮动,可以看到横向顺序排列。
  • 第三个例子:红块级元素放到了最后,可以看到依然横向排列,只不过位置发生了变化。
  • 第四个例子:棕块级元素,第二和第四个文本元素浮动。可以看到在第二排横向展示,非浮动元素排列在后面。注意第三个文本元素因为和第一个元素的HTML中实际有个块级元素,因此有个空隙。而第一个文本元素虽然和第四个元素的HTML中间也有块级元素,但左侧是浮动区域,因此不展示空隙了。
  • 第五个例子:第二个文本元素长度加长。其它元素被挤到右边,发生了换行。注意看此时虽然第四个文本元素浮动,一三文本元素未浮动,但是依然一三元素在上方,第四个元素在下方。
  • 第六个例子:我们继续加长第二个文本元素,使其超过一行。可以看到一三文本元素在第二行上方,第四个元素则跑到了最后。注意第二个文本元素在HTML中的位置是比第三个文本元素靠前,但是依然在后面展示。这与前面行内元素的展示逻辑是一样的。

那么如果多个不同的块级元素中间出现的“空白区域”,浮动或者浮动文本元素会不会补齐呢?我们看一下例子:

<html>
  <body>
    <div class="wrapper" style="margin-bottom: 50px">
      <div class="red block1 left"></div>
      <div class="brown block2 left"></div>
      <span class="yellow">第1个</span><span class="green">第2个</span
      ><span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <div class="red block1 left"></div>
      <div class="brown block2 left"></div>
      <span class="yellow"
        >第1个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green">第2个</span><span class="gray">第3个</span
      ><span class="pink">第4个</span>
    </div>
    <div class="wrapper" style="margin-bottom: 100px">
      <div class="red block1 left"></div>
      <div class="brown block2 left"></div>
      <span class="yellow left"
        >第1个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green">第2个</span><span class="gray">第3个</span
      ><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <div class="red block1 left"></div>
      <div class="brown block2 left"></div>
      <span class="yellow"
        >第1个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green left">第2个</span><span class="gray">第3个</span
      ><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <span class="yellow left">第1个</span>
      <div class="red block1 left"></div>
      <div class="brown block2 left"></div>
      <span class="green">第2个个个个个个个个个个个个个个个个个个个个个个个个个</span><span class="gray">第3个</span
      ><span class="pink">第4个</span>
    </div>
    <div class="wrapper" style="margin-bottom: 80px">
      <span class="yellow left">第1个</span>
      <div class="red block1 left"></div>
      <div class="brown block2 left"></div>
      <span class="green left">第2个个个个个个个个</span><span class="gray left">第3个</span
      ><span class="pink">第4个</span>
    </div>
    <div class="wrapper" style="margin-bottom: 100px">
      <span class="yellow left">第1个个个个个个个个个个</span>
      <div class="red block1 left"></div>
      <div class="brown block2 left"></div>
      <span class="green left">第2个个个个个个个个</span><span class="gray left">第3个</span
      ><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <span class="yellow left">第1个个个个个个个个个个</span>
      <div class="red block1 left"></div>
      <div class="brown block2 left"></div>
      <span class="green left">第2个个个个个个个个</span><span class="gray left">第3个</span
      ><span class="pink">第4个个个个个个个个个个个个个个个个个个</span>
    </div>
  </body>
  <style>
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
    .block1 {
      width: 100px;
      height: 20px;
    }
    .block2 {
      width: 200px;
      height: 50px;
    }
  </style>
</html>

float-24.png

  • 第一个例子:设置了两个浮动的块级元素,左边低,右边高,左边下方留下的空白。这个例子主要做对比用。
  • 第二个例子:第一个文本元素长度加长,使得其它文本元素跑到空白下面了,但是没有去补齐空白。
  • 第三个例子:第一个文本元素设置浮动,在下面单独一行。可以看到其它文本元素在第一个文本元素上面展示了。
  • 第四个例子:文本元素仅第二个设置了浮动,可以看到它位于当前行的最左位置,没有向上补齐。
  • 第五个例子:第一个文本元素改成了在块级元素之前,第二个文本元素加长。可以看到依然没有元素补齐空白。
  • 第六个例子:在第五个例子的条件下,第二第三个文本元素浮动。可以看到第四个文本元素在右侧上方展示。
  • 第七个例子:第一个文本元素加长,导致两个块级元素不能在同一行。
  • 第八个例子:第四个文本元素加长,可以看到依然没有补齐空白。

因此,块级浮动元素造成的左侧空白是不能被补齐的,文本元素只能补齐右侧的空白,而且是在非浮动状态下。我们列举几个可以被补齐的例子:

<html>
  <body>
    <div class="wrapper" style="margin-bottom: 70px">
      <span class="yellow">第1个个个个个个个个个个个个个个个个个个个个</span>
      <div class="red block2 left"></div>
      <span class="green left">第2个</span><span class="gray left">第3个</span
      ><span class="pink">第4个</span>
    </div>
    <div class="wrapper" style="margin-bottom: 50px">
      <div class="red block2 left"></div>
      <div class="pink block2 left"></div>
      <div class="purple block2 left"></div>
      <span class="yellow">第1个个</span>
      <span class="green">第2个个个个个</span><span class="gray">第3个</span
      ><span class="pink">第4个</span>
    </div>
    <div class="wrapper" style="margin-bottom: 50px">
      <div class="red block2 left"></div>
      <div class="pink block2 left"></div>
      <div class="purple block2 left"></div>
      <span class="yellow left">第1个个</span>
      <span class="green">第2个个个个个</span><span class="gray">第3个</span
      ><span class="pink">第4个</span>
    </div>
  </body>
  <style>
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
    .block1 {
      width: 100px;
      height: 20px;
    }
    .block2 {
      width: 200px;
      height: 50px;
    }
  </style>
</html>

float-25.png

  • 第一个例子:第一个文本元素与块级元素在两行分别左浮动。第二第三个文本元素左浮动。可以看到第四个文本元素去第一行补齐第一个文本元素右侧的空白,但是左浮动的文本元素却没有。
  • 第二个例子:三个块级元素左浮动。右侧的空白由文本元素补齐了。
  • 第三个例子:第一个文本元素左浮动。可以看到其它文本元素去补齐右侧,但是浮动的文本元素没有。

浮动流中不同高度行内元素

前面列举的元素中,行内元素中文本高度都是相同的,因此一行一行顺序排列。假设行内元素的文本高度不同,现象又会怎样呢?我们可以通过控制字号来实现对文本高度的控制。

场景举例

<html>
  <body>
    <div class="wrapper">
      <span class="yellow size14">第1个</span
      ><span class="green size35">第2个</span
      ><span class="gray size14">第3个</span
      ><span class="pink size35">第4个</span>
    </div>
    <div class="wrapper">
      <span class="yellow size14 left">第1个</span
      ><span class="green size35">第2个</span
      ><span class="gray size14">第3个</span
      ><span class="pink size35 left">第4个</span>
    </div>
    <div class="wrapper">
      <span class="yellow size14">第1个</span
      ><span class="green size35">第2个</span
      ><span class="gray size14">第3个</span
      ><span class="pink size35">第4个</span
      ><span class="brown size14">第5个个个个个个个个个个个个个个个个个个</span>
    </div>
    <div class="wrapper">
      <span class="yellow size14 left">第1个</span
      ><span class="green size35">第2个</span
      ><span class="gray size14"
        >第3个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="pink size35 left">第4个</span>
    </div>
    <div class="wrapper">
      <span class="yellow size14 left">第1个</span
      ><span class="green size35 left">第2个</span
      ><span class="gray size14"
        >第3个个个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="pink size35 left">第4个</span>
    </div>
  </body>
  <style>
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
    .size14 {
      font-size: 14;
    }
    .size35 {
      font-size: 35;
    }
  </style>
</html>

float-26.png

  • 第一个例子:两种不同高度的文本元素展示,未浮动做对比用。默认垂直对齐是baseline。
  • 第二个例子:第一个第四个元素左浮动。看到左浮动的1号元素的“垂直对齐方式”改变了。但实际上并未改变,观察vertical-align还是baseline。这是因为设置后变为了块级元素,是自己跟自己对齐,因此垂直对齐无意义了。
  • 第三个例子:最后增加了第五个元素,且长度超过一行,可以看到并没有填充左边的空白,而是把所有元素整体作为一行,它自己跨到第二行展示。
  • 第四个例子:第三个元素加长,使其换行,可以看到第四个元素到了最左边浮动。
  • 第五个例子:一二四元素都浮动,只有第三个元素非浮动。可以看到非浮动元素在浮动元素右侧换行,没有把左浮动都作为一行处理。

右侧换行规律

这里再列举几个右侧换行的例子:

<html>
  <body>
    <div class="wrapper">
      <span class="yellow size14">第1个个个</span
      ><span class="green size35">第2个个个个个个个</span
      ><span class="gray size14"
        >第3个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      >
    </div>
    <div class="wrapper">
      <span class="yellow size14">第1个个个</span
      ><span class="green size35 left">第2个个个个个个个</span
      ><span class="gray size14"
        >第3个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      >
    </div>
    <div class="wrapper">
      <span class="yellow size14 left">第1个个个</span
      ><span class="green size35">第2个个个个个个个</span
      ><span class="gray size14"
        >第3个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      >
    </div>
    <div class="wrapper">
      <span class="yellow size14 left">第1个个个</span
      ><span class="green size35 left">第2个个个个个个个</span
      ><span class="gray size14"
        >第3个个个个个个个个个个个个个个个个个个个个个个个个个个</span
      >
    </div>
    <div class="wrapper">
      <span class="yellow size14 left">第1个个个</span
      ><span class="green size35 left">第2个个个个个个个</span
      ><span class="gray size14">第3个</span
      ><span class="pink size14">第4个个个个个个个个个个个个个</span>
    </div>
    <div class="wrapper">
      <span class="yellow size14 left">第1个个个</span
      ><span class="green size35 left">第2个</span
      ><span class="gray size35">第3个</span
      ><span class="pink size14">第4个个个个个个个个个个个个个个个个个个个个个</span>
    </div>
    <div class="wrapper">
      <span class="yellow size14 left">第1个个个</span
      ><span class="green size35 left">第2个</span
      ><span class="gray size35 left">第3个</span
      ><span class="pink size14">第4个个个个个个个个个个个个个个个个个个个个个</span>
    </div>
  </body>
  <style>
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
    .size14 {
      font-size: 14;
    }
    .size35 {
      font-size: 35;
    }
  </style>
</html>

float-27.png

  • 第一个例子:一三是矮元素,二是高元素,均未设置浮动,做对比用。
  • 第二个例子:二元素浮动,一元素被挤到右边。三元素在右侧换行了。
  • 第三个例子:一元素浮动,二元素未浮动。这时候三元素没有在右边换行,而是整体换行了。
  • 第四个例子:一二元素均浮动,三元素在右侧换行。
  • 第五个例子:增加了第四个元素,且三元素变短,四元素在右侧换行了。
  • 第六个例子:三元素变高,这时候四元素没有在右侧换行,而是整体换行了。
  • 第七个例子:三元素变为左浮动,四元素又在右侧换行了。

从这些例子中,可以看出行内元素在右侧换行的规律:右侧换行的多个元素是一个整体,这个整体的左边的元素超过右边这个整体的行高,且是浮动元素。即假设这个换行元素左侧是非浮动元素,也可以换行,只要把右侧的非浮动行内元素作为整体来换行即可。但如果些非浮动元素第一行的行高(取每个元素最高值)达到或者超过左侧浮动元素的行高,则不在右侧换行,而是和浮动元素作为一个整体换行。

浮动流中的可替换元素与行内块元素

元素介绍

可替换元素是一类特殊的HTML元素,它内容的展现效果是由内部内容控制的,比如iframe和img元素。

行内块元素是同时具有行内元素和块级元素部分特性的元素,比如input和button元素。可以通过设置display: inlin-block来指定为行内块元素。

可替换元素和行内元素类似,在默认情况下它们都像行内元素一样,在同一行横向展示。但是也与块级元素类似,可以指定元素的宽高。其它特性在这里就不描述了。我们举例看一下这两种元素的展示效果:

<html>
  <body>
    <div>
      个个个个<img class="common" src="./1.jpg" />个个个个<button class="common">按钮</button>个个个个<button class="common vertical-top">按钮</button>个个个个<input class="common" />个个个个<div class="common yellow inline-block"></div>个个个个
      </div>
  </body>
  <style>
    .common {
      width: 100px;
      height: 100px;
    }
    .inline-block {
      display: inline-block;
    }
    .vertical-top {
      vertical-align: top;
    }
  </style>
</html>

float-28.png

上面的例子展示了在文本中的可替换元素与行内块元素的表现。可以看到它们都是行内元素,但可以设置宽度与高度。同时它们在行中的纵向位置表现不一样:

  • 第一个图片元素和最后一个手动设置inline-block的黄元素,默认和文字的底端对齐。
  • 第二个按钮和第四个input元素,因为其中有文字,所以和文字居中对齐。
  • 第三个按钮,我们手动设置了vertical-align: top,于是和行内元素的最高处对齐。

浮动表现

看完了默认表现,我们再看一下它们在浮动流中的表现。

<html>
  <body>
    <div class="wrapper" style="margin-bottom: 100px">
      <span class="yellow">第1个</span><span class="green">第2个</span
      ><img class="common left" src="./1.jpg" /><span class="gray">第3个</span
      ><span class="pink">第4个</span>
    </div>
    <div class="wrapper" style="margin-bottom: 100px">
      <span class="yellow">第1个个个个个个个个个</span
      ><span class="green">第2个个个个个个个个个个</span
      ><img class="common left" src="./1.jpg" /><span class="gray">第3个</span
      ><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <span class="yellow">第1个</span
      ><span class="green">第2个</span
      ><img class="common" src="./1.jpg" /><span class="gray left">第3个</span
      ><span class="pink">第4个</span>
    </div>
    <div class="wrapper" style="margin-bottom: 100px">
      <span class="yellow">第1个</span><span class="green">第2个</span
      ><input class="common left" /><span class="gray">第3个</span
      ><span class="pink">第4个</span>
    </div>
    <div class="wrapper" style="margin-bottom: 100px">
      <span class="yellow">第1个个个个个个个个个</span
      ><span class="green">第2个个个个个个个个个个</span
      ><input class="common left" /><span class="gray">第3个</span
      ><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <span class="yellow">第1个</span
      ><span class="green">第2个</span
      ><input class="common" /><span class="gray left">第3个</span
      ><span class="pink">第4个</span>
    </div>
    <div class="wrapper" style="margin-bottom: 100px">
      <span class="yellow">第1个</span><span class="green">第2个</span
      ><div class="common yellow inline-block left"></div><span class="gray">第3个</span
      ><span class="pink">第4个</span>
    </div>
    <div class="wrapper" style="margin-bottom: 100px">
      <span class="yellow">第1个个个个个个个个个</span
      ><span class="green">第2个个个个个个个个个个</span
      ><div class="common yellow inline-block left"></div><span class="gray">第3个</span
      ><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <span class="yellow">第1个</span
      ><span class="green">第2个</span
      ><div class="common yellow inline-block"></div><span class="gray left">第3个</span
      ><span class="pink">第4个</span>
    </div>
  </body>
  <style>
    .common {
      width: 100px;
      height: 100px;
    }
    .inline-block {
      display: inline-block;
    }
    .vertical-top {
      vertical-align: top;
    }
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
  </style>
</html>

float-29.png

例子比较长,但是没什么难点,可替换元素与行内块元素就像一个大块头的行内元素,具体表现和前面的部分场景是类似的。

  • 第一个例子:img元素浮动。
  • 第二个例子:img元素浮动的基础上,文本长度增加,右侧出现换行。
  • 第三个例子:img元素不浮动,单独一个文本元素浮动,虽然看起来纵向位置差较多,但实际上是在一行的。
  • 第四个例子:与第一个例子一样,但浮动元素变成了input,表现一致。
  • 第五个例子:与第二个例子一样,但浮动元素变成了input,表现一致。
  • 第六个例子:与第三个例子一样,但元素变成了input,除了垂直对齐外表现一致。
  • 第七个例子:与第一个例子一样,但浮动元素变成了行内块的div,表现一致。
  • 第八个例子:与第二个例子一样,但浮动元素变成了行内块的div,表现一致。
  • 第九个例子:与第三个例子一样,但元素变成了行内块的div,表现一致。

元素内部包含行内元素

块级元素内部可以包含行内元素,一些行内块元素内部也可以包含行内元素,那么包含行内元素时,浮动的表现又如何呢?

块级元素包含行内元素

<html>
  <body>
    <div class="wrapper">
      <span class="yellow">第1个</span><span class="green">第2个</span>
      <div class="orange">块级元素1</div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <span class="yellow">第1个</span><span class="green">第2个</span>
      <div class="orange left">块级元素1</div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <span class="yellow"
        >第1个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green">第2个</span>
      <div class="orange left">块级元素1</div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <span class="yellow">第1个</span><span class="green">第2个</span>
      <div class="orange left">
        块级元素1个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个
      </div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
  </body>
  <style>
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
  </style>
</html>

float-30.png

我们在一个div内部放置了行内元素,没有给这个div设置宽高。

  • 第一个例子:div元素未浮动,独占一整行,且高度由文本内容撑开。
  • 第二个例子:div元素设置浮动,元素实际宽高都由文本内容撑开。
  • 第三个例子:行内元素超长,浮动的div被挤到第二行。
  • 第四个例子:浮动的div超长,元素在第二行展示且独占两整行。

上面列举的例子看起来很常规,和前面正常块级元素的表现基本一致。下面我们试一下,当块级元素设置宽高,但不足以容纳其中内容时的表现。

<html>
  <body>
    <div class="wrapper">
      <div class="orange common">块级元素1个个个个个个个个个</div>
    </div>
    <div class="wrapper">
      <span class="yellow">第1个</span><span class="green">第2个</span>
      <div class="orange common">块级元素1个个个个个个个个个</div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <span class="yellow">第1个</span><span class="green">第2个</span>
      <div class="orange left common">块级元素1个个个个个个个个个</div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <span class="yellow"
        >第1个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green">第2个</span>
      <div class="orange left common">块级元素1个个个个个个个个个</div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <span class="yellow">第1个</span><span class="green">第2个</span>
      <div class="orange left common">块级元素1个个个个个个个个个</div>
      <span class="gray">第3个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="pink">第4个个个个个个个个个个个个个个个个个个个个个个个个个个</span>
    </div>
  </body>
  <style>
    .common {
      width: 100px;
      height: 40px;
    }
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 80px;
    }
  </style>
</html>

float-31.png

  • 第一个例子:单独列举了div元素的宽高不能容纳内容的例子。橘黄色背景的为div实际占用空间,文本内容超出展示了。
  • 第二个例子:将div元素放到行内元素中,可以看到后面的行内元素覆盖了超出的块级元素文本。
  • 第三个例子:div元素设置浮动,超出的文本元素依旧。
  • 第四个例子:前面的行内元素超长,浮动的div被挤到第二行。
  • 第五个例子:后面的行内元素超长,可以看到依旧覆盖了div中超出的文本元素。

行内块元素包含行内元素

<html>
  <body>
    <div class="wrapper">
      <span class="yellow">第1个</span><span class="green">第2个</span>
      <div class="orange inline-block">块级元素1</div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <span class="yellow">第1个</span><span class="green">第2个</span>
      <div class="orange left inline-block">块级元素1</div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <span class="yellow"
        >第1个个个个个个个个个个个个个个个个个个个个个个个</span
      ><span class="green">第2个</span>
      <div class="orange left inline-block">块级元素1</div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
    <div class="wrapper">
      <span class="yellow">第1个</span><span class="green">第2个</span>
      <div class="orange left inline-block">
        块级元素1个个个个个个个个个个个个个个个个个个个个个个个个个个个个个个
      </div>
      <span class="gray">第3个</span><span class="pink">第4个</span>
    </div>
  </body>
  <style>
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
    .inline-block {
      display: inline-block;
    }
  </style>
</html>

float-32.png

可以看到,除了第一个未浮动的例子行内块元素和文本元素一行之外,其它例子和块级元素一摸一样,因此这里不再赘述了。下面再来看一下设置宽高,但不足以容纳其中内容时的表现。

<html>
  <body>
    <body>
      <div class="wrapper">
        <div class="orange common">块级元素1个个个个个个个个个</div>
      </div>
      <div class="wrapper">
        <span class="yellow">第1个</span><span class="green">第2个</span>
        <div class="orange common">块级元素1个个个个个个个个个</div>
        <span class="gray">第3个</span><span class="pink">第4个</span>
      </div>
      <div class="wrapper">
        <span class="yellow">第1个</span><span class="green">第2个</span>
        <div class="orange left common">块级元素1个个个个个个个个个</div>
        <span class="gray">第3个</span><span class="pink">第4个</span>
      </div>
      <div class="wrapper">
        <span class="yellow"
          >第1个个个个个个个个个个个个个个个个个个个个个个个</span
        ><span class="green">第2个</span>
        <div class="orange left common">块级元素1个个个个个个个个个</div>
        <span class="gray">第3个</span><span class="pink">第4个</span>
      </div>
      <div class="wrapper">
        <span class="yellow">第1个</span><span class="green">第2个</span>
        <div class="orange left common">块级元素1个个个个个个个个个</div>
        <span class="gray">第3个个个个个个个个个个个个个个个个个个个个个</span
        ><span class="pink">第4个个个个个个个个个个个个个个个个个个个个个个个个个个</span>
      </div>
      <div class="wrapper">
        <span class="yellow">第1个</span><span class="green">第2个</span>
        <div class="orange common vertical-top">块级元素1个个个个个个个个个</div>
        <span class="gray">第3个</span><span class="pink">第4个</span>
      </div>
  </body>
  <style>
    .common {
      width: 100px;
      height: 40px;
      display: inline-block;
    }
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 80px;
    }
  </style>
</html>

float-33.png

这里只有第二个例子与上面块级元素不一样。虽然行内块元素中的文本超过了宽高,但第二个例子(也只有这个例子)为其留出了位置。如果我们设置div元素vertical-align: top,(看第六个例子),则超过的区域又没有被父级包裹。猜测应该是垂直对齐通过作用于行内元素,影响了父元素的包裹区域大小。

clear属性清除浮动

在CSS中还有个clear属性,用于清除浮动,取值有:none、left、right、both,对应不清除,清除左边,清除右边,左右都清除。虽然看起来叫做“clear”、“清除浮动”,但实际上不能起到“清除”的作用,只能说是“避开”,而且应用于行内元素无效。

clear用于块级元素

看一下clear属性在块级元素上的表现,也通过这些例子了解clear属性的作用。首先看一下clear作用于浮动元素上的例子。

<html>
  <body>
    <body>
      <div class="wrapper">
        <div class="orange common right"></div>
        <span class="yellow">第1个</span><span class="green">第2个</span>
        <div class="teal common left"></div>
        <span class="gray">第3个</span><span class="pink">第4个</span>
        <div class="purple common left"></div>
      </div>
      <div class="wrapper">
        <div class="orange common right"></div>
        <span class="yellow">第1个</span><span class="green">第2个</span>
        <div class="teal common left"></div>
        <span class="gray">第3个</span><span class="pink">第4个</span>
        <div class="purple common left clear-left"></div>
      </div>
      <div class="wrapper">
        <div class="orange common right"></div>
        <span class="yellow">第1个</span><span class="green">第2个</span>
        <div class="teal common left clear-left"></div>
        <span class="gray">第3个</span><span class="pink">第4个</span>
        <div class="purple common left"></div>
      </div>
      <div class="wrapper">
        <div class="orange common right"></div>
        <span class="yellow">第1个</span><span class="green">第2个</span>
        <div class="teal common left clear-right"></div>
        <span class="gray">第3个</span><span class="pink">第4个</span>
        <div class="purple common left"></div>
      </div>
      <div class="wrapper">
        <div class="orange common right"></div>
        <span class="yellow">第1个</span><span class="green">第2个</span>
        <div class="teal common left"></div>
        <span class="gray">第3个</span><span class="pink">第4个</span>
        <div class="purple common left clear-right"></div>
      </div>
  </body>
  <style>
    .common {
      width: 100px;
      height: 40px;
    }
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 80px;
    }
  </style>
</html>

float-34.png

  • 第一个例子:三个块级元素,两个左浮动,一个右浮动,未设置clear,做对比用。
  • 第二个例子:紫元素clear:left,因为前面有一个左浮动元素,因此避开到第二行展示。
  • 第三个例子:青元素clear:left,但是前面并没有左浮动元素了,因此无变化。注意clear并不会清除自己身上的浮动,也并不会清除其它元素上的浮动。
  • 第四个例子:青元素clear:right。它的前面有一个右浮动元素,因此避开到第二行展示。后面的左浮动元素跟着也到了第二行展示。
  • 第五个例子:紫元素clear:right。它的前面有一个右浮动元素,因此避开到第二行展示。

从上面几个例子可以看到,clear并不会清除自身的浮动属性,也更不会影响和清除其它浮动元素的属性。它仅仅是通过改变自身的位置,避开前面元素的浮动行,另起一行展示而已。

<html>
  <body>
    <body>
      <div class="wrapper">
        <div class="red common left clear-right"></div>
        <div class="yellow common left"></div>
        <div class="green common left"></div>
        <div class="blue common right"></div>
      </div>
      <div class="wrapper" style="margin-bottom: 120px">
        <div class="blue common right"></div>
        <div class="red common left clear-right"></div>
        <div class="yellow common left"></div>
        <div class="green common left"></div>
      </div>
      <div class="wrapper">
        <div class="red common left"></div>
        <div class="yellow common left clear-right"></div>
        <div class="green common left"></div>
      </div>
      <div class="wrapper" style="margin-bottom: 120px">
        <div class="red common left"></div>
        <div class="yellow common left clear-left"></div>
        <div class="green common left"></div>
      </div>
      <div class="wrapper">
        <div class="red common left"></div>
        <div class="yellow common left clear-both"></div>
        <div class="green common left"></div>
      </div>
  </body>
  <style>
    .common {
      width: 100px;
      height: 40px;
    }
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 80px;
    }
  </style>
</html>

float-35.png

  • 第一个例子:三个元素左浮动,一个元素右浮动。第一个左浮动元素设置clear:right,但是并无效果。因为右浮动元素在HTML中的位置在后面,因此无法影响到。
  • 第二个例子:右浮动元素在HTML中的位置提到前面,这时候左浮动元素设置clear:right,就有效果了。
  • 第三个例子:只有三个左浮动元素,中间元素设置了clear:right,效果和未设置一样。因为中间元素的HTML位置中的前面并没有右浮动元素。
  • 第四个例子:三个左浮动元素,中间元素设置了clear:left,第二个元素和它后面的浮动元素被移动到第二行展示。
  • 第五个例子:中间元素设置了clear:both,相当于第三个和第四个例子的结合。

通过上面的例子,我们可以看到,clear只能判断HTML中位置处于前面的元素是否有左右浮动。我们再看一下clear应用于非浮动元素的例子。

<html>
  <body>
    <body>
      <div class="wrapper">
        <div class="red common left"></div>
        <div class="yellow common left"></div>
        <div class="green common left"></div>
        <div class="blue commonHeight"></div>
      </div>
      <div class="wrapper">
        <div class="red common left"></div>
        <div class="yellow common left"></div>
        <div class="green common left"></div>
        <div class="blue commonHeight clear-left"></div>
      </div>
      <div class="wrapper">
        <div class="red common left"></div>
        <div class="yellow common left"></div>
        <div class="green commonHeight left"></div>
        <div class="blue commonHeight"></div>
      </div>
      <div class="wrapper">
        <div class="red common left"></div>
        <div class="yellow common left"></div>
        <div class="green commonHeight left"></div>
        <div class="blue commonHeight clear-left"></div>
      </div>
      <div class="wrapper">
        <div class="red common left"></div>
        <div class="yellow commonHeight left"></div>
        <div class="green common left"></div>
        <div class="blue commonHeight clear-left"></div>
      </div>
      <div class="wrapper">
        <div class="red common left"></div>
        <div class="yellow commonHeight right"></div>
        <div class="green common left"></div>
        <div class="blue commonHeight clear-left"></div>
      </div>
      <div class="wrapper">
        <div class="red common left"></div>
        <div class="yellow commonHeight right"></div>
        <div class="green common left"></div>
        <div class="blue commonHeight clear-right"></div>
      </div>
  </body>
  <style>
    .common {
      width: 100px;
      height: 40px;
    }
    .commonHeight {
      width: 100px;
      height: 60px;
    }
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
  </style>
</html>

float-36.png

  • 第一个例子:前三个元素左浮动,第四个更高的蓝元素不浮动,因为前面浮动元素不占位置,因此重叠展示。
  • 第二个例子:第四个更高的蓝元素设置了clear:left,可以看到蓝元素靠下展示,没有和浮动元素重叠。
  • 第三个例子:未设置clear,第三个浮动元素高度提高,做对比用。
  • 第四个例子:第四个蓝元素设置了clear:left,由于第三个浮动是高元素,因此蓝元素也避开了这部分高度。
  • 第五个例子:换到了第二个浮动元素是高元素,四个蓝元素位置不变。
  • 第六个例子:第二个浮动元素变为了右浮动。因为第四个蓝元素是clear:left,因此没有空出右浮动元素高的这部分区域。
  • 第七个例子:第四个蓝元素设置为clear:right,可以看到有空出右浮动元素高的这部分区域。

通过上面的例子可以看到,clear属性也能作用于非浮动元素,而且效果类似,都使得被设置的元素避开前面的浮动元素。

clear用于行内元素

<html>
  <body>
    <body>
      <div class="wrapper">
        <span class="red">第1个</span><span class="yellow right">第2个</span
          ><span class="green">第3个</span><span class="blue left">第4个</span
          ><span class="gray">第5个</span><span class="pink left">第6个</span
          ><span class="brown left">第7个</span>
      </div>
      <div class="wrapper">
        <span class="red">第1个</span><span class="yellow right">第2个</span
          ><span class="green">第3个</span><span class="blue left">第4个</span
          ><span class="gray">第5个</span><span class="pink left clear-left">第6个</span
          ><span class="brown left">第7个</span>
      </div>
      <div class="wrapper">
        <span class="red">第1个</span><span class="yellow right">第2个</span
          ><span class="green">第3个</span><span class="blue left">第4个</span
          ><span class="gray">第5个</span><span class="pink left clear-right">第6个</span
          ><span class="brown left">第7个</span>
      </div>
      <div class="wrapper">
        <span class="red">第1个</span><span class="yellow right">第2个</span
          ><span class="green">第3个</span><span class="blue left">第4个</span
          ><span class="gray clear-both">第5个</span><span class="pink left">第6个</span
          ><span class="brown left">第7个</span>
      </div>
  </body>
  <style>
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
  </style>
</html>

float-37.png

上面的例子展示了行内元素clear的场景,我们具体分析一下:

  • 第一个例子:第四个第六个第七个元素左浮动,第二个元素右浮动,做对比用。
  • 第二个例子:第六个元素设置了clear:left,因此避开到第二行展示。后面的左浮动第七个元素跟随到了第二行。
  • 第三个例子:第六个元素设置了clear:right,因此避开到第二行展示。后面的左浮动第七个元素跟随到了第二行。
  • 第四个例子:第五个非浮动元素设置了clear:both,现象与不设置一样无变化。

从上面的例子可以看到,对于行内元素来说,对浮动元素设置clear有效,对非浮动元素设置无效。但这背后的原因是行内元素浮动后,display属性变化,会变为块级元素,因此设置有效。综合来看,clear属性对于行内元素的设置是无效的。

<html>
  <body>
    <body>
      <div class="wrapper">
        <span class="red">第1个</span><button class="common left">按钮1</button><span class="yellow">第2个</span
          ><button class="common right">按钮2</button><span class="green">第3个</span><button class="common left"
          >按钮3</button><span class="blue">第4个</span
          >
      </div>
      <div class="wrapper" style="margin-bottom: 100px">
        <span class="red">第1个</span><button class="common left">按钮1</button><span class="yellow">第2个</span
          ><button class="common right clear-left">按钮2</button><span class="green">第3个</span><button class="common left"
          >按钮3</button><span class="blue">第4个</span
          >
      </div>
      <div class="wrapper">
        <span class="red">第1个</span><button class="common left">按钮1</button><span class="yellow">第2个</span
          ><button class="common right">按钮2</button><span class="green">第3个</span><button class="common"
          >按钮3</button><span class="blue">第4个</span
          >
      </div>
      <div class="wrapper">
        <span class="red">第1个</span><button class="common left">按钮1</button><span class="yellow">第2个</span
          ><button class="common right">按钮2</button><span class="green">第3个</span><button class="common clear-left"
          >按钮3</button><span class="blue">第4个</span
          >
      </div>
      <div class="wrapper">
        <span class="red">第1个</span><button class="common left">按钮1</button><span class="yellow">第2个</span
          ><button class="common right">按钮2</button><span class="green">第3个</span><button class="common"
          >按钮3</button><span class="blue clear-left">第4个</span
          >
      </div>
      <div class="wrapper">
        <div class="common left pink"></div>
        <span class="red">第1个</span><button class="common left">按钮1</button><span class="yellow">第2个</span
          ><button class="common clear-left">按钮2</button><span class="green clear-left">第3个</span>
      </div>
  </body>
  <style>
    .common {
      width: 80px;
      height: 40px;
    }
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
  </style>
</html>

float-38.png

这些例子展示了行内元素,行内块元素,块级元素组合使用clear时的场景,可以发现行内元素与行内块元素对于clear的规律是一致的:

  • 第一个例子:三个按钮,两个左浮动,一个右浮动,作对比用。
  • 第二个例子:按钮2设置clear:left,按钮2和3避开到第二行展示。
  • 第三个例子:按钮1和2设置浮动,按钮3未设置浮动,做对比用。
  • 第四个例子:按钮3未浮动,设置clear:left,没有效果。
  • 第五个例子:文本4未浮动,设置clear:left,没有效果。
  • 第六个例子:最前面增加了一个左浮动块级元素,按钮2和文本3均未浮动,设置clear:left,没有效果。

浮动的父元素塌陷与解决方案

问题描述

由于浮动元素不占空间,父元素可能无法把其中的浮动元素完全包裹在内。这时候浮动元素会覆盖后面其它元素的展示,这种特点会对我们造成困扰。这些解决父元素塌陷问题的方法,被叫做clearfix。

<html>
  <body>
    <body>
      <div class="wrapper">
        <img src="./1.jpg" class="common left">
        <span>第一段文字。第一段文字。第一段文字。第一段文字。第一段文字。第一段文字。第一段文字。第一段文字。第一段文字。第一段文字。</span>
      </div>
      <div class="wrapper">
        <div class="common2 red"></div>
        第二段文字。第二段文字。第二段文字。第二段文字。第二段文字。第二段文字。第二段文字。第二段文字。
      </div>
  </body>
  <style>
    .common {
      width: 100px;
      height: 100px;
    }
    .common2 {
      width: 120px;
      height: 20px;
    }
    .wrapper {
      border: 1px dotted blue;
    }
  </style>
</html>

float-39.png

在上面的例子中,第一段有一个浮动的大图片,它的高度超过了第一段的父元素的高度,侵占了第二段的部分空间,把第二段中红色div的大部分区域都挡住了。这就是浮动的父元素塌陷问题。

下面会介绍很多种父元素塌陷问题的解决方案,这些解决方案不仅能应用到父元素,也能应用到后面被覆盖的元素上解决问题。

方法:父元素设置固定高度

最简单的方法就是给父元素设置一个固定的高度,或者固定的底部padding,margin等高度,即可以包裹进浮动元素。

<html>
  <body>
    <body>
      <div class="wrapper" style="height: 120px">
        <img src="./1.jpg" class="common left">
        <span>第一段文字。第一段文字。第一段文字。第一段文字。第一段文字。</span>
      </div>
      <div class="wrapper">
        <div class="common2 red"></div>
      </div>
  </body>
  <style>
    .common {
      width: 100px;
      height: 100px;
    }
    .common2 {
      width: 120px;
      height: 20px;
    }
    .wrapper {
      border: 1px dotted blue;
    }
  </style>
</html>

float-40.png

在上面的例子中,我们给父级div设置了固定高度,下面的内容就可以露出来了。但这种方法太死板,不能根据浮动内容的高度自适应。

方法:clear和空块级元素

前面我们聊过,使用css中的clear属性,可以让块级元素避开浮动元素,在后面新起一行展示。利用这个特性,可以做到自适应浮动内容的高度。

<html>
  <body>
    <body>
      <div class="wrapper">
        <img src="./1.jpg" class="common left">
        <span>第一段文字。第一段文字。第一段文字。第一段文字。第一段文字。</span>
        <div class="clear-both"></div>
      </div>
      <div class="wrapper">
        <div class="common2 red"></div>
      </div>
  </body>
  <style>
    .common {
      width: 100px;
      height: 100px;
    }
    .common2 {
      width: 120px;
      height: 20px;
    }
    .wrapper {
      border: 1px dotted blue;
    }
  </style>
</html>

float-41.png

在父元素的最后,添加一个空的块级元素,设置clear,即可让父元素的区域包含这个空块级元素以及上面的浮动元素。由于空块级元素并不在视觉上占空间,因此页面展示中看不出它的存在。

方法:clear和after伪类

有些人会觉得在html中增加空元素不太美观,这时候还可以使用after伪类来代替空块级元素。

<html>
  <body>
    <body>
      <div class="wrapper after">
        <img src="./1.jpg" class="common left">
        <span>第一段文字。第一段文字。第一段文字。第一段文字。第一段文字。</span>
      </div>
      <div class="wrapper">
        <div class="common2 red"></div>
      </div>
  </body>
  <style>
    .after::after {
      content: "";
      display: block;
      clear: both;
    }
    .common {
      width: 100px;
      height: 100px;
    }
    .common2 {
      width: 120px;
      height: 20px;
    }
    .wrapper {
      border: 1px dotted blue;
    }
  </style>
</html>

float-41.png

使用after伪类在父元素的最后添加一个空元素,设置为块级且clear,这时候就能起到和html中设置空块级元素一样的效果了。

方法:使用BFC(overflow)

区块格式化上下文(Block Formatting Context,BFC)是一个以特定方式渲染的区域,在这里我们并不详细介绍BFC的特点,但是BFC可以包含内部浮动与排除外部浮动,解决浮动的父元素塌陷问题。

float-42.png

很多种方式可以创建一个BFC,这里我们以比较常用的overflow为例。

<html>
  <body>
    <body>
      <div class="wrapper overflow">
        <img src="./1.jpg" class="common left">
        <span>第一段文字。第一段文字。第一段文字。第一段文字。第一段文字。</span>
      </div>
      <div class="wrapper">
        <div class="common2 red"></div>
      </div>
  </body>
  <style>
    .overflow {
      overflow: auto;
    }
    .common {
      width: 100px;
      height: 100px;
    }
    .common2 {
      width: 120px;
      height: 20px;
    }
    .wrapper {
      border: 1px dotted blue;
    }
  </style>
</html>

float-41.png

可以看到,设置overflow这个看起来不相关的属性之后,形成了一个BFC区块,区块中包含了所有浮动元素。

方法:使用BFC(flow-root)

使用overflow虽然好用,但是强制指定了overflow属性。有没有无副作用创建BFC的方式呢?那就是flow-root。设置display:flow-root可以创建一个BFC区块。通过下面的例子可以看到,效果和overflow一样。

<html>
  <body>
    <body>
      <div class="wrapper flowroot">
        <img src="./1.jpg" class="common left">
        <span>第一段文字。第一段文字。第一段文字。第一段文字。第一段文字。</span>
      </div>
      <div class="wrapper">
        <div class="common2 red"></div>
      </div>
  </body>
  <style>
    .flowroot {
      display: flow-root;
    }
    .common {
      width: 100px;
      height: 100px;
    }
    .common2 {
      width: 120px;
      height: 20px;
    }
    .wrapper {
      border: 1px dotted blue;
    }
  </style>
</html>

float-41.png

定位流

所谓定位流实际上就是CSS中的position属性。position定位属性在CSS中被经常使用,但定位流这个概念并不常见,一般只有在与浮动做对比的时候才提到。定位是针对于单个元素的,且方式有很多种,因此用一种“定位流”来描述我感觉并不准确。常用的定位方式有如下几种:

  • 静态定位 static
  • 相对定位 relative
  • 绝对定位 absolute
  • 固定定位 fixed
  • 粘性定位 sticky

除了静态定位外,与定位属性结合的还有top, bottom, left, right属性,分别设置距离上方,下方,左方,右方的偏移。下面分别简单介绍这几种定位方式。

静态定位 static

静态定位即正常的布局模式,遵守正常的文档流规则,也是position属性的默认值。由于无变化,这里就不提供例子了。

相对定位 relative

相对定位模式下,元素先按照正常的文档流规则定位。然后再根据偏移属性移动元素。移动后,原位置的空白依然被保留。

<html>
  <body>
    <body>
      <div class="wrapper">
        <div class="common red"></div>
        <div class="common yellow"></div>
        <div class="common green"></div>
      </div>
      <div class="wrapper">
        <div class="common red"></div>
        <div class="common yellow relative"></div>
        <div class="common green"></div>
      </div>
  </body>
  <style>
    .common {
      width: 100px;
      height: 100px;
      display: inline-block;
    }
    .relative {
      position: relative;
      top: 10px;
      left: 20px;
    }
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
  </style>
</html>

float-43.png

有两个例子,第一个例子没有设置定位,做对比用。第二个例子设置了相对定位,可以看到向右和向下偏移了部分位置,但偏移前的空白依然是保留的。

绝对定位 absolute

绝对定位是基于已定位的父级元素(非static)来进行定位的。定位元素会被移出文档流,偏移量是根据已定位的父级元素位置来偏移。

<html>
  <body>
    <body>
      <div class="wrapper">
        <div class="common red"></div>
        <div class="common yellow"></div>
        <div class="common green"></div>
      </div>
      <div class="wrapper" style="position:relative">
        <div class="common red"></div>
        <div class="common yellow absolute"></div>
        <div class="common green"></div>
      </div>
  </body>
  <style>
    .common {
      width: 100px;
      height: 100px;
      display: inline-block;
    }
    .absolute {
      position: absolute;
      top: 10px;
      left: 20px;
    }
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
  </style>
</html>

float-44.png

第一个例子依然没有设置定位,做对比用。第二个例子看到黄色元素原来的空位没有了,偏移定位也是基于父级元素位置。

固定定位 fixed

固定定位的元素也会被移出文档流,但定位偏移是根据屏幕视口的位置来偏移。

<html>
  <body>
    <body>
      <div class="wrapper">
        <div class="common red"></div>
        <div class="common yellow"></div>
        <div class="common green"></div>
      </div>
      <div class="wrapper">
        <div class="common red"></div>
        <div class="common yellow fixed"></div>
        <div class="common green"></div>
      </div>
  </body>
  <style>
    .common {
      width: 100px;
      height: 100px;
      display: inline-block;
    }
    .fixed {
      position: fixed;
      top: 100px;
      left: 200px;
    }
    .wrapper {
      border: 1px dotted blue;
      margin-bottom: 40px;
    }
  </style>
</html>

float-45.png

与absolute一样,没有留出空位置,但是偏移基于的位置变化了。注意fix是根据屏幕的位置来偏移的,拖动滚动条时的效果为:固定定位的元素位置不动,下面其它正常元素随着滚动条位置移动。例如对上面的例子向下拖动的效果:

float-46.png

粘性定位 sticky

网上大部分关于粘性定位的说明是:相对定位和固定定位的结合。在达到偏移的位置前为相对定位,达到后则为固定定位。但是据我测试,达到偏移的位置前与相对定位不完全一样,到偏移的位置后与固定定位也并不完全一样。我们具体来看下:

<html>
  <body>
    <body>
      <div class="wrapper">
        <div class="common red"></div>
        <div class="common yellow"></div>
        <div class="common green"></div>
      </div>
      <div class="wrapper">
        <div class="common red relative"></div>
        <div class="common yellow"></div>
        <div class="common green"></div>
      </div>
      <div class="wrapper">
        <div class="common red fixed"></div>
        <div class="common yellow"></div>
        <div class="common green"></div>
      </div>
      <div class="wrapper">
        <div class="common red sticky"></div>
        <div class="common yellow"></div>
        <div class="common green"></div>
      </div>
      <div class="wrapper">
        <div class="common red sticky"></div>
      </div>
      <div class="wrapper">
        <div class="gray" style="width: 100px; height: 10px"></div>
        <div class="common red sticky"></div>
        <div class="common yellow"></div>
        <div class="common green"></div>
      </div>
      <div class="wrapper" style="position: relative; top: 15px">
        <div class="common red sticky"></div>
        <div class="common yellow"></div>
        <div class="common green"></div>
      </div>
      <div class="wrapper">
         <div class="common gray"></div>
        <div class="common red sticky"></div>
        <div class="common yellow"></div>
        <div class="common green"></div>
      </div>
  </body>
  <style>
    .common {
      width: 100px;
      height: 100px;
    }
    .wrapper {
      border: 1px dotted blue;
      width: 130px;
      float: left;
    }
    .relative {
      position: relative;
      top: 30px;
    }
    .fixed {
      position: fixed;
      top: 30px;
    }
    .sticky {
      position: sticky;
      top: 30px;
    }
  </style>
</html>

float-47.png

float-48.png

float-49.png

粘性定位相对比较复杂,因此提供了8个例子。首先注意到第一个图,最上面有一条横线,那是浏览器窗口的顶部。默认body会有8px的margin,因此所有的父级元素都是不到顶部的。这里不同例子中,偏移量都是top:30px。

第一张图是默认最顶部无滚动条的场景,第二张图是滚动了一部分,第三张图是滚动更多。下面我们对每个例子进行分析。

  • 第一个例子:红黄绿三个元素纵向排列,没有设置定位,做对比用。
  • 第二个例子:红元素设置了相对定位。可以看到红元素位置下移,其他元素未动。红元素偏移是相对于原有的自身位置,在这里是父元素的顶部(蓝线)。
  • 第三个例子:红元素设置了固定定位。可以看到红元素位置下移,其他元素上移,没有给红元素留出空间。另外固定定位的偏移相对于视口,因此偏移后会比第二个例子的位置更高一些。滚动条向下拖动时,红元素保持屏幕视口的偏移位置不动。
  • 第四个例子:红元素设置了粘性定位。红元素的下移位置并不像相对定位那样基于父元素下移,而是类似与固定定位那样根据视口的位置下移。滚动条向下拖动时,红元素保持屏幕视口的偏移位置不动。
  • 第五个例子:只有一个粘性定位的红元素。可以看到位置并没有偏移;向下滚动时红元素也没有动,与屏幕视口的位置无关。
  • 第六个例子:粘性定位的红元素上面增加了一个小小的div元素,它没有影响红元素的定位位置和屏幕视口的偏移位置。
  • 第七个例子:父元素下移了10px,可以看到依旧没有影响粘性定位的红元素。
  • 第八个例子:红元素上方放置了灰元素,高度超过了原有红元素默认情况下的偏移量。可以看到红元素并没有保持默认情况的偏移位置,而是被灰元素挤到下面去了。当滚动条向下拖动到了红元素的偏移位置,此时红元素就固定在屏幕视口的偏移位置了。

从上面这些例子,可以看到粘性定位与相对定位和固定定位都不完全一致,不能直接等同。这里描述一下特点:

  1. 粘性定位受限于父组件的位置,不管达到偏移前后,是相对还是固定模式,都不能超过父组件容器。如果超过则没有“粘性”的效果了。
  2. 达到偏移前,元素类似于“静态定位”,或者说是偏移位置不生效的“相对定位”。
  3. 滚动达到偏移时,类似固定定位,元素固定在屏幕视口的偏移位置。
  4. 初始位置在偏移后时,类似固定定位,元素初始在屏幕视口偏移的位置。当滚动时,元素也固定在屏幕视口的偏移位置。

注意例子里面的达到偏移前,代表的是第八个例子;初始位置在偏移后,代表的是第四个例子。关于粘性定位这里只是简单描述,粘性定位还有其它一些特点,这里就略过了。

不同定位模式与文本流

这里我们看一下不同定位模式对文本流的影响。为啥不试一下定位模式对文档流的影响呢?这是因为上面介绍各种定位模式时,实际上就是在文档流中演示的,因此不用单独介绍了。

<html>
  <body>
    <body>
      <div class="wrapper">
        <div class="common red "></div>
        <span class="green">我是行内元素。我是行内元素。我是行内元素。我是行内元素。我是行内元素。</span>
        <div class="common yellow"></div>
      </div>
      <div class="wrapper">
        <div class="common red relative"></div>
        <span class="green">我是行内元素。我是行内元素。我是行内元素。我是行内元素。我是行内元素。</span>
        <div class="common yellow"></div>
      </div>
      <div class="wrapper" style="position: relative">
        <div class="common red absolute"></div>
        <span class="green">我是行内元素。我是行内元素。我是行内元素。我是行内元素。我是行内元素。</span>
        <div class="common yellow"></div>
      </div>
      <div class="wrapper">
        <div class="common red fixed"></div>
        <span class="green">我是行内元素。我是行内元素。我是行内元素。我是行内元素。我是行内元素。</span>
        <div class="common yellow"></div>
      </div>
      <div class="wrapper">
        <div class="common red sticky"></div>
        <span class="green">我是行内元素。我是行内元素。我是行内元素。我是行内元素。我是行内元素。</span>
        <div class="common yellow"></div>
      </div>
      <div class="wrapper">
        <div class="common yellow"></div>
        <div class="common red sticky"></div>
        <span class="green">我是行内元素。我是行内元素。我是行内元素。我是行内元素。我是行内元素。</span>
      </div>
  </body>
  <style>
    .common {
      width: 100px;
      height: 100px;
    }
    .wrapper {
      border: 1px dotted blue;
      width: 150px;
      float: left;
    }
    .static {
      position: static;
      top: 30px;
    }
    .relative {
      position: relative;
      top: 30px;
    }
    .absolute {
      position: absolute;
      top: 30px;
    }
    .fixed {
      position: fixed;
      top: 30px;
    }
    .sticky {
      position: sticky;
      top: 30px;
    }
  </style>
</html>

float-50.png

float-51.png

上面列举了每个定位模式,并且下面放置了文本元素,我们一个一个分析哪些模式会脱离文本流,即文本不会为定位的红元素留出空间。

  • 第一个例子:静态定位,相当于未设置定位,当然未脱离文本流。
  • 第二个例子:相对定位,文本流为偏移前的元素位置留出空间,这和文档流一致。
  • 第三个例子:绝对定位,已脱离文本流。
  • 第四个例子:固定定位,已脱离文本流。
  • 第五个例子:粘性定位,且初始位置在偏移后。类似于相对定位,文本流为偏移前的元素位置留出空间。向下滚动时文本流依然为偏移前的元素位置留出空间,和文档流一致。
  • 第六个例子:粘性定位,且初始位置在偏移前。类似于未设置定位,未脱离文本流。

总结

这篇文档讨论了文档流,文本流,浮动流,定位流以及它们之间的关系。其中浮动流花的时间最长,讨论也最细致。这也是我一开始的目的,就是想通过这个主题认真了解下浮动。通过了解我觉得浮动是比较复杂的CSS特性,即使我熟悉了排列方式,对于复杂场景如果不实际实验,还是较难猜测出展示效果。不过浮动流的应用场景现在并不太多,至少我平时开发中不太常用。

参考

Python迭代器与生成器全解析:让你的代码高效又优雅!

一、什么是迭代器(Iterator)?

1.1 迭代器的定义

迭代器是一个可以记住遍历位置的对象。它实现了两个方法:__iter__()__next__()
任何实现了这两个方法的对象都可以称为迭代器。

1.2 为什么需要迭代器?

  • 节省内存:不需要一次性加载所有数据
  • 惰性计算:按需生成数据,适合处理大数据集
  • 代码简洁:统一的遍历接口

1.3 可迭代对象与迭代器的区别

  • 可迭代对象(Iterable):实现了__iter__()方法,如list、tuple、dict、str等
  • 迭代器(Iterator):实现了__iter__()__next__()方法

1.4 例子:for循环的本质

nums = [1, 2, 3]
it = iter(nums)  # 获取迭代器
print(next(it))  # 1
print(next(it))  # 2
print(next(it))  # 3
# print(next(it))  # StopIteration异常

二、自定义迭代器

2.1 实现一个自定义迭代器

class MyRange:
    def __init__(self, start, end):
        self.current = start
        self.end = end

    def __iter__(self):
        return self

    def __next__(self):
        if self.current < self.end:
            num = self.current
            self.current += 1
            return num
        else:
            raise StopIteration

for i in MyRange(1, 4):
    print(i)  # 输出 1 2 3

三、生成器(Generator)

3.1 生成器的定义

在 Python 中,使用了 yield 的函数被称为生成器(generator)。 生成器是一种特殊的迭代器,用于生成序列。它更简单、更强大,使用yield语句返回数据。

3.2 生成器函数

def my_generator():
    yield 1
    yield 2
    yield 3

for value in my_generator():
    print(value)  # 输出 1 2 3

3.3 生成器表达式

gen = (x * x for x in range(3))
for num in gen:
    print(num)  # 输出 0 1 4

四、生成器的高级用法

4.1 惰性求值与大数据处理

生成器不会一次性生成所有数据,而是每次请求时才生成下一个值,非常适合处理大文件或无限序列。

def read_large_file(file_path):
    with open(file_path) as f:
        for line in f:
            yield line.strip()

4.2 生成器的send()throw()close()

生成器不仅可以next(),还可以通过send()向生成器内部传值。

def echo():
    received = yield "start"
    while True:
        received = yield received

gen = echo()
print(next(gen))         # 输出 start
print(gen.send("hello")) # 输出 hello
print(gen.send("world")) # 输出 world

五、迭代器与生成器的最佳实践

5.1 用生成器处理大数据

def count_up_to(n):
    count = 1
    while count <= n:
        yield count
        count += 1

for num in count_up_to(1000000):
    pass  # 不会占用太多内存,因为每次只生成一个数字

5.2 组合生成器

def gen1():
    yield from range(3)

def gen2():
    yield from gen1()
    yield 100

for i in gen2():
    print(i)  # 输出 0 1 2 100

5.3 反向迭代

list1 = [1,2,3,4,5]
for num1 in reversed(list1) : # Python 中有内置的函数 `reversed()`
    print ( num1 , end = ' ' )

注意:反向迭代仅仅当对象的大小可预先确定或者对象实现了 __reversed__() 的特殊方法时才能生效。 如果两者都不符合,那你必须先将对象转换为一个列表才行

5.4 同时迭代多个序列

names = ['laingdianshui', 'twowater', '两点水']
ages = [18, 19, 20]
for name, age in zip(names, ages): # 
     print(name,age)

注意:zip(a, b) 会生成一个可返回元组 (x, y) 的迭代器,其中 x 来自 a,y 来自 b。 一旦其中某个序列到底结尾,迭代宣告结束。 因此迭代长度跟参数中最短序列长度一致


六、常见误区与注意事项

  • 生成器只能遍历一次,遍历完后就“耗尽”了
  • 不要滥用生成器表达式,复杂逻辑建议用生成器函数
  • StopIteration异常是生成器和迭代器结束的标志

最后

如果你觉得这篇文章有用,记得点赞、关注、收藏,学Python更轻松!!

WebSocket断链后确保渲染内容时效性的完整解决方案:

一、断链检测与状态同步

  1. 连接状态实时监控

    // 前端连接状态管理
    let lastActiveTime = Date.now();
    
    ws.onmessage = (event) => {
      lastActiveTime = Date.now();  // 更新最后活跃时间
      if (event.data.type === 'data_update') {
        renderContent(event.data.payload);  // 触发重新渲染
      }
    };
    
  2. 服务端数据版本控制

    # Django Channels示例
    async def send_data(self):
        await self.send({
            'type': 'data_update',
            'version': cache.get('data_version'),  // 数据版本号
            'payload': get_latest_data()
        })
    

二、内容时效性验证机制

  1. 客户端版本校验

    let localDataVersion = 0;
    
    function handleDataUpdate(newData) {
      if (newData.version > localDataVersion) {
        renderContent(newData.payload);
        localDataVersion = newData.version;  // 更新本地版本号
      } else {
        triggerManualRefresh();  // 版本落后时强制刷新
      }
    }
    
  2. 服务端数据变更广播

    // Spring Boot WebSocket广播
    @Scheduled(fixedRate = 5000)
    public void checkDataChanges() {
        int newVersion = dataService.getCurrentVersion();
        if (newVersion != lastBroadcastVersion) {
            simpMessagingTemplate.convertAndSend("/topic/updates", 
                new DataUpdate(newVersion, getLatestData()));  // 主动推送变更
        }
    }
    

三、断链恢复后的数据同步

  1. 重连时增量同步

    ws.onopen = () => {
      ws.send(JSON.stringify({
        type: 'sync_request',
        last_version: localDataVersion  // 携带本地最后版本号
      }));
    };
    
  2. 服务端差异数据返回

    async def receive_json(self, content):
        if content['type'] == 'sync_request':
            updates = get_updates_since(content['last_version'])
            await self.send_json({
                'type': 'sync_response',
                'updates': updates  // 仅返回增量数据
            })
    

四、降级方案保障

  1. 本地缓存对比策略

    function verifyContentFreshness() {
      fetch('/api/data/version')
        .then(res => res.json())
        .then(serverVersion => {
          if (serverVersion > localDataVersion) {
            fetchFullData();  // HTTP降级请求完整数据
        });
    }
    
  2. 可视化状态提示

    .data-status {
      position: fixed;
      top: 10px;
      right: 10px;
      padding: 5px;
      background-color: var(--status-color);
    }
    
    function updateStatusIndicator() {
      const indicator = document.querySelector('.data-status');
      indicator.style.setProperty('--status-color', 
        ws.readyState === WebSocket.OPEN ? '#4CAF50' : '#FF5722');  // 连接状态可视化
    }
    

关键点总结

  1. 版本控制‌:必须实现服务端与客户端的数据版本同步机制
  2. 增量同步‌:重连后仅同步差异数据减少带宽消耗
  3. 双重验证‌:同时使用心跳检测和数据版本校验
  4. 降级策略‌:WebSocket不可用时自动切换HTTP请求

通过上述方案组合实施,可确保断链恢复后内容延迟控制在1秒内(正常网络条件)。实际项目中建议配合Sentry监控数据同步异常。

前端实现客户端扫码登录的全流程解析及实战案例

一、核心流程架构

  1. 三方协作模型

    • Web端:生成/展示二维码 + 轮询状态
    • 移动端:扫码 + 授权确认
    • 服务端:协调认证流程 + 状态同步
  2. 关键步骤时序

    Web前端->>服务端: 1.请求生成二维码(带UUID)
    服务端-->>Web前端: 2.返回二维码数据(有效期120秒)
    Web前端->>服务端: 3.开始轮询状态(间隔2秒)
    移动端->>服务端: 4.扫码提交UUID+token
    服务端->>移动端: 5.返回授权确认页
    移动端->>服务端: 6.用户点击确认登录
    服务端-->>Web前端: 7.轮询返回登录成功
    Web前端->>服务端: 8.获取用户凭证完成登录
    

    二、关键代码实现

QRCode插件是前端开发中用于生成二维码的核心工具,其核心作用和功能如下:

一、核心功能
  1. 文本转二维码
    将URL、文本、JSON等数据编码为二维码图形,支持动态内容生成

    // 示例:将URL转为二维码图片
    QRCode.toCanvas('https://example.com', { width: 200 })
    
  2. 自定义渲染控制

    • 支持Canvas/SVG/Image等多种渲染方式
    • 可调整尺寸、边距、颜色等视觉参数
    javascriptCopy Code
    QRCode.toString('hello', { 
      type: 'svg',
      color: { dark: '#123456' }
    }, (err, svg) => {})
    

QRLogin.vue


<script setup>
import { ref, onMounted } from 'vue'
import QRCode from 'qrcode'

// 关键点1:二维码状态机
const states = {
  INIT: 0,      // 初始状态
  SCANNED: 1,   // 已扫码未确认
  CONFIRMED: 2, // 已确认登录
  EXPIRED: 3    // 二维码过期
}

const qrUrl = ref('')
const status = ref(states.INIT)
let pollTimer = null

// 关键点2:生成带UUID的二维码
const generateQR = async () => {
  const { data } = await axios.post('/api/qrcode', {
    platform: 'WEB',
    scene: 'login'
  })
  qrUrl.value = `auth://${data.uuid}`
  await QRCode.toCanvas(document.getElementById('qrcode'), qrUrl.value, {
    width: 200,
    margin: 2
  })
  startPolling(data.uuid)
}

// 关键点3:轮询状态机制
const startPolling = (uuid) => {
  clearInterval(pollTimer)
  pollTimer = setInterval(async () => {
    const { data } = await axios.get(`/api/check?uuid=${uuid}`)
    switch(data.status) {
      case states.SCANNED: 
        showScanTip() // 显示"扫码成功请确认"
        break
      case states.CONFIRMED:
        clearInterval(pollTimer)
        handleLogin(data.token) // 处理登录逻辑
        break
      case states.EXPIRED:
        clearInterval(pollTimer)
        showRefreshButton() // 显示刷新按钮
    }
  }, 2000) // 关键点4:建议轮询间隔2秒
}
</script>

<template>
  <div class="qr-container">
    <!-- 关键点5:扩大点击区域的二维码容器 -->
    <canvas id="qrcode" style="padding: 15px"></canvas>
    <div v-if="status === states.EXPIRED">
      <button @click="generateQR">刷新二维码</button>
    </div>
  </div>
</template>

serve.js

 express = require('express')
const { v4: uuidv4 } = require('uuid')
const app = express()

// 关键点6:使用Redis存储临时状态
const qrCodes = new Map() // 生产环境应替换为Redis

// 生成二维码接口
app.post('/api/qrcode', (req, res) => {
  const uuid = uuidv4()
  qrCodes.set(uuid, { 
    status: 0, 
    createdAt: Date.now(),
    token: null
  })
  // 关键点7:设置120秒过期时间
  setTimeout(() => {
    if (qrCodes.get(uuid)?.status < 2) {
      qrCodes.set(uuid, { status: 3 })
    }
  }, 120000)
  res.json({ uuid })
})

// 移动端扫码回调
app.post('/api/scan', (req, res) => {
  const { uuid, deviceId } = req.body
  if (!qrCodes.has(uuid)) return res.status(410).end()
  qrCodes.set(uuid, { 
    status: 1,
    deviceId 
  })
  res.json({ confirmUrl: '/confirm?uuid='+uuid })
})

// 状态轮询接口
app.get('/api/check', (req, res) => {
  const record = qrCodes.get(req.query.uuid)
  if (!record) return res.status(410).json({ status: 3 })
  res.json(record)
})

三、实战优化方案

  1. 安全增强

    - 在移动端确认页显示登录设备IP和地理位置

      1. 异常登录识别
        通过对比用户常用设备IP与当前登录IP的差异,可快速发现异地登录等可疑行为14。例如:若用户常驻北京却突然出现海南登录记录,系统将触发风险提示。
      1. 双重确认机制
        地理位置信息作为二次验证要素,帮助用户确认是否为本人操作4。数据显示该功能可降低约37%的账号盗用风险。

    - 对UUID进行JWT签名防止伪造

      1. 签名验证真实性
        通过JWT的签名算法(如HS256/RSA256)对UUID进行加密,确保该标识符只能由持有密钥的服务端生成,第三方无法伪造有效令牌34。若攻击者篡改UUID值,签名校验将立即失败48。
      1. 抵御中间人攻击
        JWT签名可防止传输过程中UUID被截获后恶意复用,确保扫码登录流程的端到端完整性
  2. 性能优化

    • 采用WebSocket替代HTTP轮询(当在线用户>1万时)
    • 服务端使用Redis集群存储会话状态
  3. 异常处理

    // 关键点8:完善的错误处理
    const handlePollError = (error) => {
      if (error.response?.status === 410) {
        showExpiredTip()
      } else {
        // 采用指数退避重试
        currentDelay = Math.min(currentDelay * 2, 30000)
        setTimeout(startPolling, currentDelay)
      }
    }
    

1747836264094.jpg

  1. 多平台适配

    • 微信扫码需使用官方JS-SDK
    • 企业微信需处理corpId与agentId绑定

组件扩展接口设计秘籍大公开:以Select组件为例

引言:为什么需要重新设计Select组件?

在复杂的前端应用中,选择器(Select)组件的使用场景远超出基础的表单选择需求。我们经常面临:

  • 动态数据适配:需要同时支持本地枚举、远程接口、树形数据等多种数据源
  • 多态展示需求:编辑态与只读态的不同呈现方式
  • 深度交互定制:标签触发、复合搜索等进阶交互模式
  • 企业级扩展性:快速响应业务方个性化需求

本文将以Pro-Component的Select组件实现为例,深度解析专业级组件的扩展接口设计哲学。

image.png


一、组件架构设计全景

1.1 核心模块划分

graph TD
    A[FieldSelect] --> B[SearchSelect]
    A --> C[LightSelect]
    B --> D[远程搜索]
    B --> E[动态过滤]
    C --> F[标签触发]
    C --> G[轻量交互]
  • FieldSelect:数据管理层,统一处理valueEnum/options转换
  • SearchSelect:支持远程搜索的增强型选择器
  • LightSelect:轻量化呈现的快速选择器

1.2 数据流管理

分层管理,统一转化

/// 统一的数据转换层
const getOptionsFormValueEnum = (valueEnum) => {
  return proFieldParsingValueEnumToArray(valueEnum).map(
    ({ value, text, ...rest }) => ({
      label: text,
      value,
      key: value,
      ...rest,
    })
  );
};

// 统一的数据获取层
const [loading, options, fetchData, resetData] = useFieldFetchData(props);

1.3 自定义Hook抽象

// 将复杂的数据获取逻辑封装成Hook
export const useFieldFetchData = (props) => {
  // ... 
  return [loading, options, fetchData, resetData];
};

二、扩展接口设计深度解析

扩展接口全景图

flowchart LR
    A[Select扩展体系] --> B[UI定制层]
    A --> C[数据适配层]
    A --> D[行为控制层]
    A --> E[生命周期层]
    
    B --> B1[视觉呈现]
    B1 --> B11["optionItemRender\n选项级渲染控制"]
    B1 --> B12["dropdownRender\n容器级渲染扩展"]
    B --> B2[样式控制]
    B2 --> B21["className/style\nCSS定制"]
    
    C --> C1[数据源]
    C1 --> C11["request/valueEnum/options\n多数据源适配"]
    C --> C2[结构转换]
    C2 --> C21["fieldNames/labelInValue\n数据格式转换"]
    
    D --> D1[搜索优化]
    D1 --> D11["debounceTime/filterOption\n搜索体验优化"]
    D --> D2[交互模式]
    D2 --> D21["mode/labelTrigger\n交互行为配置"]
    
    E --> E1[事件回调]
    E1 --> E11["onChange/onSearch\n状态监听"]
    E --> E2[状态管理]
    E2 --> E21["loading/disabled\n状态控制"]

2.1UI定制层扩展

自定义选项渲染 —— optionItemRender

设计意图:解决复杂业务场景下的信息展示需求,如带有状态标识的用户选择器、带缩略图的商品选择器等。

实现原理:通过渲染代理模式,将选项的DOM结构控制权交给使用者,组件内部仅管理数据流

interface SearchSelectProps<T> {
  optionItemRender?: (item: T) => React.ReactNode;
}

// 使用示例:带图标的选项
<SearchSelect
  optionItemRender={(item) => (
    <Space>
      <Avatar src={item.avatar} />
      <span>{item.label}</span>
      <Tag color={item.statusColor}>{item.status}</Tag>
    </Space>
  )}
/>

容器级扩展 —— dropdownRender

典型场景:需要在下拉区域添加操作入口(如"未找到结果?立即创建")或展示辅助信息。

技术方案:采用插槽(Slot)模式,将默认渲染结果作为参数暴露

// 添加快速创建入口
<SearchSelect
  dropdownRender={(menu) => (
    <>
      {menu}
      <Divider style={{ margin: '8px 0' }} />
      <Button 
        block 
        icon={<PlusOutlined />}
        onClick={handleCreate}
      >
        新建项目
      </Button>
    </>
  )}
/>

样式覆盖接口

// 支持CSS-in-JS和CSS Modules
interface LightSelectProps {
  className?: string;
  style?: React.CSSProperties;
  prefixCls?: string;
}

2.2 数据适配层扩展

多数据源支持

interface FieldSelectProps {
  /** 静态枚举 */
  valueEnum?: Record<string, any>;
  /** 动态选项 */
  options?: RequestOptionsType[];
  /** 远程请求 */
  request?: ProFieldRequestData;
}

// 使用示例:远程搜索
<FieldSelect
  request={async (params) => {
    const res = await fetch(`/api/search?q=${params.query}`);
    return res.json();
  }}
/>

字段映射(异构数据适配)

痛点解决:当后端返回数据结构与组件预期格式不一致时,避免手动数据转换。

interface SearchSelectProps {
  fieldNames?: {
    label?: string;
    value?: string;
    options?: string;
  };
}

// 使用示例:适配异构数据
<SearchSelect
  fieldNames={{
    label: 'name',
    value: 'id',
    options: 'children'
  }}
/>

2.3 行为控制层扩展

搜索优化

interface SearchSelectProps {
  debounceTime?: number; // 防抖时间
  searchOnFocus?: boolean; // 聚焦即搜索
  resetAfterSelect?: boolean; // 选择后重置
}

// 使用示例:快速搜索
<SearchSelect
  debounceTime={500}
  searchOnFocus
  resetAfterSelect
/>

交互模式扩展

interface LightSelectProps {
  mode?: 'multiple' | 'tags';
  labelTrigger?: boolean;
  allowClear?: boolean;
}

// 使用示例:标签式选择
<LightSelect
  mode="tags"
  labelTrigger
  allowClear
/>

2.4 生命周期扩展

上层传入回调,在特定生命周期下直接调用。

interface SearchSelectProps {
  onSearch?: (value: string) => void;
  onChange?: (value: any, option: any) => void;
  onFocus?: (e: React.FocusEvent) => void;
  onBlur?: (e: React.FocusEvent) => void;
}

// 使用示例:搜索分析
<SearchSelect
  onSearch={(val) => analytics.track('search', val)}
  onChange={(val) => analytics.track('select', val)}
/>

三、扩展接口设计原则

3.1 开闭原则实践

graph LR
    A[基础功能] --> B[扩展点]
    B --> C[自定义渲染]
    B --> D[数据适配]
    B --> E[行为扩展]

3.2 接口设计最佳实践

  1. 渐进式暴露:从简单到复杂逐步开放能力
  2. 类型安全:完善的TypeScript类型定义
type ProSelectProps = {
  // 扩展属性
} & Omit<AntdSelectProps, 'onChange'>;
  1. 兼容性保障
    interface CompatibleProps {
      /** @deprecated 使用 fieldNames 替代 */
      labelKey?: string;
      valueKey?: string;
    }
    
  2. 文档驱动:为每个扩展接口提供使用示例
  3. 智能默认值
const mergedProps = {
  ...defaultProps,
  ...userProps,
  fieldNames: {
    ...defaultFieldNames,
    ...userProps.fieldNames,
  },
};

五、总结与启示

5.1 关键设计模式

模式 应用场景 示例
控制反转 自定义渲染 optionItemRender
策略模式 数据获取 request/valueEnum
装饰器模式 功能扩展 dropdownRender

5.2 通用组件设计原则

  1. 80/20法则:覆盖80%通用场景,开放20%扩展点
  2. 沙箱原则:扩展功能不影响核心逻辑
  3. 可测性设计:独立的扩展接口更易测试
  4. 文档即合约:清晰的接口约定胜过复杂实现
  5. 开放封闭原则:对扩展开放,对修改封闭
  6. 关注点分离:数据、UI、逻辑分层管理

"优秀的组件设计不是功能的堆砌,而是恰到好处的扩展性与克制。" —— 组件设计之道

🚀🚀🚀 AI 助手好写,太好写了,分分钟写出来,不用一周,三分钟!

前言

产品最近要求在老系统里加一个AI助手功能,类似下面这样:

AI 助手

正好最近在刷到了 Ant Design X 组件库,那就马上来封装一个通用的AI助手组件吧!

往期精彩推荐

正文

Ant Design X 专为 AI 驱动的用户界面量身定制。它在 AntD 的基础上扩展。

基于 Antd 的组件

该库提供了一系列丰富的组件,这些组件高度可定制且模块化。这些组件内置了数据消费工具,支持与后端服务的无缝集成,并高效管理数据流。

和后端无缝衔接的工具

但是 Ant Design X 最大的问题就是组件过于零散,想马上实现一个完整的聊天助手比较困难,所以我进行了一次封装,方便大家更快完成开发!

我的封装仅涉及到 UI 布局组装,不涉及具体细节样式,高度可自定义!

antdx 给你的是零散的部件,而antdx-pro给你的是成型的骨骼!

细节的雕刻,需要自己完成!

仓库:github.com/mmdctjj/ant…

文档:mmdctjj.github.io/antdx-pro

明确需求

首先,我们需要明确一个AI助手应该有哪些内容

欢迎界面

欢迎界面包含:欢迎区、建议区、输入区!

聊天页面

聊天页面主要包含:输出区和输入区!

开始封装 UI

现在,我们开始封装 UI ,如果你刚上手一定要记得先阅读下文档,这里我尽量保持原有的 API 结构,再次基础上,我们定义组件的 props

interface IAssistantProps {
  /** 欢迎 **/
  welcomeProps?: WelcomeProps
  /** 聊天 **/
  bubbleProps?: BubbleProps,
  /** 建议 **/
  promptsProps?: PromptsProps
  /** 输入 **/
  expressProps?: {
    attachmentsProps?: AttachmentsProps,
    senderProp?s: SenderProps,
    suggestionProps?: SuggestionProps
  }
}

接下来写组件,要保证聊天区始终在底部,所以打算使用 flex 布局!

import { Button, Flex } from "antd";
import {
  Attachments,
  AttachmentsProps,
  Bubble,
  Prompts,
  PromptsProps,
  Sender,
  SenderProps,
  Suggestion,
  SuggestionProps,
  Welcome,
  WelcomeProps,
} from "@ant-design/x";
import { LinkOutlined, UserOutlined } from "@ant-design/icons";
import { BubbleListProps } from "@ant-design/x/es/bubble/BubbleList";

interface IAIAssistantProps {
  /** 欢迎 **/
  welcomeProps?: WelcomeProps;
  /** 聊天 **/
  bubbleListProps?: BubbleListProps;
  /** 建议 **/
  promptsProps?: PromptsProps;
  /** 输入 **/
  expressProps?: {
    attachmentsProps?: AttachmentsProps;
    senderProps?: SenderProps;
    suggestionProps?: SuggestionProps;
  };
}

export const AIAssistant = (props: IAIAssistantProps) => {
  return (
    <Flex
      vertical
      gap="middle"
      style={{ height: `100%` }}
      justify="space-between"
    >
      {!props?.bubbleListProps?.items?.length ? (
        <Flex vertical gap="middle">
          <Welcome {...props?.welcomeProps} />
          <Prompts {...props?.promptsProps} />
        </Flex>
      ) : (
        <Bubble.List {...(props?.bubbleListProps ?? {})} />
      )}
      <Flex vertical gap={0}>
        {!!props?.expressProps?.attachmentsProps?.items?.length && (
          <Attachments {...(props?.expressProps?.attachmentsProps ?? {})} />
        )}
        <Suggestion
          {...(props?.expressProps?.suggestionProps ?? { items: [] })}
        >
          {({ onTrigger, onKeyDown }) => {
            return (
              <Sender
                {...props.expressProps?.senderProps}
                onChange={(nextVal) => {
                  if (nextVal === "/") {
                    onTrigger();
                  } else if (!nextVal) {
                    onTrigger(false);
                  }
                  props?.expressProps?.senderProps?.onChange?.(nextVal);
                }}
                onKeyDown={onKeyDown}
              />
            );
          }}
        </Suggestion>
      </Flex>
    </Flex>
  );
};

代码可以看这里:http://localhost:8000/antdx-pro/components/copilot

首页会显示 欢迎语提示建议,开始聊天后这部分会隐藏!

首页

搭配 markdown-it 可以做到 markdown 渲染!

markdown-it 渲染

通过传入 Attachments 实现文件上传功能,聊天内容可以预览图片,当前文件不支预览!

支持文件上传

输入框的样式可以借助 prefix header footer 属性实现个性化的样式!

普通

仿 chatgpt

最后

希望这篇文章可以帮助到你,这个组件库还会继续完善!求 star

github.com/mmdctjj/ant…

往期精彩推荐

耗时十分钟,做了一个 uniapp 灵感泡泡机

最近,我用 UniApp 搭配 CodeBuddy 实现了一个充满童话感的小应用,名叫 IdeaBubbles(灵感泡泡机)。它是一个单页 WebApp,用来记录那些转瞬即逝的灵感时刻。整个界面以梦幻气泡和彩虹玻璃拟态为主题,视觉效果令人愉悦,而交互体验也尽可能做到了顺滑灵动。

这个项目几乎是我一句话提出想法后,由 CodeBuddy 自动构建完成的,从页面结构到动画细节,一气呵成。我想用这篇文章记录下整个过程,也许能给同样热爱前端创意项目的你一些灵感。


项目灵感和视觉设定

我一开始就对 CodeBuddy 说:“我要用 UniApp 做一个单页 WebApp,名字叫 IdeaBubbles,风格为梦幻气泡、彩虹玻璃拟态,用来记录碎片灵感。”

结果真没想到,它不仅听懂了我说的“梦幻气泡”和“玻璃拟态”,还迅速给出了完整的页面结构设计。顶部是渐变标题,中部是灵感输入和泡泡生成按钮,底部还有泡泡数量和清空功能。更惊喜的是,它自动为每个区域设计了细节:字体使用 Quicksand,背景是渐变+玻璃纹理叠加,交互上加入了拖动、双击删除、长按复制等趣味机制。


创建项目:从 0 到启动

CodeBuddy 指导我使用以下命令一键生成项目框架:

npm install -g @vue/cli @dcloudio/uni-cli
vue create -p dcloudio/uni-preset-vue IdeaBubbles
cd IdeaBubbles
npm install

我几乎不用动手去搜索或排查错误,它就一步步把项目搭建好了。所有配置都井井有条,甚至连 Vue 和 uni-app 的版本依赖也预处理好了,省去了很多环境配置上的麻烦。


中心交互:灵感生成泡泡

项目的灵魂就是“泡泡生成”。我只需输入一句灵感,比如“试试用 SVG 画点水波纹”,然后点击那个精美的圆形按钮,就会在屏幕中间“啪”地生成一个带有渐变背景的小泡泡。这个泡泡可以自由拖动、放大发光,双击就会消失,长按还能复制内容,非常有趣。

实现这部分交互时,CodeBuddy 的代码让我非常惊艳。比如泡泡生成使用 position: absolute 和容器 relative 配合,泡泡初始位置是随机计算的:

const left = Math.random() * (window.innerWidth - 100)
const top = Math.random() * (window.innerHeight - 200)

而拖拽功能则使用了 touchstarttouchmovetouchend 三个事件组合,同时在拖动过程中加了 scale 和光晕特效,视觉体验非常顺滑。

在这里插入图片描述


视觉细节:玻璃与彩虹的碰撞

视觉风格是这个项目最吸睛的部分。整个页面背景是一个柔和的彩虹渐变:

background: linear-gradient(to bottom right, #FFDEE9, #B5FFFC);

加上叠加的 SVG 流动纹理,真的有种“空气中漂浮着泡泡”的感觉。每一个 UI 元素都采用玻璃拟态的设计:透明白色背景、8px 模糊滤镜、微妙阴影和圆角。输入框和按钮都像是嵌在冰雕里,点按时带有轻微的放大动画,增强了交互反馈感。

CodeBuddy 对这些细节的处理非常专业,按钮 hover 动画使用了 CSS 的 transform: scale(1.05),加了 transition 让它看起来不突兀,甚至连 box-shadow 的透明度都调得刚刚好,不浓不淡,舒服极了。


实用功能:泡泡统计与清空动画

底部区域设置了泡泡统计和一个“清空泡泡”按钮。当点击“清空”时,屏幕上的所有泡泡会同时触发缩放+淡出动画,像一场气泡爆破秀。

这部分我本来没提需求,但 CodeBuddy 很贴心地加了这个功能,动画使用 scale(1.2) + opacity: 0,并通过 setTimeout 配合删除 DOM,确保视觉和逻辑同步完成。用户体验在细节上达到了完整闭环。

在这里插入图片描述


结语:这不仅是工具,而是代码魔术师

从最开始的构思到最后的交互完成,我几乎没有写过一行核心逻辑代码。整个项目完全是由 CodeBuddy 主动驱动完成,它不仅理解了我想要的视觉风格、功能需求,还在细节和交互上不断给我惊喜。它写的代码非常有条理,结构清晰,变量命名规范,CSS 动效自然细腻,几乎不需要我修改就可以直接上线运行。

尤其让我感动的是,它在设计交互时考虑得非常全面,哪怕是一个按钮 hover 的手感,或者泡泡出现的位置不遮挡输入框,都做了细致处理。

如果说以前我写前端更像是拼积木,那这次和 CodeBuddy 合作,更像是与一位熟悉设计美学又擅长编码的搭档共创作品。这不是建议代码,而是主动创作

希望未来还能和它一起,做出更多有趣又漂亮的网页小工具。


如果你也有一个灵感,不妨把它告诉 CodeBuddy,它说不定已经开始帮你实现了。

在这里插入图片描述

JavaScript 垃圾回收与内存泄漏

在 JavaScript 开发中,垃圾回收和内存泄漏是两个重要的概念。垃圾回收机制可以自动管理内存,但如果不了解其原理,很容易导致内存泄漏,进而影响程序性能甚至导致崩溃。

一、什么是内存泄漏?

程序运行时需要占用内存。当程序申请的内存不再使用时,如果没有及时释放,就会导致内存占用越来越高,最终可能影响系统性能,甚至导致程序崩溃。这种现象称为内存泄漏(Memory Leak)。内存泄漏不仅会导致程序运行缓慢,还可能引发更严重的问题,如内存不足导致的程序崩溃。

二、JavaScript 的垃圾回收机制

JavaScript 具有自动垃圾回收机制(Garbage Collection, GC),这意味着开发者不需要手动管理内存。垃圾回收器会定期检查并释放不再使用的内存。虽然垃圾回收机制大大简化了内存管理,但了解其工作原理仍然非常重要。

(一)垃圾回收的时机

垃圾回收器会按照固定的时间间隔周期性地运行。它会在后台自动执行,释放那些不再使用的内存。垃圾回收的频率取决于多种因素,包括程序的运行时间、内存使用情况等。

(二)垃圾回收的策略

JavaScript 中常见的垃圾回收策略有两种:标记清除引用计数

1. 标记清除

标记清除是 JavaScript 中最常用的垃圾回收方式。其工作原理如下:

  • 标记阶段:垃圾回收器会遍历所有变量,将进入环境的变量标记为“进入环境”,将离开环境的变量标记为“离开环境”。
  • 清除阶段:垃圾回收器会清除那些被标记为“离开环境”的变量所占用的内存。
function test() {
    var a = 10; // 被标记为“进入环境”
    var b = 20; // 被标记为“进入环境”
}
test(); // 执行完毕后,a 和 b 被标记为“离开环境”,并被回收

标记清除策略的优点是简单高效,但它也有一个缺点:无法处理循环引用的情况。

2. 引用计数

引用计数的含义是跟踪记录每个值被引用的次数。其工作原理如下:

  • 引用次数增加:当一个变量被赋值为某个对象时,该对象的引用次数加 1。
  • 引用次数减少:当一个变量被重新赋值或被删除时,该对象的引用次数减 1。
  • 释放内存:当一个对象的引用次数变为 0 时,垃圾回收器会释放该对象所占用的内存。
function test() {
    var a = {}; // a 的引用次数为 1
    var b = a;  // a 的引用次数加 1,变为 2
    var c = a;  // a 的引用次数再加 1,变为 3
    var b = {}; // a 的引用次数减 1,变为 2
}

引用计数策略的优点是可以快速释放不再使用的内存,但它也有一个严重的缺点:无法处理循环引用的情况。

(三)循环引用问题

循环引用是指两个或多个对象相互引用,形成一个闭环。在引用计数策略下,循环引用会导致内存泄漏,因为这些对象的引用次数永远不会变为 0。

function fn() {
    var a = {};
    var b = {};
    a.pro = b;
    b.pro = a;
}
fn();

在上面的代码中,ab 互相引用,形成一个闭环。在引用计数策略下,ab 的引用次数永远不会变为 0,因此它们不会被垃圾回收器回收,导致内存泄漏。

三、避免内存泄漏的策略

(一)及时释放引用

在不再需要某个变量时,及时将其设置为 null,释放对它的引用。

var element = document.getElementById('someElement');
element = null; // 释放引用

(二)移除事件监听器

在不再需要某个事件监听器时,及时移除它。

var element = document.getElementById('someElement');
element.addEventListener('click', function handler() {
    // 一些操作
});
element.removeEventListener('click', handler); // 移除事件监听器

(三)避免不必要的闭包

在不需要闭包时,避免使用闭包,或者及时释放闭包。

function createClosure() {
    var largeArray = new Array(1000000).fill(0);
    return function() {
        console.log(largeArray.length);
    };
}

var closure = createClosure();
closure = null; // 释放闭包

(四)使用弱引用

在某些情况下,可以使用 WeakMapWeakSet 来存储对对象的弱引用,这些引用不会阻止垃圾回收器释放内存。

var weakMap = new WeakMap();
var element = document.getElementById('someElement');
weakMap.set(element, 'some data');
element = null; // 释放引用

四、总结

JavaScript 的垃圾回收机制虽然可以自动管理内存,但开发者仍然需要了解其工作原理,以避免内存泄漏。希望本文能帮助你更好地理解和应用这些知识。

npm 【workspace】【npm link】【安装本地包】【类库开发调试】

前言

当我们在开发类库时,往往会在实际项目中引入开发中的类库,来调试实际使用的效果。并且最好是需要类库改变后,能够自动、实时的在项目中反馈。 npm有三种方法实现不发布npm包,就在本地像npm i安装一样去使用它,如下:

特性 工作空间(Workspaces) 本地路径依赖 (file:) npm link
依赖管理方式 单仓库多包(monorepo) 跨项目文件引用 全局符号链接
版本控制 由根 package.json 统一管理 各自维护版本,可能冲突 忽略版本,直接引用源码
修改传播 实时生效(无需重新安装) 实时生效 实时生效
依赖结构 扁平化(共享根 node_modules 嵌套结构(可能导致重复安装) 全局链接,可能与其他项目冲突
生产环境兼容性 完全兼容(发布独立包) 需替换为 npm 包版本 不可用(必须替换为 npm 包)
适用场景 大型项目内部模块协作 临时引用本地包 频繁开发的本地包

下面我们就来分别介绍它们的使用

1.前置条件

首先我们建立一个项目目录,后面的案例都按照这个目录操作

├─ app/                     # app项目
│  ├─ index.js               
│  └─ package.json           
├─ lib1/                     # sub项目
│  ├─ index.js             
│  └─ package.json  

1.本地路径依赖 and npm link

本地依赖开发非常简单,直接将依赖指向本地地址即可。它其实最终也是执行的npm link,相对于"scripts"项的脚本一样

// app/package.json
{
    "dependencies":{
        "lib-name1":"../lib1",// lib目录必须是一个npm项目
    }
}

执行npm i,npm就会自动把./lib目录链接到app/node_modules下,然后像普通模块一样引入即可

// app/index.js
import m from "lib-name1";

然后我们看看npm link的使用方式,直接在app/目录的命令行中执行

app> npm link ../lib1; //在app/node_modules中的文件名由../lib1/package.json中的name项决定

你可以查看app/node_modules目录,可以看到本地路径依赖npm link最终都是在其内部生成了一个目录链接符

2.workspace

建立一个新的目录结构

├─ app/ # app项目
|   ├─ index.js
|   ├─ package.json
|   |
|   ├─ pack1/
|   |   ├─ index.js
|   |   └─ package.json #s 引入lib1
|   |
|   └─ packages/
|       └─ pack2/
|           ├─ index.js
|           └─ package.json #s 引入lib2
│             
├─ pack0/                 
   ├─ index.js             
   └─ package.json #s 引入lib0

然后配置workspace

// app/package.json
{
  "workspace":[
      "../pack0",
      "./pack1",
      "./packages/*", // 引入./packages/目录下所有项目
  ]
}

workspace最终也是建立一个"npm link"目录链接,但是它与npm link本地路径依赖不一样的是,workspace如果配置的是在当前根目录内的项目作为依赖,那么所有的依赖都是统一安装到根目录node_module下进行复用,即便你是在依赖项中执行npm i也不会安装到workspace项目中的node_module

如果配置的workspace项目,根目录外,则无法复用。依赖项会安装到workspace项目的node_modules中。

在上面的目录结构执行完npm i之后,包安装位置结果如下

|-app/node_modules
|    |- lib1
|    |- lib2
|-pack0/node_modules
     |- lib0

3.结论

不论是库还是单体项目,都推荐使用workspace进行包管理!

Edge Runtime 与 Serverless Runtime

一、基本定义

类型 定义
Serverless Runtime 指在云服务(如 AWS Lambda)上运行的无状态函数,按请求触发,运行在**中心化服务器(Region)**中。
Edge Runtime 指代码运行在**CDN 边缘节点(Edge Node)**的沙箱环境中,靠近用户,延迟更低,使用 Web 标准 API 执行。

二、底层运行原理对比

特性 Serverless Runtime Edge Runtime
运行环境 Node.js、支持 CommonJS/ESM 基于 V8 引擎(非 Node.js),无 Node 核心模块
执行位置 云 Region 中心服务器(如 AWS us-east-1) 离用户最近的 CDN 边缘节点(如东京、新加坡)
触发方式 按请求启动(有冷启动) 按请求启动(极低冷启动)
资源限制 内存大、运行时间长 内存小、执行时间短
支持 API Node.js API 全支持,如 fshttpcrypto 仅支持 Web 标准 API,如 fetchRequestResponse
文件系统访问 ✅ 支持 ❌ 不支持
数据库连接 ✅ 支持直连 MySQL/MongoDB 等 ⚠️ 不推荐(无 TCP 支持、无连接池)

三、启动与响应性能对比

对比项 Serverless Edge
启动时间 50ms ~ 800ms(冷启动) <10ms(近似无冷启动)
运行时长限制 通常几分钟(如 AWS Lambda 最长 15 分钟) 通常几秒(如 Cloudflare Worker 最多 30 秒)
并发能力 支持高并发(自动扩展) 支持极高并发(且无需冷启动)
本地模拟支持 ✅ 可用 next dev 本地模拟 ⚠️ 本地模拟困难、需平台支持(如 Vercel CLI)

四、应用场景对比

场景类型 Serverless Runtime Edge Runtime
SSR 页面渲染 ✅ 推荐,支持数据库、缓存等复杂逻辑 ⚠️ 不推荐,因不支持数据库直连
国际化重定向 ✅ 可做,但慢 ✅ 非常推荐,极快
登录鉴权 ✅ 推荐 ✅ 推荐(如 JWT 验证)
A/B 实验 ✅ 推荐 ✅ 推荐(执行速度快)
数据库操作 ✅ 推荐 ❌ 不推荐
静态资源预处理 ❌ 无意义 ✅ 可拦截 CDN 请求处理逻辑
CDN 边缘响应 API ❌ 慢 ✅ 最佳选择
页面 Layout Server Components(App Router) ✅ 支持 ✅ 支持,需 export const runtime = 'edge'

五、Next.js 中的使用方式

1. Serverless(默认)

适用于任何 API 路由、SSR 页面:

// 默认 Node.js Runtime
export async function GET(req: Request) {
  // 可以连接数据库、使用 fs、进行 SSR 等
}

2. Edge Runtime

middleware.ts、API 路由或 page.tsx 中启用:

export const runtime = 'edge';

export async function GET() {
  return new Response('Hello from Edge!');
}

六、与平台的关系

平台 Serverless Runtime Edge Runtime
Vercel ✅ 默认支持 ✅ 强力支持(推荐平台)
Cloudflare Workers ❌ 不支持 ✅ 支持(原生)
Netlify Functions ✅ 支持 ✅ 支持(部分)
AWS Lambda ✅ 支持 ✅ 需使用 Lambda@Edge
阿里云函数计算 ✅ 支持 ⚠️ 支持有限
自建 Node.js 服务 ✅ 支持 ❌ 不支持 Edge

七、优缺点总结

✅ Serverless Runtime 优势

  • 支持数据库连接、文件操作

  • 更强的计算能力和执行时长

  • 更灵活,调试方便

❌ Serverless Runtime 缺点

  • 冷启动慢(尤其是首次)
  • 距离用户远,延迟高
  • 无法处理 CDN 级别请求

✅ Edge Runtime 优势

  • 毫秒级冷启动
  • 离用户更近,延迟极低
  • 非常适合轻量请求、前置逻辑、边缘缓存判断

❌ Edge Runtime 缺点

  • 无法访问 Node 核心模块
  • 无法直连数据库
  • 无文件系统、不适合重计算任务

总结建议

场景 推荐运行时
页面 SSR + 数据库渲染 ✅ Serverless
登录状态校验 / token 验证 ✅ Edge
CDN 边缘路由拦截(如国际化) ✅ Edge
复杂 API 接口处理 ✅ Serverless
页面 Layout 的快速响应组件 ✅ Edge(App Router)

实战例子

登录校验(middleware.ts + Edge)

export function middleware(request) {
  const token = request.cookies.get('token')
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  return NextResponse.next()
}

国际化跳转

export function middleware(req) {
  const lang = req.headers.get('accept-language')
  const url = req.nextUrl
  if (!url.pathname.startsWith('/zh')) {
    url.pathname = '/zh' + url.pathname
    return NextResponse.redirect(url)
  }
}

突破浏览器最小字号限制:前端超小字号实现技巧

一. 使用SVG文本呈现文字

 <svg width="100%" height="20">
       <text x="50%" y="10" font-size="7" font-family='SourceHanSansCN-bold' fill="#a62f1c" font-weight="600" text-anchor="middle">{{ gift.txt }}</text>
</svg>
  1. 使用x="50%"text-anchor="middle"实现文字水平居中

二. 使用 transform: scale()缩小文字

.txt {
     font-size: 12px;
     transform: scale(0.8);
 }
  1. font-size的大小取决于浏览器限制的最小字体

AI回答:

突破浏览器最小字号限制的方法主要有以下几种‌:

  1. 使用CSS的transform: scale()属性‌:通过将字体大小设置为一个较大的值,然后使用transform: scale()属性进行缩放,可以实现小于12px的字体效果。例如,将字体大小设置为20px,然后通过0.5倍缩放来实现10px的字体效果。这种方法适用于块级元素和行内块元素,但需要注意缩放属性会影响元素的布局和样式‌12。
  2. 使用SVG文本‌:对于需要在图标或徽标中显示非常小的文本,可以考虑使用SVG(可缩放矢量图形)来呈现文本。SVG文本不受浏览器最小字体大小的限制,可以直接设置所需的字体大小‌4。
  3. 修改Chrome浏览器设置‌:可以通过调整Chrome浏览器的设置来禁用最小字体大小限制。在Chrome地址栏输入chrome://settings/,然后搜索“字体”或“外观”,尝试调整相关设置。但这种方法可能因Chrome版本和操作系统而异,且不保证所有用户都会进行相同的设置‌45。

这些方法的优缺点如下‌:

  • CSS缩放‌:

    • 优点‌:代码简洁,文本可选中,兼容性好。
    • 缺点‌:影响布局和样式,大量文本缩放会影响渲染性能‌。
  • SVG文本‌:

    • 优点‌:不受浏览器最小字体大小限制,适合特定场景。
    • 缺点‌:需要使用SVG,不适合所有文本内容‌。
  • 修改浏览器设置‌:

    • 优点‌:操作简单,直接。
    • 缺点‌:只对当前用户的浏览器生效,不适用于所有用户‌45。

zoom

CSS 中的 zoom 属性用于缩放元素的大小,包括其内容、边框和内边距。它类似于 transform: scale(),但有一些关键区别。以下是关于 zoom 的详细说明和使用方法。


1.基本用法

zoom 属性接受一个数值或百分比,表示元素的缩放比例。

.example {
  zoom: 1.5; /* 放大到 150% */
}css
  • zoom: 1:默认大小(无缩放)。
  • zoom: 0.5:缩小到 50%。
  • zoom: 200%:放大到 200%。

2.与 transform: scale() 的区别

特性 zoom transform: scale()
兼容性 仅部分浏览器支持(如 IE、Edge) 所有现代浏览器支持
布局影响 会重新计算布局,影响其他元素 不会影响布局,仅视觉缩放
性能 性能较差 性能较好
缩放中心 默认从左上角缩放 可以通过 transform-origin 设置中心点

3.使用 zoom 的场景

  • 兼容旧版浏览器:如果需要支持 IE 或旧版 Edge,可以使用 zoom
  • 简单缩放:如果不需要复杂的布局控制,zoom 是一个简单的解决方案。

4.示例

放大元素
.zoom-in {
  zoom: 1.5; /* 放大到 150% */
}css
缩小元素
.zoom-out {
  zoom: 0.75; /* 缩小到 75% */
}css
结合百分比
.zoom-percent {
  zoom: 200%; /* 放大到 200% */
}css

5.注意事项

  1. 兼容性问题

    • zoom 不是标准属性,现代浏览器(如 Chrome、Firefox)不支持。
    • 如果需要跨浏览器兼容,建议使用 transform: scale()
  2. 布局影响

    • zoom 会重新计算元素的布局,可能导致页面其他元素的位置发生变化。
  3. 性能问题

    • zoom 的性能较差,尤其是在复杂页面中。

6.替代方案:transform: scale()

如果不需要支持旧版浏览器,建议使用 transform: scale()

.example {
  transform: scale(1.5); /* 放大到 150% */
  transform-origin: 0 0; /* 设置缩放中心点 */
}css
  • transform-origin:设置缩放的中心点,默认是元素中心。

7.结合 JavaScript 动态缩放

通过 JavaScript 动态设置 zoom 或 transform: scale()

<div id="box" style="width: 100px; height: 100px; background: red;"></div>
<button onclick="zoomIn()">放大</button>
<button onclick="zoomOut()">缩小</button>

<script>
  const box = document.getElementById('box');

  function zoomIn() {
    box.style.zoom = (parseFloat(box.style.zoom) || 1) + 0.1;
  }

  function zoomOut() {
    box.style.zoom = (parseFloat(box.style.zoom) || 1) - 0.1;
  }
</script>html

8.响应式缩放

通过媒体查询实现响应式缩放。

@media (max-width: 600px) {
  .responsive-zoom {
    zoom: 0.8; /* 在小屏幕上缩小到 80% */
  }
}css

总结

  • zoom 是一个简单的缩放属性,但兼容性和性能较差。
  • 现代开发中,建议使用 transform: scale() 作为替代方案。
  • 如果需要支持旧版浏览器(如 IE),可以结合 zoom 和 transform: scale() 实现兼容性。

参考文档

blog.csdn.net/weixin_4509…

juejin.cn/post/733874…

uni-app项目从0-1基础架构搭建全流程

前情

最近新接了一个全新项目,我负责从0开始搭建小程序,我选用的技术栈是uni-app技术栈,UI库选择的是uview-plus,CSS引入现在流行的tainlwindcss,实现CSS原子化书写,实现小程序分包,分包中实现webview使用和彩蛋页(方便开发和测试使用的功能页),同时实现接口请求、本地缓存等常用工具方法的封装

基础架构主要内容

image.png

项目搭建

tailwindcss原子化CSS框架引入

对于uni-app项目官方有自带的IDE,以往新项目我都是通过IDE新建空白项目,再一点点增加自己的东西这样做的。

image.png

但这一次我就踩到了第一个小坑,我一开始是建的空白项目,在引入tainlwindcss的时候,发现按文挡一步步操作下来,最后一直报错,折腾了好一会没解决,我都已经为前面我接手的二个历史项目都成功引入了,但这一次就是没法搞成功,好在因项目是刚刚开始,没有历史负担,也不想浪费太多时间折腾了,最后是直接用的已经引入了tailwindcss的项目模板开始的。

tailwindcss引入方案选用的是weapp-tailwindcss,官网:tw.icebreaker.top

使用的项目基础模板仓库:github.com/icebreaker-…

uview-plus的ui库引入

官方文挡有提供几种方式在自己项目中引入,我这里使用的Hbuilder X的方式,文挡地址:uiadmin.net/uview-plus/…

image 1.png

点击下载地址去unicloud插件市场下载安装插件即可,安装好插件后,按官方文挡:下载安装方式配置 | uview-plus - 全面兼容nvue/鸿蒙/uni-app-x的uni-app生态框架 - uni-app UI框架 一步一步配置即可,至此UI组件已成功引入

uview-plus我在很多个项目中都有使用,组件挺丰富,但是BUG也有遇到一些,自己改改源码问题不大,最让人恼火的就是每天看文挡都要强制你看一条视频广告,其实页面中放广告,这真的不是问题,这是开源作者的自救,我是支持的,但是强制观看广告才能查文挡有点吃相不是特别好,但这是人家的开源项目,你也不能说什么,如果你很介意那强制的广告官方好像有办会员免广告,或者你可以使用uvui库:我的资料管理-uv-ui 是全面兼容vue3+2、nvue、app、h5、小程序等多端的uni-app生态框架,也是个不错选择,他们的组件API大差不大,我接手过一个老项目里面就用了很多的uvui的组件,我都是改下组件名无缝切换了,当然对于前端开发的我们,uview-plus那小小拦截弹框也难不倒我们,再加之我先接触的是uview-plus,我此项目使用的就是它了

分包实现

对于绝大多数公司的小程序项目,分包是必须要做的,就是不为性能考虑,小程序单包2M的限制,对于公司项目动不动就上百个页面的,不分包是不太可能的,所以我一开始搭基础架构的时候就做好分包

小程序分包是非常简单的,就一个配置一个目录就搞定的事,一个配置就是在pages.json中申明分包名称和当前分包的路由,目录就是分包的本地目录

{
    "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
        {
            "path": "pages/index/index",
            "style": {
                "navigationBarTitleText": "首页",
                "navigationBarTextStyle": "white",
                "navigationBarBackgroundColor": "#191320",
                "disableScroll": true,
                "enablePullDownRefresh": false,
                "navigationStyle": "custom",
                // #ifdef MP-ALIPAY
                "transparentTitle": "always",
                "titlePenetrate": "YES",
                "allowsBounceVertical": "NO"
                // #endif
            }
        },
        ...
    ],
    // 分包配置
    "subPackages": [
        {
            "root": "other",
            "pages": [
                {
                    "path" : "egg/egg",
                    "style" :
                    {
                        "navigationBarTitleText" : "彩蛋页"
                    }
                },
                {
                    "path" : "webview/webview",
                    "style" :
                    {
                        "navigationBarTitleText" : ""
                    }
                },
                ...
  ],
    ...
}

你想把放到子包的页面放到对应子包目录中即完成了分包,这里分了一个other分包,基础架构提供彩蛋页和webview页面

彩蛋页主要用于方便测试,提供了当前小程序所处环境展示,手动切换小程序环境,还有本地缓存管理,这个你可以在开发版中在页面的某一个不显眼的地点放一个入口,我的做法是连续点击底部logo 6次进入彩蛋页,彩蛋页可以根据你项目要求自己实现添加一些方便测试的功能。

image 2.png

webview页面主要是用于小程序中展示H5页面,像隐私协议等一些不是特别重要,但又不得不有的H5页面,通过link传一个链接过去,就可以展示H5页面

通用工具方法

对于前端项目,接口请求,本地缓存操作是基本不会缺少的功能需求,于是引入我自己已经封装好且已经发布在插件市场的一个插件:常用工具方法 - DCloud 插件市场,该插件基于uni.request封装了接口请求,支持请求和响应的拦截,同时地封装本地存储方法,支持设置过期时间,本地存储提供了二个,你可以选择使用,同时提供了小程序获取元素信息(宽高位置等)的方法,对于一些常用的正则也提供了方法,具体使用可以查看文挡,或者查看源码使用,都不复杂,使用中遇到问题可以留言,我会不时观看用户留言的

基础架构模板目录结构

经过上面一些折腾最终生成的项目目录结构如下:

项目根目录
├── .editorconfig                // 编辑器配置文件
├── .gitignore                   // Git忽略文件
├── App.vue                      // 应用入口组件
├── index.html                   // HTML入口文件
├── main.js                      // 主入口文件
├── manifest.json               // uni-app配置文件
├── package.json                // 项目配置文件
├── pages.json                  // 页面路由配置
├── postcss.config.cjs         // PostCSS配置
├── README.md                   // 项目说明文档
├── shared.js                   // 公用配置
├── tailwind.config.js         // Tailwind CSS配置
├── uni.scss                    // uniapp全局样式
├── vite.config.js             // Vite配置文件
│
├── api/                        // API接口目录
│   └── login.js
│
├── components/                 // 组件目录
│
├── config/                     // 配置目录
│   └── http.js                // HTTP请求配置
│
├── other/                      // other子包
│   ├── egg/
│   │   └── egg.vue
│   └── webview/
│       └── webview.vue
│
├── pages/                      // 页面目录
│   ├── example_tailwindcss/
│   │   └── index.vue
│   └── index/
│       └── index.vue
│
├── static/                     // 静态资源目录,放弃项目图片等静态资源
│   └── .gitkeep
│
├── uni_modules/                // uni-app模块目录
│   ├── hbxw-utils/            // hbxw-utils工具包
│   │   ├── changelog.md
│   │   ├── package.json
│   │   ├── readme.md
│   │   └── js_sdk/
│   └── uview-plus/            // uView Plus组件库
│       ├── changelog.md
│       ├── index.js
│       └── ...
│
└── utils/                    // 工具函数目录
    ├── request.js            // 请求工具
    └── share.js              // 微信分享方法

基本项目架构模板分享

代码仓库

其实有机会参与一个项目从0-1是很幸运的,很多时间大多数都是接手前辈们留下的珍贵历史代码,久久在其中出不来,当然这也是常态,我也接手过不少的这种历史项目,其实还好吧,在公司没有下决心重构代码的时候,记住一点就好,前辈的代码能跑就就不要去动它,开发功能也是基于扩展,除非是非动不可,有点说跑题,这一次正好有一个从0到1的项目,在进入开发的时候我就发现有必要整理出一套模板,后续新项目的基础架构可以一步到位,我于是趁这个机会搭建了这一套基础架构模板,欢迎大家clone使用,仓库地址:xiewu/uniapp-vue3-tailwindcss-uview-plus

模板生成器cli

直接clone使用体验,而且代码仓库没有放github,你也不用老担心没法访问的问题,体验也是非常棒的,如果你觉得clone不合自己味口,可以使用我写好的模板生成器cli:@xwy-cli/cli - npm,它可以一键生成这个项目模板,同时还支持生成一些其它开发中常用的文件模板,如.gitignore,editconfig等,后续会有别的模板我也会持续更新添加,欢迎关注使用,如果你有需要添加的模板,我也可以帮忙添加的,当然前提是要通用的,而不是个人定制的,如果你有这种个人定制的需求,可以留言或者私信我,我推荐个软件给你,也是我经常用的用于保存自己的代码段和模板的工具🤝

最后闲聊

模板只提供了基本的项目架构,可能并不一定满足你的需求,或者你clone下来项目报错了,使用不了,就在心里骂娘了,大可不必,我在DCloud插件市场分享了不少组件,有好几个开发者即不说明报什么错,也不说遇到什么问题,直接一星差评又说垃圾代码不要分享出来,我又无法跟踪问题在哪里,很头疼,使用不了有可能跟你node版本或者别的环境问题有关,你可以尝试解决,通过搜索引擎搜索对应错误提示,加之现在又AI胜行,报什么错可以尝试丢给AI,让它帮你瞧瞧,也许调整调整就好了,实在解决不了可以留言或者换一个,东边不亮西边亮👀

鸿蒙next 定位开发全场景实践

一、开场白

在智能设备普及的今天,位置服务已成为移动应用的基础设施。无论是外卖配送的实时轨迹追踪、导航应用的路径规划,还是运动健康类App的卡路里计算,精准的位置定位都是用户体验的关键支撑。鸿蒙next提供的位置服务框架,通过整合GNSS卫星定位与网络定位能力,为开发者提供了一套覆盖多场景、高可用的定位解决方案。本文将结合实际开发场景,深入解析四种典型定位模式的实现原理与代码实践,并分享常见问题的排查思路,帮助大家快速掌握鸿蒙next位置定位开发的核心知识点。

二、定位基础:两种核心定位方式解析

在鸿蒙next中,位置服务主要通过两种方式实现定位:GNSS定位网络定位,二者特性对比如下:

定位方式 技术原理 核心优势 典型场景
GNSS定位 基于GPS、北斗等全球导航卫星系统,通过设备芯片算法解算卫星信号获取位置 精度高(米级) 户外导航、精准打卡
网络定位 整合WLAN热点、蓝牙信标、基站信号等网络数据进行位置估算 响应快(秒级) 室内定位、快速签到

实际开发中,大家可以根据自己的业务需求灵活选择定位策略:例如户外导航场景优先使用GNSS定位保证精度,而室内场景则可结合网络定位提升响应速度。两种定位方式并非互斥,通过合理配置定位请求参数,可实现优势互补。

三、四大核心场景开发实战

3.1 当前位置定位:获取设备瞬时坐标

3.1.1 实现原理

通过getCurrentLocation()接口单次获取设备位置,支持两种定位请求类型:

  • CurrentLocationRequest:通用定位请求,可配置定位优先级(如精度优先或速度优先)
  • SingleLocationRequest:单次快速定位请求,适用于对时效性要求高的场景(如打车定位)

3.1.2 开发步骤

  1. 权限申请:在config.json中声明定位权限
{
  "reqPermissions": [
    {
      "name": "ohos.permission.LOCATION"
    }
  ]
}
  1. 配置定位请求:以快速定位为例,设置速度优先策略
// 实例化单次定位请求对象
const singleRequest: geoLocationManager.SingleLocationRequest = {
  locatingPriority: geoLocationManager.LocatingPriority.PRIORITY_LOCATING_SPEED, // 速度优先策略
  locatingTimeoutMs: 10000 // 超时时间10秒
};
  1. 获取位置信息:通过Promise方式处理异步结果
geoLocationManager.getCurrentLocation(singleRequest)
  .then((location: geoLocationManager.Location) => {
    // 解析位置坐标
    const { latitude, longitude } = location;
    console.log(`当前坐标:纬度${latitude},经度${longitude}`);
  })
  .catch((err: BusinessError) => {
    console.error(`定位失败:${err.code}, ${err.message}`);
  });
  1. 逆地理编码:将坐标转换为地址描述
const reverseGeocodeRequest: geoLocationManager.ReverseGeocodeRequest = {
  latitude: location.latitude,
  longitude: location.longitude
};

geoLocationManager.getAddressesFromLocation(reverseGeocodeRequest, (err, data) => {
  if (data && data.length > 0) {
    const address = data[0].placeName;
    console.log(`当前地址:${address}`); // 如:文松路6号院1号
  }
});

3.2 实时位置定位,追踪运动轨迹

3.2.1 实现原理

通过on('locationChange')接口订阅位置变化事件,支持配置定位场景类型(如步行、驾车)与上报间隔,系统会根据场景自动优化定位策略。

3.2.2 开发步骤

  1. 权限与请求配置:声明权限并创建持续定位请求
const continuousRequest: geoLocationManager.ContinuousLocationRequest = {
  locationScenario: geoLocationManager.UserActivityScenario.NAVIGATION, // 导航场景(优化定位频率)
  interval: 1, // 每秒上报一次位置
  locatingPriority: geoLocationManager.LocatingPriority.PRIORITY_LOCATING_ACCURACY // 精度优先
};
  1. 开启位置订阅:绑定回调函数处理实时数据
// 定义位置变化处理器
const handleLocationChange = (location: geoLocationManager.Location) => {
  const timestamp = new Date().toLocaleTimeString();
  console.log(`${timestamp} 实时坐标:${location.latitude}, ${location.longitude}`);
  // 此处可将坐标发送至服务器更新轨迹
};

// 订阅位置变化事件
geoLocationManager.on('locationChange', continuousRequest, handleLocationChange);
  1. 资源释放:停止定位时取消订阅
// 移除位置变化监听器
geoLocationManager.off('locationChange', handleLocationChange);

··

3.3 应用后台持续获取定位

3.3.1 实现原理

后台定位需要同时申请后台定位权限长时任务权限,通过BackgroundTaskManager维持后台服务,确保应用切至后台后仍能获取位置更新。

3.3.2 开发步骤

  1. 权限声明:在module.json5中配置后台权限
{
  "reqPermissions": [
    {
      "name": "ohos.permission.LOCATION_IN_BACKGROUND",
      "reason": "需要在后台获取位置信息",
      "usedScene": {
        "abilities": ["MainAbility"],
        "when": "always"
      }
    },
    {
      "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
      "reason": "维持后台定位任务"
    }
  ],
  "abilities": [
    {
      "name": ".MainAbility",
      "backgroundModes": ["location"] // 声明支持定位后台模式
    }
  ]
}
  1. 启动后台任务:创建长时任务并绑定定位订阅
// 启动后台定位任务
const startBackgroundTask = () => {
  const context = getContext(this) as common.UIAbilityContext;
  if (!context) return;

  // 创建任务代理
  const wantAgentInfo: wantAgent.WantAgentInfo = {
    wants: [{
      bundleName: context.abilityInfo.bundleName,
      abilityName: context.abilityInfo.name
    }],
    operationType: wantAgent.OperationType.START_ABILITY
  };

  wantAgent.getWantAgent(wantAgentInfo).then(wantAgentObj => {
    backgroundTaskManager.startBackgroundRunning(context, 
      backgroundTaskManager.BackgroundMode.LOCATION, 
      wantAgentObj
    ).then(() => {
      // 启动位置订阅
      this.subscribeLocationChange();
      console.log('后台任务启动成功');
    });
  });
};
  1. 位置订阅与处理:在后台任务中持续获取位置
private subscribeLocationChange() {
  const request: geoLocationManager.ContinuousLocationRequest = {
    locationScenario: geoLocationManager.UserActivityScenario.FITNESS, // 运动场景(平衡精度与功耗)
    interval: 5 // 每5秒上报一次
  };

  geoLocationManager.on('locationChange', request, (location) => {
    // 存储轨迹数据或发送至云端
    this.saveTrack(location);
  });
}
  1. 停止后台任务:确保资源正确释放
const stopBackgroundTask = () => {
  const context = getContext(this) as common.UIAbilityContext;
  backgroundTaskManager.stopBackgroundRunning(context).then(() => {
    geoLocationManager.off('locationChange'); // 取消位置订阅
    console.log('后台任务停止');
  });
};

3.4 历史定位获取,结合缓存数据

3.4.1 实现原理

通过getLastLocation()接口获取系统缓存的最近一次有效位置,适用于网络信号弱或需要降低功耗的场景,比如后台静默定位。

3.4.2 开发步骤

// 获取缓存位置
const lastLocation = geoLocationManager.getLastLocation();
if (lastLocation) {
  console.log(`缓存坐标:${lastLocation.latitude}, ${lastLocation.longitude}`);
  // 执行逆地理编码
  this.reverseGeocode(lastLocation);
} else {
  console.log('没有可用缓存位置,需要发起实时定位');
}

3.4.3 注意事项

  • 缓存位置可能非最新,需结合时间戳判断有效性
  • 首次定位时可能无缓存数据,需 fallback 至实时定位

四、常见问题排查和解决

4.1 定位不准或偏差

问题现象

定位结果在地图上显示偏移,尤其在国内使用非华为地图时更为明显。

根因分析

  • 定位接口返回的是国际通用的WGS84坐标系
  • 国内地图服务(如华为地图)通常使用GCJ02坐标系,直接渲染会导致偏移

解决方案

// WGS84转GCJ02坐标系示例(需引入坐标转换工具库)
import { wgs84ToGcj02 } from '@huawei/map-kit-utils';

const { latitude, longitude } = location;
const [gcjLat, gcjLng] = wgs84ToGcj02(latitude, longitude);
// 使用转换后的坐标渲染地图

4.2 定位失败了如何检查

排查步骤 检查点 修复措施
1. 权限校验 是否申请ohos.permission.LOCATION module.json5配置文件中补充权限声明
2. 系统设置 设备定位开关是否开启 引导用户至系统设置开启位置服务
3. 网络状态 是否连接网络/插入SIM卡 提示用户检查网络连接
4. 物理环境 是否处于室内或信号遮挡区域 建议移动至开阔地带重新定位

4.3 缓存位置不一致

问题场景

连续调用getCurrentLocation()getLastLocation()返回不同结果。

原因解析

系统缓存位置为全局共享,其他应用的定位操作可能刷新缓存。

应对策略

// 获取位置时记录时间戳
const currentLocation = {
  ...location,
  timestamp: Date.now()
};

// 对比缓存位置时间
const lastLocation = geoLocationManager.getLastLocation();
if (lastLocation && currentLocation.timestamp - lastLocation.timestamp < 30000) {
  // 缓存有效,使用当前数据
} else {
  // 缓存过期,发起新定位
}

五、性能优化与功耗控制

5.1 定位策略动态调整

  • 户外场景:优先使用GNSS定位(PRIORITY_LOCATING_ACCURACY),提升精度
  • 室内场景:切换至网络定位(PRIORITY_LOCATING_SPEED),降低功耗
  • 后台场景:采用UserActivityScenario.FITNESS模式,延长上报间隔至30秒以上

5.2 资源及时释放

  • 非必要时调用off('locationChange')取消位置订阅
  • 后台任务使用完毕后通过stopBackgroundRunning()终止服务
  • 页面销毁时及时释放资源

5.3 功耗测试

可以使用DevEco Studio的Energy Profile工具分析定位模块功耗,重点关注:

  • 定位接口调用频率
  • 后台任务存活时间
  • 网络请求与传感器使用时长

六、总结

本文通过了最常见的四大核心场景,展现了鸿蒙next位置服务从权限配置、定位请求构建到数据处理的完整流程,大家重点关注以下的四点。

  1. 定位策略的选择:根据业务需求平衡精度与功耗
  2. 坐标系转换:国内场景需强制进行WGS84到GCJ02的坐标转换
  3. 后台任务管理:合理使用长时任务,避免资源泄漏
  4. 异常处理:完善的错误捕获与用户引导机制

没有设计稿也能很漂亮,非常适合独立开发:Trae + 飞个马MCP

大家好,我是一名前端工程师,也是开源图片编辑器vue-fabric-editor项目的作者,最近一直在迭代我们的商业版图片编辑器😍,因为团队规模比较小,没有专门的设计师,就尝试使用Trae + figma + MCP来优化页面样式,没想到效果超级棒,真的惊艳到我了, 非常适合没有设计师提供设计稿的小团队或者独立开发者。

作为一个工作十余年的切图仔,真的觉得是在解放生产力,这里做一下使用的简单介绍,推荐给大家。

说明

大部分开发者都希望一键生成,目前看多少还是有点噱头的,直接生成HTML可以,但是要生成完整可运行的代码,稍微加点业务逻辑就不行了,但是换个思路,稍微调整一下步骤,就出现了事半功倍的效果

我的思路是先开发功能,再调整样式,使用起来效果就很好。

我们的步骤是先让实习的同事做功能开发,把调用接口和展示逻辑开发完成,但是一般页面都会素素的,很没有食欲(别笑,你写也不行...), 如下图:

image.png

然后我再通过Trae + figma + MCP来做样式优化,这是优化完成的效果,下边是调整后的效果:

image.png

样式优化的结果我很满意的,另外我只是在AI的结果上做了轻微少量的调整,真的很高效。

如何使用

一共分为5步,前2个步骤只需要设置一次,几分钟搞定,后续直接使用就可以。

  1. 获取Figma账号 Token。
  2. Trae设置 MCP Token。
  3. Figma 挑选喜欢的模板
  4. 复制元素链接并交给AI,预览结果
  5. 微调 上线。

1. 获取Figma账号 Token。

登录后从设置页面,生成Token,权限选择只读。

20250520094828.png

image.png

image.png

2. Trae设置MCP Token

搞前端Trae还不知道就不说了,这么漂亮的编辑器,用起来很顺手,我是不舍得换了。 AI对话框点击设置 => MCP,然后点击添加,搜索Figma AI Bridge,安装后设置Token就可以了。

image.png

image.png

image.png

好了,这些设置只需要1次,设置完以后就不用每次调整了,接下来就是使用了。

3. Figma 挑选喜欢的模板

接下来就很简单了,在Figma网站上挑选自己喜欢的模板,我搜索的关键词是 dashboard,可以挑选一些和现有页面机构类似的效果图。

image.png

这是我挑选的几个效果图:

image.png

image.png

4. 复制元素链接并交给AI,预览结果

Figma 可以直接定位到某个元素的链接,我们选中一个区域后,右键复制链接。

image.png

然后在Trae的AI对话框中选择智能体,把链接复制上,并选中要调优的代码,把你的需求告诉AI。

image.png

image.png

5. 微调 上线

相比比较我们自己手写很多样式去调整,AI的效率很高了,好描述好理解的就交给AI,简单的就自己手动调整一下(别太懒,AI再智能就没工作了😂)。

image.png

结尾

自己也算是一个比较资深的切图仔了,从网页三剑客的Dreamweaver写Table布局开始,再到Sublime的快捷键编写网页,再到VScode,再到现在的AI类智能编辑器,真的是翻天覆地的变化。

我很认同在某个播客采访中提到的一个观点:积极的拥抱AI吧,未来是属于会用好AI的人。

最后,为我们的开源图片编辑器 github.com/ikuaitu/vue… 拉个粉,大家Star一下吧。

计算机图形学中的法线

引言

在计算机图形学中,法线(Normal)是一个基础且关键的概念。它们在光照计算、表面交互、物理模拟等多个领域都有重要应用。理解法线的概念及其工作原理,是掌握计算机图形学渲染技术的重要一步。

为什么需要学习法线?

法线是 3D 渲染中光照计算的基础。没有正确的法线信息,3D 模型看起来会是完全平坦的,没有阴影、高光或任何立体感。

什么是法线?

在计算机图形学中,法线是垂直于表面的向量。对于平面来说,法线在整个平面上都是相同的;但对于曲面,法线会随着表面的弯曲而变化。

法线的直观理解

想象你站在一个 3D 模型的表面上,头顶所指的方向就是该点的法线方向。对于一个完美的球体,表面上每个点的法线都指向球心的反方向。

  • 法线总是垂直于表面
  • 法线通常是单位向量(长度为 1)
  • 法线的方向决定了表面看起来是朝向还是背向光源

法线的数学表示

在 3D 空间中,法线通常用三维向量表示。对于一个平面,可以通过平面上的两个不平行向量的叉乘来计算法线。

向量叉乘计算法线

给定平面上的两个向量 A 和 B,它们的叉乘结果是一个垂直于这两个向量的新向量,即平面的法线。

// 向量叉乘计算法线
function crossProduct(vectorA, vectorB) {
    return [        vectorA[1] * vectorB[2] - vectorA[2] * vectorB[1],
        vectorA[2] * vectorB[0] - vectorA[0] * vectorB[2],
        vectorA[0] * vectorB[1] - vectorA[1] * vectorB[0]
    ];
}
// 计算平面法线示例
const vectorA = [1, 0, 0]; // X轴方向的向量
const vectorB = [0, 1, 0]; // Y轴方向的向量
const normal = crossProduct(vectorA, vectorB);
console.log("计算得到的法线:", normal); // 输出: [0, 0, 1],即Z轴方向

注意: 法线向量的方向遵循右手定则:如果你的右手四指从向量 A 弯曲到向量 B,那么大拇指所指的方向就是法线方向。

归一化法线向量

在实际应用中,我们通常需要将法线向量归一化,使其长度为 1。这样可以确保光照计算的一致性和正确性。

// 向量归一化函数
function normalize(vector) {
    const length = Math.sqrt(
        vector[0] * vector[0] + 
        vector[1] * vector[1] + 
        vector[2] * vector[2]
    );
    
    // 避免除以零
    if (length === 0) return [0, 0, 0];
    
    return [
        vector[0] / length,
        vector[1] / length,
        vector[2] / length
    ];
}
// 归一化法线示例
const nonNormalizedNormal = [3, 4, 0];
const normalizedNormal = normalize(nonNormalizedNormal);
console.log("归一化前:", nonNormalizedNormal); // 输出: [3, 4, 0]
console.log("归一化后:", normalizedNormal);   // 输出: [0.6, 0.8, 0],长度为1

计算法线

在计算机图形学中,我们通常需要为 3D 模型的每个顶点或面计算法线。下面介绍几种常见的计算方法。

面法线(Face Normals)

面法线是指垂直于多边形面的法线。对于三角形面,可以通过其三个顶点的位置计算得出。

// 计算三角形面法线
function calculateFaceNormal(vertexA, vertexB, vertexC) {
    // 计算边向量
    const edge1 = [        vertexB[0] - vertexA[0],
        vertexB[1] - vertexA[1],
        vertexB[2] - vertexA[2]
    ];
    
    const edge2 = [        vertexC[0] - vertexA[0],
        vertexC[1] - vertexA[1],
        vertexC[2] - vertexA[2]
    ];
    
    // 计算叉乘
    const normal = crossProduct(edge1, edge2);
    
    // 归一化
    return normalize(normal);
}
// 示例:计算三角形面法线
const vertexA = [0, 0, 0];
const vertexB = [1, 0, 0];
const vertexC = [0, 1, 0];
const faceNormal = calculateFaceNormal(vertexA, vertexB, vertexC);
console.log("三角形面法线:", faceNormal); // 输出: [0, 0, 1]

顶点法线(Vertex Normals)

顶点法线是指与顶点相关联的法线。对于平滑表面,顶点法线通常是共享该顶点的所有面法线的平均值。

// 计算顶点法线
function calculateVertexNormals(vertices, faces) {
    // 初始化所有顶点法线为零向量
    const vertexNormals = Array(vertices.length).fill().map(() => [0, 0, 0]);
    
    // 遍历每个面,累加面法线到对应的顶点
    faces.forEach(face => {
        const vA = vertices[face[0]];
        const vB = vertices[face[1]];
        const vC = vertices[face[2]];
        
        // 计算面法线
        const faceNormal = calculateFaceNormal(vA, vB, vC);
        
        // 累加到每个顶点的法线
        for (let i = 0; i < 3; i++) {
            vertexNormals[face[i]][0] += faceNormal[0];
            vertexNormals[face[i]][1] += faceNormal[1];
            vertexNormals[face[i]][2] += faceNormal[2];
        }
    });
    
    // 归一化所有顶点法线
    return vertexNormals.map(normalize);
}
// 示例:计算简单立方体的顶点法线
const cubeVertices = [    // 前面    [0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0],
    // 后面
    [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]
];
const cubeFaces = [    [0, 1, 2], [0, 2, 3], // 前面
    [1, 5, 6], [1, 6, 2], // 右面
    [5, 4, 7], [5, 7, 6], // 后面
    [4, 0, 3], [4, 3, 7], // 左面
    [3, 2, 6], [3, 6, 7], // 上面
    [4, 5, 1], [4, 1, 0]  // 下面
];
const vertexNormals = calculateVertexNormals(cubeVertices, cubeFaces);
console.log("立方体顶点法线:", vertexNormals);

法线在着色中的应用

法线在光照和着色计算中起着核心作用。它们决定了光线如何与物体表面交互,从而影响表面的亮度和颜色。

兰伯特着色模型(Lambertian Shading)

兰伯特着色模型是一种基于表面法线和光线方向的基本光照模型。表面的亮度与表面法线和光线方向之间的夹角的余弦成正比。

// 兰伯特着色计算
function lambertShading(normal, lightDirection) {
    // 确保法线和光线方向都是单位向量
    const normalizedNormal = normalize(normal);
    const normalizedLightDirection = normalize(lightDirection);
    
    // 计算点积
    const dotProduct = 
        normalizedNormal[0] * normalizedLightDirection[0] +
        normalizedNormal[1] * normalizedLightDirection[1] +
        normalizedNormal[2] * normalizedLightDirection[2];
    
    // 确保结果非负(如果表面背对光源,则为0)
    return Math.max(0, dotProduct);
}
// 示例:计算表面点的光照强度
const surfaceNormal = [0, 0, 1]; // 表面法线朝上
const lightDirection = [0.5, 0, -0.5]; // 光线从斜上方照射
const intensity = lambertShading(surfaceNormal, lightDirection);
console.log("光照强度:", intensity); // 输出约为0.707

Phong 着色模型

Phong 着色模型在兰伯特模型的基础上增加了镜面反射分量,能够模拟光泽表面的高光效果。

// Phong着色计算
function phongShading(normal, lightDirection, viewDirection, shininess) {
    // 兰伯特漫反射分量
    const diffuseIntensity = lambertShading(normal, lightDirection);
    
    // 计算反射光线方向
    const normalizedNormal = normalize(normal);
    const normalizedLightDirection = normalize(lightDirection);
    
    // 反射向量计算: R = 2(N·L)N - L
    const dotNL = 
        normalizedNormal[0] * normalizedLightDirection[0] +
        normalizedNormal[1] * normalizedLightDirection[1] +
        normalizedNormal[2] * normalizedLightDirection[2];
    
    const reflectionDirection = [
        2 * dotNL * normalizedNormal[0] - normalizedLightDirection[0],
        2 * dotNL * normalizedNormal[1] - normalizedLightDirection[1],
        2 * dotNL * normalizedNormal[2] - normalizedLightDirection[2]
    ];
    
    // 计算镜面反射分量
    const normalizedViewDirection = normalize(viewDirection);
    const dotRV = 
        reflectionDirection[0] * normalizedViewDirection[0] +
        reflectionDirection[1] * normalizedViewDirection[1] +
        reflectionDirection[2] * normalizedViewDirection[2];
    
    const specularIntensity = Math.max(0, dotRV);
    const specularComponent = Math.pow(specularIntensity, shininess);
    
    // 返回总光照强度(漫反射 + 镜面反射)
    return diffuseIntensity + specularComponent;
}
// 示例:计算Phong着色
const viewDirection = [0, 0, 1]; // 观察方向
const shininess = 32; // 光泽度参数
const phongIntensity = phongShading(surfaceNormal, lightDirection, viewDirection, shininess);
console.log("Phong光照强度:", phongIntensity);

法线贴图(Normal Mapping)

法线贴图是一种纹理技术,通过存储表面细节的法线信息来模拟复杂的表面细节,而不需要增加实际的几何复杂度。

法线贴图的原理

法线贴图使用 RGB 颜色来存储表面法线信息。在法线贴图中:

  • 红色通道存储法线的 X 分量
  • 绿色通道存储法线的 Y 分量
  • 蓝色通道存储法线的 Z 分量
// 从法线贴图颜色值还原法线向量
function decodeNormalFromTexture(rgbColor) {
    // rgbColor是一个包含R、G、B值的数组,范围从0到255
    const r = rgbColor[0] / 255;
    const g = rgbColor[1] / 255;
    const b = rgbColor[2] / 255;
    
    // 将颜色值从[0,1]范围转换到[-1,1]范围
    const normal = [
        r * 2 - 1,
        g * 2 - 1,
        b * 2 - 1
    ];
    
    // 归一化法线向量
    return normalize(normal);
}
// 示例:从法线贴图颜色值还原法线
const textureColor = [128, 128, 255]; // 典型的蓝色法线贴图颜色
const normalVector = decodeNormalFromTexture(textureColor);
console.log("还原的法线向量:", normalVector); // 输出: [0, 0, 1]

切线空间(Tangent Space)

法线贴图通常在切线空间中定义,这样可以在不同的表面方向上正确应用。切线空间由三个向量定义:

  • 法线向量(Normal):垂直于表面
  • 切线向量(Tangent):沿着纹理 U 方向
  • 副切线向量(Bitangent):沿着纹理 V 方向,由法线和切线叉乘得到
// 计算切线空间矩阵
function calculateTangentSpace(normal, tangent) {
    // 归一化输入向量
    const normalizedNormal = normalize(normal);
    const normalizedTangent = normalize(tangent);
    
    // 计算副切线
    const bitangent = crossProduct(normalizedTangent, normalizedNormal);
    
    // 返回TBN矩阵(切线-副切线-法线矩阵)
    return [
        normalizedTangent[0], normalizedTangent[1], normalizedTangent[2],
        bitangent[0], bitangent[1], bitangent[2],
        normalizedNormal[0], normalizedNormal[1], normalizedNormal[2]
    ];
}
// 示例:计算TBN矩阵
const surfaceNormal = [0, 0, 1]; // 表面法线
const surfaceTangent = [1, 0, 0]; // 表面切线
const tbnMatrix = calculateTangentSpace(surfaceNormal, surfaceTangent);
console.log("切线空间矩阵:", tbnMatrix);

总结

法线是计算机图形学中不可或缺的概念,它们在光照计算、表面渲染和物理模拟中起着关键作用。通过本文,你应该对法线有了更深入的理解,包括:

  • 法线的基本概念:法线是垂直于表面的向量,用于确定表面在空间中的朝向。
  • 法线的计算方法:可以通过向量叉乘计算面法线,通过平均相邻面法线计算顶点法线。
  • 法线在光照中的应用:法线是计算光照效果的基础,如兰伯特漫反射和 Phong 镜面反射。
  • 法线贴图技术:法线贴图通过存储表面细节的法线信息,在不增加几何复杂度的情况下模拟复杂表面细节。

下一步学习建议

  • 学习更高级的光照模型,如 Blinn-Phong 模型和 PBR(基于物理的渲染)
  • 探索其他法线相关的纹理技术,如视差贴图和浮雕贴图
  • 了解法线在实时渲染引擎(如 Unity、Unreal Engine)中的应用

SVG 图形路径与 Three.js 绘图路径的关联与差异

在 Web 图形绘制领域,SVG(Scalable Vector Graphics)和 Three.js 是两个常用的技术。SVG 主要用于二维矢量图形绘制,而 Three.js 则是用于创建和渲染三维计算机图形的 JavaScript 库。尽管它们的应用场景有所不同,但在图形路径的概念和实现上存在一定的关联,同时也有各自的特点。接下来,我们深入探讨 SVG 的图形路径和 Three.js 的绘图路径之间的关系。

一、路径的基本概念

1.1 SVG 路径

SVG 中的路径通过path元素来定义,使用一组指令和参数来描述图形的形状。这些指令包括移动到(M)、直线到(L)、水平直线到(H)、垂直直线到(V)、曲线到(C、S、Q、T)等,参数则用于指定坐标点和曲线的控制点。例如,下面这段代码绘制了一个简单的三角形:

<svg width="200" height="200">
  <path d="M 100 10 L 190 190 L 10 190 Z" stroke="black" stroke-width="2" fill="none" />
</svg>

在上述代码中,M 100 10表示将绘图起点移动到坐标(100,10),L 190 190绘制一条从当前点到(190,190)的直线,L 10 190再绘制一条到(10,190)的直线,最后Z表示闭合路径,将当前点与起点连接起来。

1.2 Three.js 路径

在 Three.js 中,路径的概念同样用于描述形状,不过主要用于创建二维图形,之后可以通过拉伸、旋转等操作将其转换为三维模型。Three.js 中的路径通过THREE.Path类来创建和管理。例如:

const path = new THREE.Path();
path.moveTo(0, 0);
path.lineTo(100, 0);
path.lineTo(100, 100);
path.lineTo(0, 100);
path.closePath();

上述代码先实例化了一个THREE.Path对象,然后使用moveTo方法设置起始点,lineTo方法绘制直线,最后通过closePath方法闭合路径,形成一个正方形路径。

二、两者的关联

2.1 基础绘图指令相似性

SVG 和 Three.js 在基础绘图指令上有明显的相似性。比如,SVG 中的M(移动到)指令对应 Three.js 中Path类的moveTo方法,都是用于设置绘图的起始位置;SVG 的L(直线到)指令和 Three.js 中Path类的lineTo方法功能一致,都是从当前点绘制一条直线到指定点。这种相似性使得熟悉 SVG 路径绘制的开发者能够快速上手 Three.js 中的路径创建。

2.2 路径数据的转换可能性

由于两者在路径描述上的相似性,SVG 的路径数据是可以转换为 Three.js 可用的路径数据的。可以通过解析 SVG 的d属性值,提取其中的指令和参数,然后对应调用 Three.js 中Path类的方法来重新构建路径。例如,使用 JavaScript 的字符串处理方法解析 SVG 的d属性:

const svgPathData = "M 100 10 L 190 190 L 10 190 Z";
const threePath = new THREE.Path();
const commands = svgPathData.split(/\s+/).filter(Boolean);
let currentCommand;
for (const command of commands) {
  if (/^[A-Za-z]$/.test(command)) {
    currentCommand = command;
  } else {
    const [x, y] = command.split(",").map(Number);
    if (currentCommand === "M") {
      threePath.moveTo(x, y);
    } else if (currentCommand === "L") {
      threePath.lineTo(x, y);
    }
  }
}

上述代码将 SVG 路径数据解析并转换为了 Three.js 的路径对象。

2.3 作为形状基础

无论是 SVG 还是 Three.js,路径都是构建复杂形状的基础。在 SVG 中,通过组合各种路径指令可以绘制出各种二维图形;在 Three.js 中,路径可以作为ExtrudeGeometry(拉伸几何体)、LatheGeometry(车床几何体)等三维几何体创建的基础形状,通过对路径进行三维操作生成复杂的三维模型。例如,使用 Three.js 的ExtrudeGeometry将之前创建的正方形路径拉伸成一个立方体:

const geometry = new THREE.ExtrudeGeometry(path, {
  depth: 20,
  bevelEnabled: false
});
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

三、两者的差异

3.1 维度与应用场景

SVG 主要用于二维图形的绘制和展示,适用于图标、图表、简单动画等场景;而 Three.js 专注于三维图形的创建和渲染,常用于游戏开发、3D 可视化、虚拟场景构建等。这导致了它们在路径处理上的侧重点不同,SVG 路径更关注二维平面上的形状精确绘制,Three.js 路径则更多作为三维建模的起点。

3.2 指令丰富度与复杂度

SVG 的路径指令更加丰富和细致,拥有多种曲线绘制指令(如三次贝塞尔曲线C、二次贝塞尔曲线Q等),可以精确控制曲线形状;相比之下,Three.js 的Path类提供的指令相对较少,主要集中在基础的直线和简单曲线绘制上。如果需要在 Three.js 中创建复杂曲线,可能需要通过多次调用简单指令或者借助数学计算来模拟。

3.3 与渲染系统的集成方式

SVG 是基于 HTML 和 CSS 的,其路径绘制和渲染直接由浏览器的渲染引擎处理,与网页的布局和样式系统紧密结合;而 Three.js 有自己独立的渲染系统,需要通过场景(Scene)、相机(Camera)和渲染器(Renderer)来完成图形的渲染,路径创建后还需要经过几何体和材质的设置以及添加到场景中等一系列步骤才能最终显示出来。

四、实际应用示例

4.1 使用 SVG 路径创建动态图表

在一个简单的柱状图示例中,我们可以使用 SVG 路径来绘制柱状条。假设我们有一组数据[10, 20, 15, 25],可以通过以下代码绘制柱状图:

<svg width="300" height="200">
  <g transform="translate(20, 20)">
    <rect x="0" y="180" width="50" height="-10" fill="blue" />
    <rect x="60" y="160" width="50" height="-20" fill="blue" />
    <rect x="120" y="170" width="50" height="-15" fill="blue" />
    <rect x="180" y="150" width="50" height="-25" fill="blue" />
  </g>
</svg>

这里虽然没有直接使用path元素,但也可以通过path元素结合直线指令来绘制更复杂的自定义形状的图表。

4.2 使用 Three.js 路径创建三维模型

下面我们使用 Three.js 路径创建一个螺旋楼梯模型。首先创建螺旋形状的路径:

const path = new THREE.Path();
const numSteps = 20;
const radius = 5;
for (let i = 0; i <= numSteps; i++) {
  const angle = (i / numSteps) * (2 * Math.PI);
  const x = radius * Math.cos(angle);
  const y = (i / numSteps) * 10;
  const z = radius * Math.sin(angle);
  if (i === 0) {
    path.moveTo(x, z);
  } else {
    path.lineTo(x, z);
  }
}

然后将路径拉伸成几何体并创建三维模型:

const geometry = new THREE.ExtrudeGeometry(path, {
  depth: 0.5,
  bevelEnabled: false
});
const material = new THREE.MeshBasicMaterial({ color: 0x8B4513 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

通过以上内容,我们详细了解了 SVG 的图形路径和 Three.js 的绘图路径之间的关联与差异。无论是进行二维图形绘制还是三维模型创建,理解和掌握路径的概念和使用方法都至关重要。在实际项目中,可以根据具体需求灵活选择和运用这两种技术,充分发挥它们的优势。

以上文章全面剖析了两者绘图路径的联系与区别。若你觉得某些部分还需深入展开,或想补充其他内容,欢迎随时告诉我。

🚀一文看懂 npm 与 pnpm 的本质区别!不仅更快,甚至更安全!

你还在用 npm 吗?可能你已经踩过“幽灵依赖”的坑但没意识到。而今天,我们不仅要搞懂 npm 与 pnpm 的核心差异,还要快速上手 pnpm,享受它带来的飞一般的开发体验!

在日常开发中,你是否遇到过以下情况:

  • “某个包你明明没有安装,却能正常使用。”
  • “CI 测试莫名其妙挂了,本地一切正常。”
  • “升级依赖后突然报错,毫无头绪。”
  • “不仅安装依赖慢,node_modules 占用磁盘空间还大”

🎯 什么是 pnpm?它真的值得换吗?

pnpm 是一款速度飞快、节省磁盘空间、并彻底解决依赖管理问题的包管理器。

维度 npm pnpm(优势)
依赖结构 扁平结构,容易产生“幽灵依赖” 严格隔离依赖,根治幽灵依赖
安装速度 中等,重复下载依赖 极快,复用缓存,避免重复下载
磁盘空间 多项目重复安装,占用大 利用硬链接,节省50%以上磁盘空间
monorepo 支持 需额外工具(如 Lerna) 原生支持 Workspaces
Peer依赖冲突 运行时可能出错 安装时严格校验,提前发现冲突
迁移成本 - 命令兼容 npm,切换零成本

✅ 为什么 pnpm 更“安全”?看两个关键设计!

1. 使用“硬链接”,多项目共享依赖,磁盘更轻

pnpm 会把依赖缓存到统一的 .pnpm-store,再通过硬链接写入各项目的 node_modules

项目A、项目B 共用 react@18.2.0
→ 实际只下载一次,磁盘只占一份!

不仅安装快,还能在弱网/离线状态下秒装依赖(本地就存在)。

2. 安装阶段就校验 peerDependencies,早发现冲突

pnpm 默认启用 strict-peer-dependencies,防止不一致版本潜入项目中。举个例子:

"my-lib": {
  "peerDependencies": {
    "react": "^18.0.0"
  }
}

如果你的项目用了 react@17,npm 会静悄悄放过,而 pnpm 会立即报错阻止安装,提前避免踩雷。

🔍 什么是“幽灵依赖”?你可能踩过而没意识到

幽灵依赖 ,指的是你在项目中使用了某个包,但它并没有出现在 package.json 中的 dependenciesdevDependencies 中。

这是 npm/yarn 的扁平化结构导致的副作用,例如:

// 你在项目中直接用到了 lodash
import _ from 'lodash';

但你的 package.json 并没有依赖声明:

"dependencies": {
  // 没有 lodash
}

项目还能跑?是因为 lodash 是某个间接依赖(如 webpack)的依赖,被 npm 扁平化安装到了 node_modules/ 根目录。但你换台电脑、CI 构建、升级依赖时,就可能直接 崩了

node_modules/
├─ webpack/
├─ lodash/  ← 本不该在这里,但 webpack 引入了它

而 pnpm 的结构则完全隔离每个包的依赖路径(沙箱隔离),避免这种“偷用”现象:

node_modules/
├─ .pnpm/
│  ├─ webpack@5.0.0/
│  │   └─ node_modules/
│  │       └─ lodash/

pnpm 常用命令速查表(几乎无学习成本)

场景 npm 命令 pnpm 命令 说明
安装所有依赖 npm install pnpm install 安装项目依赖
添加生产依赖 npm install lodash pnpm add lodash 默认加到 dependencies
添加开发依赖 npm install -D ts pnpm add -D ts 加到 devDependencies
移除依赖 npm uninstall lodash pnpm remove lodash 删除依赖
运行脚本 npm run build pnpm build 支持直接执行脚本名
清理缓存 npm cache clean --force pnpm store prune 清理缓存
查看缓存路径 npm config get cache pnpm store path 依赖缓存实际路径
查看依赖树 npm ls pnpm list 依赖树结构
更新依赖 npm update lodash pnpm update lodash 升级包到兼容版本
发布包 npm publish pnpm publish 完全兼容

pnpm 使用 pnpm-lock.yaml 代替 package-lock.json 锁文件。如何下载 pnpm:

npm install -g pnpm

项目迁移小贴士:统一团队用 pnpm

为了团队协作更顺畅,推荐用 only-allow 工具强制统一包管理器:

// package.json
"scripts": {
  "preinstall": "npx only-allow pnpm",
  "dev": "pnpm dev",
  "build": "pnpm build"
}

通过 only-allow 工具,开发或 CI 阶段都会强制使用 pnpm,避免混用 npm/yarn 导致的依赖差异或构建异常

总结

不信你试试:

pnpm install
pnpm list

感受一下安装速度和依赖结构的清晰度!
遇到 npm/yarn 依赖坑,也欢迎留言一起避坑~

❌