蘇z 发布的文章

过去几个月中有两个需求都用到了html2canvas插件,目的是使Web中的DOM结构转为图片,但由于html2canvas的原理限制,遇到了一系列问题:

跨域问题

html2canvas转图片的原理是将传入的dom元素渲染到canvas画布上,然后利用canvas的toDataURL方式转位图片的。然而html2canvas在将dom元素绘制在canvas上时,如下html2canvas加载图片的核心代码所示:

return await new Promise((resolve, reject) => {
            const img = new Image();
            img.onload = () => resolve(img);
            img.onerror = reject;
            //ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
            if (isInlineBase64Image(src) || useCORS) {
                img.crossOrigin = 'anonymous';
            }
            img.src = src;
            if (img.complete === true) {
                // Inline XML images may fail to parse, throwing an Error later on
                setTimeout(() => resolve(img), 500);
            }
            if (this._options.imageTimeout > 0) {
                setTimeout(
                    () => reject(`Timed out (${this._options.imageTimeout}ms) loading image`),
                    this._options.imageTimeout
                );
            }
        });

对于img会重新发起网络请求获取图片,因为是js发起的请求,所以对于有跨域限制的图片会提示跨域错误,而且可以注意到当传入的options参数设置useCORS时,是会设置img.crossOrigin = 'anonymous'的。
要想解决这个问题,最简单的方法就是在图片服务器上进行设置允许跨域,但是这种方式在服务器自己不可控的情况下是行不通的。所以在解决问题的过程中就想着是否有前端侧的方法来避免这个问题呢?网上有很多文章介绍说可以给图片设置crossorigin="anonymous"html2canvas的option设置userCORS: true、提前把跨域的网络图片转为base64等方法来解决,但是经过一系列折腾后发现,这些方法都是需要服务端的支持才行得通的。可行的方法主要包括:

  • 服务端配置跨域访问
  • 设置代理服务器
  • 图片数量固定且不多的情况下可选择放置在项目中

图片显示不完整问题

当dom元素有滚动条时在生成图片后可能会出现图片不完整,部分区域是空白的情况,可以调用图下代码 dom.scrollTo(0, dom.scrollHeight)控制滚动条至页面底部解决。另外还可能出现图片如果有偏移的话,可添加配置scrollY:0, scrollX:0,解决。

所生成图片边缘有缝隙/空白问题

由于不同设备的屏幕像素密度不一样,html2canvas在计算宽高的时候可能出现了误差导致。解决方法:

  • 设置宽度或高度减去几个像素,刚好把空白的地方裁掉(当然前提是在不影响图片内容的情况下)
  • 如果是纯色背景,可以设置html2cavas配置里的背景色为相同颜色即可

透明图片异常显示的问题

很奇怪的异常,最开始这个图片是以背景图方式显示的,最后换成img标签形式展示就没这个问题了。

由dom元素的动画引起的显示不完整问题

问题的根因是html2canvas在解析传入的dom节点之前会先去克隆一个副本,这个操作的过程可能就重新触发了动画的播放条件,于是html2cavas在将dom绘制至canvas时可能只是截取到了动画过程中的一个状态。解决这个问题的本质是要在html2cavans绘制dom时,dom的动画是不执行的或者动画已经执行完毕,具体解决方法包括:

  • 另外创建一个不可视且无动画版本的dom作为html2canvas的入参
  • 利用定时器保证在动画播放完毕时再执行html2canvas
  • 声明一个css类,设置规则animation-duration: 0s !important; animation-delay: 0s !important;使动画执行时间可延迟时间都为0s,这样dom会直接处于动画执行完毕的状态,当执行html2canvas之前动态添加这个类,当执行完毕后再删除掉这个类,不影响后续动画的执行。

问题背景

在一次使用Chrome开发者工具调试中,发现找不到接口参数了,但仔细检查代码后以及结合接口响应数据来看确确实实是传参数了的,并且后端也正常接收到了请求数据,当时没有深入探究原因,现在对这个问题进行一下复盘。

接口数据传输的方式

