100vh小记

3,181 阅读6分钟

起始

最近H5 项目,遇到了一个「有趣」的问题,首先我们看一个简单的demo,一个列表,从1-100

<div class="container">
  <div class="page"></div>
  <div class="tabbar">TabBar</div>
</div>
.page {
    background-color: #f8f8f8;
    height: calc(100vh - 50px);
    overflow-y: scroll;
}

.tabbar {
    position: absolute;
    bottom: 0;
    width: 100%;
    height: 50px;
    line-height: 50px;
    background-color: #fff;
    box-sizing: border-box;
}

image.png

这样看着是没有什么问题的,我们在iPhone 的 Safari 浏览器打开此页面试一下,以iphoneX 15.0以及ios 13 15.4的系统为例。

iphoneX 15.0

image.pngimage.png

ios 13 15.4

image.png"image.png

我们可以看到,页面刚一开始进去是正常的,但是iphonex在下滑的过程中,顶部和底部导航栏会收缩起来。iphone13的状态栏会一直保持,但是底部有被遮挡。

原因

我们知道,vh 是 CSS 中的一种相对长度单位,1vh 表示 viewport 高度的 1%)。简单来讲,viewport 基本上是指当前文档的可见部分,因此 100vh 表示可见文档的最大高度。

"图片自定义高度" height="300" width=""

按上面所说,100vh 不应该是 viewport 可视区域的全部高度,为什么右图的高度会超出的呢? 可以看一张简图: "图片自定义高度" height="300" width=""

这就是为什么在 Safari 会被挡住一部分的原因。

为什么

这是个bug,还是其它什么原因么?

这里有一篇文章,有一个用户,给 WebKit 提了个 bug,其中 Apple 工程师 Benjamin Poulain 的回答如下,什么意思呢? "图片自定义高度" height="300" width=""

这完全是故意的。我们花了很多功夫才达到这个效果。:)

基本问题是:滚动时可见区域会动态变化。如果我们相应地更新 CSS 视口高度,我们需要在滚动期间更新布局。这不仅看起来很糟糕,而且在大多数页面中以 60 FPS 的速度执行此操作实际上是不可能的(60 FPS 是 iOS 上的基准帧率)。

很难向您展示“看起来很糟糕”的部分,但想象一下,当您滚动时,内容会移动,屏幕上您想要的内容也在不断变化。

动态更新高度不起作用,我们有几个选择:在 iOS 上删除视口单位,像 iOS 8 之前一样匹配文档大小,使用小视图大小,使用大视图大小。

从我们拥有的数据来看,使用较大的视图尺寸是最好的折衷方案。大多数使用视口单位的网站在大多数时候看起来都很棒。

然而,并不是只有 Safari 是这样做的,比如 iOS 端 Chrome 浏览器表现与 iOS 端 Safari 一致。

解决问题

方案一:使用 -webkit-fill-available

.page {
    background-color: #f8f8f8;
    min-height: 100vh;
    min-height: -webkit-fill-available;
    overflow-y: scroll;
    padding-bottom: 50px; // 注意,动态设置距离底部的高度
}

.tabbar {
    position: fixed;
    bottom: 0;
    width: 100%;
    height: 50px;
    line-height: 50px;
    background-color: #fff;
    box-sizing: border-box;
}

然而,以上方案并不适用于这些情况,比如:height: 90vhheight: calc(100vh - 50px)

方案二: 通过 CSS 变量计算 1vh 所表示的实际高度 思路:

设置一个 CSS 变量(比如 --vh),然后通过 JavaScript 脚本动态设置 --vh 的值,然后使用时需兼容处理即可 比如,height: 100vh; height: calc(var(--vh) * 100)

:root {
  --vh: 1vh;
}

.page {
  background-color: #f8f8f8;
  height: calc(var(--vh) * 100 - 50px);
  overflow-y: scroll;
}