PayloadForm Data是接口数据传输的两种方式,浏览器通过Content-Type来进行区分使用哪种方式:

  • 当值为application/x-www-form-urlencodedmultipart/form-data时,是Form Data方式
  • 当值为application/json时,是Request Payload方式

    Request Payload

    如下代码所示,使用axios发起post请求,设置请求头'Content-Type':'application/json',成功发起请求后可在调试工具Payload Tab面板中看到请求参数。Request Payload通常用于发送JSONXML格式的数据。

    axios.post(host + '/payload', {
      username: 'admin',
      password: 'admin',
    }, {
      headers: {
        'Content-Type': 'application/json'
      }
    }).then((res) => {
      console.log(res)
    }).catch((err) => {
      console.log(err)
    })

    Form Data

    如下代码所示,设置请求头'Content-Type':'application/x-www-form-urlencoded',请求参数使用Qs插件进行序列化后使用axios发送,在调试工具Payload Tab面板中可看到请求参数为Form Data类型。Form Data类型通常用于发送表单数据,并且可用来进行上传文件

    axios.post(host + '/payload', Qs.stringify({
      username: 'admin',
      password: 'admin',
    }), {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    }).then((res) => {
      console.log(res)
    }).catch((err) => {
      console.log(err)
    })


    如上图所示,再点击view source后,可看到实际上传输的数据是一个字符串,由于设置了正确的Content-Type,可以被浏览器正常识别。但如果请求参数不进行序列化,还是用Form Data方式进行请求,浏览器依然是能够正常发送出请求的,并且浏览器的Payload将会显示:

    但是如果Server端如果仍按照Form Data方式对请求参数处理,那么可能会出错。所以,请求参数一定要和Content-Type是对应的,否则可能会影响Server端对请求参数的正常解析。

    Query String Parameters

    另外在Chrome中,Query参数也会被放到Payload中,如下图所示:

    Chrome 96版本前,Request PayloadForm Data信息是在HeadersTab中的,96版本之后它们都被放到了PayloadTab中。

    问题复现

    所发生问题的接口参数是Request Payload格式的,并且注意到其中有个对象数据量是很大的,因此根据这一特征构造一个请求尝试复现问题:

    如图所示,通过循环构造了一个长字符串作为对象data属性值,经过调整不同的循环次数进行测试,确定了当Content-Length超过10000000字节时Payload Tab就会消失,并且PreviewResponse显示“Failed to load response data: request content was evicted from inspector cache”。

    这句话翻译为中文意思是“未能加载响应数据:请求内容已从检查器缓存中收回”,大概意思就是由于超出缓存限制,请求参数以及响应参数都不能正常显示了。通过观察后端接口的请求以及浏览器请求状态码(200 OK)可知,虽然调试工具不显示请求以及响应数据,但实际上是没有影响接口的正常请求以及响应的,并且前端也能够正常接收到后端响应数据,也就是说这个问题是不影响项目的正常运行的。
    另外通过测试发现,当Server端的响应数据过长时,在浏览器调试工具中也会发生同样的事情,而且也不会实际影响接口数据的响应。

    问题原因

    通过查询发现以下一些情况都可能导致此问题:

  • 请求的请求数据超过了浏览器的缓存限制
  • 请求的响应数据超过了浏览器的缓存限制
  • 在浏览器的开发者工具中查看了大量的请求,并超过了浏览器的缓存限制
  • 电脑可用内存不足

背景

有个移动端项目用了Vant组件库进行开发,其中有个需求要实现一个轮播图的效果,但是Vant组件库自带的轮播图组件功能有限,只能实现一些很基础的效果,而且在其基础上只进行一些样式的调整也难以实现想要的效果,所以就考虑使用其它的插件。
Swiper是很著名的一个用于轮播图及移动动画的插件,面向手机、平板电脑等移动终端,并且开源免费。在网上进行搜索之后发现大多数的用法都是用原生js方式所实现的,并且Swiper官方在7版本之前也是没有适配Vue的,因此为了在Vue中能以组件方式进行调用Swiper,还诞生了一个12.7k star的第三方开源项目vue-awesome-swiper。另外在Vant的官网上也提到,如果需要更复杂的轮播效果,可以使用vue-awesome-swiper,因此就开始了一系列的踩坑之旅。

遇到的问题(坑)

安装模块

点击进入vue-awesome-swiper的github主页,就可看到黄色的提示以及官方介绍,说是由于Swiper官网对vue的支持,这个库已经弃用了,当时看到这个提示的时候并没有细想(确实是有点急),看了如下的版本兼容性列表后发现 vue-awesome-swiper是支持项目所使用的Vue2的,于是就想着那就用它了。