.tabbar {
  position: absolute;
  bottom: 0;
  width: 100%;
  height: 50px;
  line-height: 50px;
  background-color: #fff;
  box-sizing: border-box;
}
function setViewHeight() {
    // 计算当前窗口高度的 1% 的值,用作 CSS 自定义属性的值
    var windowVH = window.innerHeight / 100 
    // 将计算得到的窗口高度的 1% 的值设置为名为 --vh 的 CSS 自定义属性的值,单位为像素
    document.documentElement.style.setProperty('--vh', windowVH + 'px') 
}
// 在页面加载完成后,调用 setViewHeight 函数,即初始化时计算并设置视图的高度
document.addEventListener('DOMContentLoaded', setViewHeight) 
// 当屏幕窗口大小改变时,重新计算并设置视图的高度
window.addEventListener('resize', setViewHeight) 

方案三:postcss-100vh-fix github.com/postcss/pos…

了解规则集之前需要先了解一下两个属性@supports,-webkit-touch-callout,-webkit-fill-available。

@supports

@supports CSS at-rule 您可以指定依赖于浏览器中的一个或多个特定的 CSS 功能的支持声明。这被称为特性查询。该规则可以放在代码的顶层,也可以嵌套在任何其他条件组规则中。

通过使用 @supports 规则,你可以根据不同浏览器的支持情况,为不同的 CSS 功能设置不同的样式。

@supports (display: grid) {
  div {
    display: grid;
  }
}

@supports not (display: grid) {
  div {
    float: right;
  }
}

-webkit-touch-callout

-webkit-touch-callout 这个CSS 属性禁用了默认的 callout 展示,callout 是指当触摸并按住一个元素的时候出现的提示。

当在 iOS 上一直按住一个目标元素时,Safari 会展示一个关于这个链接的 callout 信息。webkit-touch-callout属性允许禁用掉这一行为。

-webkit-fill-available

css的自适应关键字,其作用为撑满可用空间.

什么是规则集

在 CSS 中,规则集(Rule Set)是一组定义了样式属性和值的规则的集合,用于定义如何渲染 HTML 或 XML 文档中的元素。每个规则集通常由一个选择器和一个声明块组成。 简单的 CSS 规则集

h1 {
  color: blue;
  font-size: 24px;
}
  • h1 是选择器,表示应用样式于 HTML 中的 <h1> 元素
  • color font-size属性
  • blue 和 24px 是值,用于设置 <h1> 元素的文本颜色和字体大小 这个规则集将使所有 <h1> 元素的文本颜色变为蓝色,并将字体大小设置为 24 像素。
// decl 表示当前处理的 CSS 属性声明节点
// { AtRule, Rule } 表示 PostCSS 中的两个辅助类,用于创建新的规则和规则集
function process (decl, { AtRule, Rule }) {
  if (decl.value !== '100vh') return
  // 获取当前 CSS 属性声明节点的父节点,即所属的规则集。
  let rule = decl.parent

  // 创建一个新的 @supports 规则,并设置其参数为 (-webkit-touch-callout: none)
  // 并将其来源位置(source)设置为当前属性声明节点的来源位置(source)。
  let media = new AtRule({
    name: 'supports',
    params: '(-webkit-touch-callout: none)',
    source: decl.source
  })
  // 将创建的 @supports 规则插入到当前规则集的后面。
  rule.after(media)

 // 创建一个新的规则集,其选择器和来源位置(source)与当前规则集相同。
  let clonedRule = new Rule({
    selector: rule.selector,
    source: rule.source
  })
  // 将新创建的规则集作为子节点添加到 @supports 规则中。
  media.append(clonedRule)

    //  将一个新的属性声明添加到新创建的规则集中,其中属性为当前属性声明节点的属性名,值为 '-webkit-fill-available'
    // 并将来源位置设置为当前属性声明节点的来源位置。
  clonedRule.append({
    prop: decl.prop,
    value: '-webkit-fill-available',
    source: decl.source
  })
}

module.exports = () => {
  return {
    postcssPlugin: 'postcss-100vh-fix',
    Declaration: {
      'min-height': process,
      'max-height': process,
      'height': process
    }
  }
}
module.exports.postcss = true