点击进去vue-awesome-swiper@4.1.1的github主页之后,看到了官方给的安装指令,心想真好,一顿CV执行了命令yarn add swiper vue-awesome-swiper,甚至都没有多看一眼但是很顺利安装上了vue-awesome-swiper。

组件调用

根据官方的引导进入了示例网站Examples (Vue2),在里面找到了一个与想要效果类似的示例,并将代码复制到项目中测试运行,首先是导入组件:

import { Swiper, SwiperSlide } from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'

然后IDE报错,说是swiper.css找不到,其实这时就应该有所察觉的,但当时就想着怎么官网实例都有问题呢,可真不靠谱。在晚上一顿搜索说是,Swiper不同的版本swiper.css的位置可能是不一样的(这会竟然还没意识到Swiper的版本有问题),然后修改了正确的地址,没报错了。

<template>
  <swiper class="swiper" :options="swiperOption">
    <swiper-slide>Slide 1</swiper-slide>
    <swiper-slide>Slide 2</swiper-slide>
    <swiper-slide>Slide 3</swiper-slide>
    <swiper-slide>Slide 4</swiper-slide>
    <swiper-slide>Slide 5</swiper-slide>
    <swiper-slide>Slide 6</swiper-slide>
    <swiper-slide>Slide 7</swiper-slide>
    <swiper-slide>Slide 8</swiper-slide>
    <swiper-slide>Slide 9</swiper-slide>
    <swiper-slide>Slide 10</swiper-slide>
    <div class="swiper-pagination" slot="pagination"></div>
  </swiper>
</template>
<script>
  export default {
    data() {
      return {
        swiperOption: {
          slidesPerView: 3,
          spaceBetween: 30,
          pagination: {
            el: '.swiper-pagination',
            clickable: true
          }
        }
      }
    }
  }
</script>

接着把上面的代码复制进行了项目中,也成功运行了,并且没有任何报错,但是出来的效果没有和官网效果图一样,代码中slidesPerView设置3含义是容器中同时会出现3个swiper-slide,如下图所示。但实际上运行出的效果只有一个swiper-slide,之后又对比了与官方代码的区别,并没有什么不同,这就导致对官方代码暂时失去了信任。

初步实现效果

于是就在网上搜索了vue-awesome-swiper的相关用法,最终找到了能实现类似效果的一篇文章,那篇文章中的代码和官方代码相比,除了swiperOption配置外,最大的区别就是添加了一些自定义的css样式,使其实现了想要的效果。但是一个最大的问题就是分页器不显示,配置中的pagination属性就描述了分页器的相关信息,最初以为是配置写错了,但是经过一番检查后还是没发现问题,而且还用了旧版本的pagination写法也还是不行。
通过Chrome的调试工具可发现,模板中写的paginationdiv里面是空的,但是官方运行的实例里面是有元素的,也就是说pagination根本就没有生成,所以这就不是单单的样式问题了,就像是我写的配置没有生效一样,然后我还尝试修改了其它的一些配置发现都是没有效果的。

新的思路

这时想这应该还是仔细看下官方文档比较好,于是又回到了vue-awesome-swiper的github主页,才重视了"Swiper官方已对vue进行了支持"这句话,又心想既然已经有了官方的支持,那么就不用这个作为中间桥梁的第三方插件了吧,免不了这个插件会有什么莫名其妙的bug呢。
Swiper官方在从7版本开始就对各大流行组件库进行了支持,包括Vue、React、Angular等。但是Swiper Vue组件的文档中有一句红色提示,说Swiper Vue组件以后很可能就不再支持了,推荐使用Swiper ElementSwiper78为适配Vue、React、Angular,都要为他们单独各写一套代码,随着Web Component技术的发展与流行,Swiper就提倡开发者使用Web Component组件进行开发。
经过这一番考量,想着既然这样就直接用最新的吧(完全把兼容性抛在了脑后),又是一顿安装运行,发现总是报错,最终发现是不支持Vue2的。接着又尝试了Swiper Vue组件,甚至是退回到了7版本,都是不支持Vue2的(官网并没有看到醒目的兼容性提示)。
这才醒悟了为什么vue-awesome-swiper为什么重点适配到了Swiper6,因为Swiper6之前是没有官网支持的组件的,而vue-awesome-swiper恰恰就干了这个事情,并且也支持Vue2。那为什么刚开始我运行是有问题的呢,总不能用原生js方式去用吧?

恍然大悟

干了这么多问题也没解决,但让我突然想到了是不是我最开始安装的vue-awesome-swiper版本有问题了,这时又去看了一下最开始安装命令yarn add swiper vue-awesome-swiper发现没有加版本号,这就导致我安装的Swiper是最新的11版本!!!怪不得出现了乱七八糟的问题呢。

最终解决

调整了安装命令为yarn add swiper@5 vue-awesome-swiper@4,又一次成功安装了Swiper。运行了代码,分页器的DOM结构终于出来了,但样式还有点问题。通过审查发现官网实例上的分页器一些样式,在我的项目里是没有的,所以肯定就是引入的样式有问题了,去看了node_modules里的css文件,重新引入了一下,问题终于解决。
这时调整Swiper配置swiperOption,都是有效果的,这也证实了刚开始的配置根本没生效的猜想。

反思

本来没有多难得事情浪费了那么久,实属不应该,其中暴露出来的问题主要有:

  • 在不熟悉插件的情况下没有认真看文档
  • 习惯性的运行安装插件的代码,没有进行思考
  • 用新的模块/插件,没有考虑兼容性的问题
  • 做事不能乱着急,所谓心急吃不了热豆腐呀

在使用git进行代码版本控制时,经常会遇到分支合并的情况,而mergerebase都能够实现分支的合并,那么什么时候用merge什么时候用rebase呢。

Branch分支

git中的分支可以理解为是一条条的开发线路,这些线路可能会有重叠的部分,也可能有自己独有的部分,而且不同的线路还可以合并到一起。实质上git中的分支是指向了某个commit的指针,创建分支相当是创建了一个指针,指向某个commit,分支的切换实质上是指针的切换,git中的指针主要有Branch指针(分支指针)和Head指针。

Branch指针

如上图所示,dev分支和feature分支实质上就是分支指针,它们分别指向了各自分支中最新的提交。

Head指针

一个项目中可能会有多个分支,但当前使用的只能是一个分支,Head指针可以理解为是一种标记,它指向了当前正在使用的分支指针。当使用git checkout或者git switch命令切换分支时实际上就是切换Head指针的指向,将Head指针指向了Branch指针。如下图所示,Head指针指向了dev分支指针,即表示当前正在使用的分支是dev分支。
注:一种例外情况,当使用git checkout切换到当前分支的某次commit上时,Head指针的指向将从Branch指针移动到此commit上,并且在内部创建一个匿名分支,这种情况下Head指针处于一种游离状态。开发者在此匿名分支上进行修改之后,可选择保存或者丢弃修改,如果保存修改并进行了commit,那么可以使用git checkout -b [New Branch Name] 命令创建一个新的分支(Branch指针)。

merge命令

如下图所示,假设有一个开发分支dev,以及从dev分支拉取的特性分支feature,然后在feature分支上进行新功能的开发并有了两次提交DE。如果要将feature分支上的内容mergedev分支,考虑到此时dev分支可能没有新的提交也可能有新的提交,就会存在两种情况,这两种情况的merge分别称为快进式合并(Fast-forward merge)和三路合并(Three-way merge)。

快进式合并

如上图所示,如果dev分支没有新的提交,那么feature分支和dev分支其实是在一条链上的,只需要dev分支指针指向到feature分支指针上即可,如下图所示。

三方合并

如图所示,如果dev分支有了新的提交FG,此时两个分支所做的修改会生成一个新的提交E,然后dev分支将会移动到该提交上。具体来说,就是分别将feature分支的提交DE以及dev分支的提交FG,和两个分支的共同祖先C,这三方进行合并并生成一次新的提交H。另外,因为dev分支和feature分支有可能会对同一文件进行了修改,所以三路合并过程中git可能会提示冲突,需要开发者解除冲突后才可完成合并。
注:如果设置git merge时使用了--no-ff(non fast-forward)参数,那么即使满足快进式合并的条件,也会以类似三路合并的方式生成一次新的提交完成merge操作。

rebase命令

如上图左图所示,有dev分支和feature两个分支,它们有共同的祖先C,并且在此基础上还都有两次新的提交,如上节所属,使用merge命令进行合并,将会产生一次新的提交。如使用以下rebase命令进行合并:

git checkout dev
git rebase feature

其中,feature称为基分支,dev称为待变基分支,当执行 rebase 操作时,git会从两个分支的共同祖先开始提取待变基分支上的修改,然后将待变基分支指向基分支的最新提交,最后将刚才提取的修改应用到基分支的最新提交的后面。
结合上图解释,就是在执行rebase操作时,git会先将待变基分支dev上的新提交FG提取出来暂存,然后dev分支指针会指向feature分支的最新提交E上,接着暂存的提交FG会接在E后面,最后dev分支指针移动到G上,完成rebase操作。其中要注意的是,虽然rebase操作后端提交FG和之前的提交内容是一样的,但是它们的commit id是不同的,因为它们实质上是用同样的内容进行了行的提交,而且在rebase过程中也可能会存在冲突。

git pull的过程

通常使用git pull命令将本地代码与远端的代码同步,执行git pull命令实际上会先执行git fetch,再执行git merge命令,其中,git fetch表示将远程仓库的所有文件拉取到本地的远程仓库,然后git merge将本地远程仓库的文件与本地仓库中的文件进行合并。但是当使用git pull --rebase命令拉取代码时,在fetch操作之后,git将会运行git rebase命令去合并代码。

总结

  1. 下游分支更新上游分支内容的时候根据情况使用mergerebase
  2. 上游分支(master)合并下游分支内容的时候使用merge
  3. 更新当前正在使用分支的内容时使用rebase

mergerebase虽都能完成分支的合并,但由于其合并原理的不同,拥有不同的使用场景。git merge适合在公共分支上使用,用以将其它分支合并到公共分支,由于merge操作会新生成一个commit提交,因此会留下清晰的合并记录便于追溯合并情况。git rebase适合个人分支(只自己一个人提交)。日常开发过程中,个人分支代码需要和公共分支代码保持一致,定期合并公共分支代码到个人分支,这种场景很适合rebase,可以形成线性提交记录,更加直观。

盒模型

概念

所有的HTML元素都被一个看不见的盒子包围着,这个盒子被称为盒模型(Box Model)。

如图所示,是Chrome浏览器所绘制的某个元素的盒模型示意图,由内容区(content)、内边距(padding)、边框(border)、外边距(margin)。

内容区

指盒子的主要内容区域,用来显示和存储元素的“真实”内容,通常可由 widthmin-widthmax-widthheightmin-height 和 max-height分别设置内容区的宽度最小宽度最大宽度高度最小高度最大高度

内边距

内容区边框之间的距离,可使用padding属性设置或者padding-toppadding-bottompadding-leftpadding-right分别可设置上、下、左、右四个方向上的距离。内边距不支持设置负值,可以设置百分比,且按照宽度计算

边框

内边距外边距间的距离,可使用boder属性设置或者使用border-widthborder-styleborder-color分别设置边框的宽度样式颜色等属性

外边距

指元素边框周围一定距离的空间,与内边距类似,可使用padding属性设置或者margin-topmargin-bottommargin-leftmargin-right分别设置上、下、左、右四个方向上的距离。但外边距可设置负值,相邻元素之间的外边距大小遵循规则“正正按大,正负相加,负负最小”

分类

实际上,CSS的盒模型有两种,分别是标准盒模型怪异盒模型,它们的区别主要体现在元素的width属性是否只包含内容区的宽度。

标准盒模型

在标准盒模型中,widht属性只包含内容区content的宽度。因此标准盒模型的宽度为:width+padding+border+magin

元素可通过设置box-sizing: content-box使用标准盒模型,这也是默认的属性。

怪异盒模型(IE盒模型)

图2IE模型宽度计算示意图

在怪异盒模型中,width属性不仅包含内容区content宽度,还包括边框和内边距的宽度。因此标准盒模型的宽度为:width+magin

元素可通过设置box-sizing: border-box使用怪异盒模型。

BFC

概念

块级盒子(Block-level Box)和行内盒子(Inline Box)

块级元素和行内元素会生成上述盒模型所表达的块级盒子和行内盒子,这两种盒子类型是CSS中用来描述HTML元素的基本显示类型,可通过display属性的值来定义元素的显示类型。块级盒子的display属性值通常为"block",行内盒子的display属性值通为"inline"(并不绝对)。

BFC的定义

MDN的定义为:区块格式化上下文(Block Formatting Context,BFC)是 Web 页面的可视 CSS 渲染的一部分,是块级盒子(Block-level Box)布局过程发生的区域,也是浮动元素与其他元素交互的区域。

通俗来说,BFC是一个独立的渲染区域,他规定了内部块级盒子的布局方式,此区域的布局不会受到BFC外部元素布局的影响。

注:实际开发中,BFC中是允许存在行内元素的,但是上述定义却说BFC是块级盒子的布局过程发生的区域,这是因为行内元素所存在的内联盒子在布局时会形成一个匿名块级盒子,该匿名块级盒子的布局行为与普通块级盒子类似。因此,内联盒子在BFC中的布局行为可以看作是一种特殊的块级盒子布局。

BFC的产生条件

  • 文档的根元素(<html>)。
  • 浮动元素( float 值不为 none 的元素)。
  • 绝对定位元素(position值为 absolute 或 fixed 的元素)。
  • display值为 inline-blocktableinline-tabletable-celltable-captiontable-rowtable-row-grouptable-header-grouptable-footer-groupflow-root
  • overflow值不为 visible 或 clip 的块级元素。
  • contain 值为 layoutcontent 或 paint 的元素。
  • 弹性元素(display 值为 flex 或 inline-flex 元素的直接子元素),如果它们本身既不是弹性、网格也不是表格容器。
  • 网格元素(display值为 grid 或 inline-grid 元素的直接子元素),如果它们本身既不是弹性、网格也不是表格容器。
  • 多列容器(column-count 或 column-width值不为 auto,且含有 column-count: 1 的元素)。
  • column-span 值为 all 的元素始终会创建一个新的格式化上下文,即使该元素没有包裹在一个多列容器中。

BFC的布局规则

  • 内部的Box会在垂直方向,一个接一个地放置。
  • Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Boxmargin会发生重叠。
  • 每个盒子(块盒与行盒)的margin box的左边,与包含块border box的左边相接触(对于从左往右的格式化,否则相反),即使存在浮动也是如此。
  • BFC的区域不会与float box重叠。
  • BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之亦然。
  • 计算BFC的高度时,浮动元素也参与计算。

BFC在开发中的作用

解决外边距重叠问题

假如页面中有两个div元素,他们的样式设置如下:

    #div1, #div2{
        width: 100px;
        height: 100px;
    }
    #div1 {
        background-color: yellow;
        margin: 20px;
    }
    #div2 {
        background-color: aliceblue;
        margin: 30px;
    }

只看代码可能会理所当然的认为两个div之间的边距应该是50px,但通过调试工具发现实际上只有30px

这是因为当两个div同时设置margin的时候,它们接触地方的外边距会发生重叠,即实际边距为两者中最大的一个。其原因就是BFC的布局规则第二条所讲的属于同一个BFC的两个相邻Boxmargin会发生重叠。既然规则中说的是同属于一个BFC下的两个DIV会发生这种情况,那么只需将其放在不同的BFC下,就可以解决这个问题。

如以下代码所示,在div2上新包裹一个div标签,并设置属性overflow: hidden创建一个BFC区域,从而解决外边距重叠的问题。

    #div1, #div2{
        width: 100px;
        height: 100px;
    }
    #div1 {
        background-color: yellow;
        margin: 20px;
    }
    #div2 {
        background-color: aliceblue;
        margin: 30px;
    }
    #div3 {
        overflow: hidden;
    }

包含内部浮动元素

假设div1作为父元素,有div2div3两个子元素,并且设置以下样式:

    #div2, #div3{
        width: 100px;
        height: 100px;
    }
    #div1 {
        border: 1px solid;
        margin: 20px;
    }
    #div2 {
        background-color: aliceblue;
    }
    #div3 {
        background-color: grey;
        float: left;
    }

由上图可知,父元素div1并没有包含浮动子元素div3的高度,这便是经典的高度塌陷问题。

根据BFC布局规则的第六条,计算BFC的高度时,浮动元素也参与计算,因此可通过为div1设置样式overflow: hidden,使其创建一个BFC的方式,消除因子元素浮动引起的高度塌陷。

排除外部浮动元素

假设有div1div2两个元素,设置以下样式:

    #div1 {
        background-color: yellowgreen;
        float: left;
    }
    #div2 {
        background-color: lightblue;
    }

如上图所示,当div1设置浮动后,div2中的文本会环绕在div1周围。

根据BFC布局规则的第四条,BFC的区域不会与float box重叠,可以给div2设置 overflow: hidder属性从而创建一个独立的BFC后,从而形成一种两栏布局,如下图所示: