引子
布局视口,理想视口,初始包含块等概念都是什么意思,如何正确的区分,如何使用响应式布局呢?meta标签是什么意思呢,各种宽高相关的 API有什么区别,如何实现虚拟滚动呢?这篇文章会尽量理清这些容易让人困惑的概念~
几个概念
首先对于视口, CSS像素,物理像素等概念做一个简单的介绍~
屏幕尺寸|分辨率|像素密度
- 屏幕尺寸:屏幕对角线的长度(单位为英寸)
- 屏幕分辨率:屏幕在宽高上的物理像素数量,区别于图片的分辨率,图片分辨率是指图像在宽高上每英寸有多少个像素点
- 屏幕像素密度:显示器在宽高上单位长度的像素数量
-
CSS像素|物理像素|设备像素比
- CSS像素(逻辑像素):设备独立像素(device independent pixel),可以通过
window.screen.width以及window.screen.height获取浏览器的CSS像素宽高
CSS像素的单位是px,我们说它是一个相对单位,相对性体现在在同一个设备上或不同设备之间每1px代表的物理像素是变化的
px的含义因上下文不同而不同:
- CSS中: CSS像素(逻辑单位),相对单位,跨设备统一
- 图像尺寸中: 位图像素(物理像素),是固定的像素网格
- canvas / video: 通常是设备像素或可配置单位
那么对于一个图像,我们将100px x 100px的图像(物理像素)设置为width:100px,这个px是CSS单位
- 在普通屏幕(DPR=1):图片原图大小 = 显示大小,没问题
- 在 Retina 屏(DPR=2):需要提供
2x图像资源,否则图片会被拉伸模糊- DPR=2 => 1 个 CSS 像素 = 2×2 个设备像素(4 倍区域)
- 所以定义的 width: 100px 实际会占据 200×200 设备像素, 把图片“拉伸”到更大的空间去显示 →所以会变模糊!
- 可以选择对于Retina屏提供更高分辨率的图,称为 2x 图
这个 logo@2x.png 的真实尺寸是 200×200 像素,但显示仍然是 100px 宽高,浏览器在 Retina 屏幕上用 200×200 的图像内容显示成 100×100 的逻辑尺寸,就能高分清晰、不模糊
- 物理像素(设备像素):屏幕真正显示图像的最小单位,指的是手机或电脑屏上的发光点,手机的物理分辨率指的是一块屏幕横竖有多少物理像素排列
- 设备像素比:每个 CSS 像素占多少设备像素,如 DPR=2 → 1 个 CSS 像素 = 2×2 设备像素(不缩放的情况下)
浏览器缩放 = 改变 CSS 像素 和 设备像素之间的映射比例
浏览器本来的 DPR 可能是 1(1 CSS px = 1 设备 px),把页面放大 200%,实际上就是让 1 个 CSS 像素 = 2 个设备像素, 于是页面看起来大了(因为内容需要更多真实像素来渲染)
布局视口|理想视口|视觉视口
在PC端,视口(viewport)指的是浏览器的可视区域,它的宽度和浏览器的宽度是一致的,不包含滚动条、工具栏等浏览器自身的UI。在相对单位中的vw 和 vh 就是相对于这里的视口,JavaScript 或媒体查询时用它判屏幕尺寸
初始包含块是
<html>元素的包含块,是所有元素布局的根源块盒子,大小,位置默认是由"视口"决定的,受到<meta>的影响,所有绝对定位,百分比布局都要基于它计算
布局视口和meta标签
布局视口
布局视口是浏览器用来排版和布局页面的**「虚拟画布」**,它可能不同于设备实际的屏幕宽度,尤其是在移动端。利用meta标签可以改变布局视口~ meta标签
1. 没有设置 meta viewport 的情况(早期移动浏览器行为)
移动设备默认用宽虚拟视口(如980px)渲染网页(即:默认的布局视口宽度),由于默认情况下,移动设备的浏览器会假设网页是为 PC 编写的,所以会导致针对小屏的媒体查询(如max-width: 640px)基于虚拟宽度判断,无法触发。此时写 div { width: 100%; },它是相对于 ICB(980px)计算的,导致页面在小屏上横向超出。
2. 设置了 meta viewport 之后
<meta name="viewport" content="width=device-width, initial-scale=1.0">
通过 <meta name="viewport" content="width=device-width"> 将虚拟视口设为设备实际宽度(如375px),媒体查询即可基于真实屏幕尺寸生效,确保移动端优化样式正常应用。同时,由于ICB被设置了正确的值,百分比和定位可以正确计算生效了
这是一个典型的meta标签的写法:
<!-- 设置布局视口为设备宽度,并禁止缩放 -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
- width=600 or width=device-width(让视口宽度等于设备的 CSS 逻辑像素宽度), 这里设置的取值会定义
vw单位的计算基准- 一般没有特殊情况都推荐写
width=device-width
- 一般没有特殊情况都推荐写
- initial-scale:控制首次加载页面时的缩放级别
- ......
视觉视口
是 用户此刻屏幕上可见的网页区域 用户进行 缩放操作(放大/缩小) 时,视觉视口会发生变化:
- 缩小页面:视觉视口变大(能看到更多页面内容)
- 放大页面:视觉视口变小(看到的内容更少)
理想视口
理想视口就是开发者希望页面最终呈现的那个视口宽度(一般等于设备宽度),用于实现无缩放、自然适配屏幕的最佳浏览体验。 主要是理解布局视口,后面两个了解即可~
相关API
在了解了相关的概念之后,我们对于宽高相关的API做一个简单的介绍,这些API大部分我们经常看到,但是很难做出合适的区分~
1.vw | vh | innerHeight | outerHeight
1.1 vw | vh
vw= 1% of the width of the viewport size.vh= 1% of the height of the viewport size. 注意,视口只代表当前"可视区域",而不是整个页面的长度。
举个例子,也就是说:
- 如果你的页面高度是 3000px,但视口高度只有 800px,
- 那么
100vh始终是 800px,不管页面多长。
这也是为什么说 vh 不受内容长度影响。同时如果动态缩放视口的话,这个视口也会动态变化的
1.2 window.innerHeight | window.outerHeight
window.innerHeight视口高度window.outerHeight整个浏览器窗口高度(包括工具栏、地址栏、边框等)
1.3 window.pageXOffset | window.pageYOffset(window.scrollX | window.scrollY)
window.pageYOffset 和 window.pageXOffset: 这两个属性都属于window对象, 用于获取整个页面的垂直和水平滚动距离,这两个属性的值和 scrollY、scrollX 的值一样,通常用于计算页面的滚动位置。
console.log(window.pageYOffset); // 当前页面的垂直滚动距离
console.log(window.pageXOffset); // 当前页面的水平滚动距离
console.log(window.scrollY); // 当前页面的垂直滚动距离
console.log(window.scrollX); // 当前页面的水平滚动距离
1.4 window.screen.width | window.screen.height
- window.screen.width => 设备整个屏幕的宽度(包括浏览器外部,比如状态栏)
- window.screen.height => 设备整个屏幕的高度(也是物理像素)
2. clientHeight | offsetHeight | scrollHeigh
在介绍了window对象身上几个常见的属性之后,接下来介绍元素身上几个常见的属性. 首先区分是 clientHeight、offsetHeight、以及scrollHeight这三个概念,在overflow看到一个回答(what-is-offsetheight-clientheight-scrollheight)很形象,这里直接粘过来了~
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#MainDIV {
border: 5px solid red;
}
</style>
</head>
<body>
<button id="offset">offsetHeight & offsetWidth</button>
<button id="client">clientHeight & clientWidth</button>
<button id="scroll">scrollHeight & scrollWidth</button>
<div
id="MainDIV"
style="margin: auto; height: 200px; width: 400px; overflow: auto"
>
<div style="height: 400px; width: 500px; overflow: hidden"></div>
</div>
<script>
function whatis(propType) {
var mainDiv = document.getElementById("MainDIV");
if (window.sampleDiv == null) {
var div = document.createElement("div");
window.sampleDiv = div;
}
div = window.sampleDiv;
var propTypeWidth = propType.toLowerCase() + "Width";
var propTypeHeight = propType + "Height";
var computedStyle = window.getComputedStyle(mainDiv, null);
var borderLeftWidth =
computedStyle.getPropertyValue("border-left-width");
var borderTopWidth = computedStyle.getPropertyValue("border-top-width");
div.style.position = "absolute";
div.style.left =
mainDiv.offsetLeft +
Math.round(parseFloat(propType == "client" ? borderLeftWidth : 0)) +
"px";
div.style.top =
mainDiv.offsetTop +
Math.round(parseFloat(propType == "client" ? borderTopWidth : 0)) +
"px";
div.style.height = mainDiv[propTypeHeight] + "px";
div.style.lineHeight = mainDiv[propTypeHeight] + "px";
div.style.width = mainDiv[propTypeWidth] + "px";
div.style.textAlign = "center";
div.innerHTML =
propTypeWidth +
" X " +
propTypeHeight +
"( " +
mainDiv[propTypeWidth] +
" x " +
mainDiv[propTypeHeight] +
" )";
div.style.background = "rgba(0,0,246,0.5)";
document.body.appendChild(div);
}
document.getElementById("offset").onclick = function () {
whatis("offset");
};
document.getElementById("client").onclick = function () {
whatis("client");
};
document.getElementById("scroll").onclick = function () {
whatis("scroll");
};
</script>
</body>
</html>
上面可以直接看出三者的大概区别,下面具体来了解以下[client] [offset] 以及[scroll]相关的API
2.1 clientWidth、clientHeight、clientLeft、clientTop
<style>
.clientDOM {
width: 200px;
height: 200px;
background-color: rgba(0, 0, 246, 0.5);
border-top: 2px solid red;
border-left: 4px solid red;
border-right: 2px solid red;
border-bottom: 2px solid red;
padding: 10px;
overflow: auto;
}
.inner {
height: 400px;
}
</style>
<div class="clientDOM">
<div class="inner"></div>
</div>
| 属性 | 描述 | 举例 |
|---|---|---|
| clientWidth | 元素的可见内容区域的宽度,不包括滚动条、边框、外边距,但包括内边距 | 获取内容区域的宽度(不包括滚动条和边框) |
| clientHeight | 元素的可见内容区域的高度,不包括滚动条、边框、外边距,但包括内边距 | 获取内容区域的高度(不包括滚动条和边框) |
| clientLeft | 元素左边框的宽度 | 获取左边框的宽度 |
| clientTop | 元素上边框的宽度 | 获取上边框的宽度 |
总结:client相关的属性关注的内容区域
2.2 offsetWidth、offsetHeight、offsetLeft、offsetTop
打开控制台可以看见,实际上先减去滚动条的区域,然后在计算Padding内的实际内容区域的,也就意味着如果我们本来设置的宽高一样,由于滚动条的存在,宽度实际上是被压缩的,我们需要增加宽度,或采取其他处理方式~
* {
padding: 0;
margin: 0;
}
.offsetDOM {
position: relative;
left: 20px;
top: 10px;
width: 200px;
height: 200px;
background-color: rgba(0, 0, 246, 0.5);
border: 2px solid red;
padding: 10px;
overflow: auto;
}
.inner {
height: 400px;
}
<div class="offsetDOM">
<div class="inner">inner content xxxxxxxx xxxxxxxx</div>
</div>
在日常开发中用的比较多的应该是offsetTop: 表示该元素相对于其 最近的已定位祖先元素(即具有position属性为relative, absolute, fixed或sticky的祖先元素)垂直偏移量(距离)。如果该元素没有已定位的祖先元素,则offsetTop会返回相对于文档 的顶部的距离
举个例子:
| 属性 | 描述 | 举例 |
|---|---|---|
| offsetWidth | 元素的总宽度,包含内容、内边距和边框,但不包括外边距 | 获取元素的可见宽度(包括边框,不包括外边距) |
| offsetHeight | 元素的总高度,包含内容、内边距和边框,但不包括外边距 | 获取元素的可见高度(包括边框,不包括外边距) |
| offsetLeft | 元素左边缘相对于最近定位祖先元素的左边缘的距离(包括滚动偏移) | 获取元素相对于其定位祖先元素的水平位置 |
| offsetTop | 元素上边缘相对于最近定位祖先元素的上边缘的距离(包括滚动偏移) | 获取元素相对于其定位祖先元素的垂直位置 |
2.3 scrollWidth、scrollHeight、scrollLeft、scrollTop
| 属性 | 描述 | 举例 |
|---|---|---|
| scrollWidth | 元素内容的实际宽度(包括溢出不可见部分),单位是像素 | 该元素是个滚动元素,横向被其他元素撑开的时候,实际上获取的滚动块的宽度 |
| scrollHeight | 元素内容的实际高度(包括溢出不可见部分),单位是像素 | 该元素是个滚动元素,纵向被其他元素撑开的时候,实际上获取的滚动块的长度 |
| scrollLeft | 元素左侧已被卷去的宽度,单位是像素。 | 表示当前水平滚动条滚动了多少距离 |
| scrollTop | 元素顶部已被卷去的高度,单位是像素。 | 表示当前垂直滚动条滚动了多少距离。 |
<style>
* {
padding: 0;
margin: 0;
}
.scrollDOM {
position: relative;
left: 180px;
top: 80px;
width: 200px;
height: 200px;
background-color: rgba(0, 0, 246, 0.5);
border: 2px solid red;
padding: 10px;
overflow: auto;
}
.inner {
width: 250px;
height: 400px;
}
</style>
<body>
<div class="scrollDOM">
<div class="inner">inner content xxxxxxxx xxxxxxxx</div>
</div>
<script>
const divEl = document.querySelector(".scrollDOM");
const debounceFn = (fn, delay) => {
let timer = null;
function _debounce(...args) {
if (timer) {
clearTimeout(timer)
timer = null
}
timer = setTimeout(() => {
fn(...args)
}, delay)
}
return _debounce
}
const debounceScrollFn = debounceFn(() => {
console.log("divEl.scrollWidth:", divEl.scrollWidth);
console.log("divEl.scrollHeight:", divEl.scrollHeight);
console.log("divEl.scrollTop:", divEl.scrollTop);
console.log("divEl.oscrollLeft:", divEl.scrollLeft);
}, 200)
divEl.addEventListener("scroll", debounceScrollFn);
</script>
</body>
3. getBoundingCLientRect() | 交叉观察器
接下来是两个判断元素可见性/元素交集的API
3.1 getBoundingCLientRect()
getBoundingCLientRect()是一个DOM元素的方法,返回元素的大小及其相对于视口的位置,它返回一个包含元素位置和尺寸的DOMRect对象。这个方法适用于获取元素在页面中的相对位置,尤其在滚动的时候
该DOMRect对象包含以下属性:
此外,还有width和height(包含padding和border)
- 注意
getBoundingClientRect()返回的坐标是相对于视口的,如果需要它相对于文档(即包括滚动偏移),可以加上window.scrollY或window.scrollX:
const rect = element.getBoundingClientRect();
const docTop = rect.top + window.scrollY; // 相对于文档顶部
const docLeft = rect.left + window.scrollX; // 相对于文档左侧
3.2 交叉观察器
交叉观察器利用IntersectionObserver来实现一种异步观察两个元素之间交集的方式,这里涉及到observer相关的api,后续会单独介绍~
响应式设计
在了解了各种知识之后,我们来看看响应式设计。在《深入解析CSS》中谈到,响应式设计的第一原则是移动优先,首先考虑移动端布局,移动版设计就是内容的设计。做响应式设计时,一定要确保HTML包含了各种屏幕尺寸所需的全部内容。
移动优先
响应式设计的第一原则是 移动优先(Mobile First) 。这一理念要求开发者在设计和编码时,首先考虑移动端的布局与交互,然后再通过媒体查询(Media Queries)逐步适配更大尺寸的设备。
Tips: 做响应式设计时,一定要确保HTML包含了各种屏幕尺寸所需的全部内容。你可以对每个屏幕尺寸应用不同的CSS,但是它们必须共享同一份HTML。
在完成移动端的设计之后,一个重要的细节是添加视口的meta标签,加上这个标签之后,相当于告诉移动设置已经适配了小屏设备
媒体查询
媒体查询大家都比较熟悉,这里分享几个写媒体查询的tips:
-
在媒体查询里更适合用em, em是基于浏览器默认字号的(通常是16px)
-
min-width和max-width是目前用得最广泛的媒体特征,但还有一些别的媒体特征,如下所示。
- (min-height: 20em)—匹配高度大于等于20em的视口
- (max-height: 20em)—匹配高度小于等于20em的视口
- (orientation: landscape)—匹配宽度大于高度的视口
- (orientation: portrait)—匹配高度大于宽度的视口
- 其他可以查看 @media
-
最后一个媒体查询的选项是媒体类型(media type)。常见的两种媒体类型是screen和print
- 在需要的时候才会去考虑,但还是有必要思考用户是否想要打印网页的
-
@media print { * { color: black ! important; background: none ! important; } }
-
容器查询(@container)基于容器宽度,可以做到组件级响应式,可感知所在容器尺寸,这个在特定情况下用得到
流式布局
- 流式布局是容器宽度随视口变化而自动调整的布局方式。
- 相对固定宽度布局(如
width: 800px),流式布局避免了小屏下的横向滚动。 - 常使用百分比宽度、
auto外边距、弹性盒子(Flexbox)等技术实现。
以小屏为基础设计(移动优先),用媒体查询控制不同屏宽的样式(媒体查询),让元素宽度随视口或容器变化自动适应(流式布局)——这是响应式设计的底层逻辑。
虚拟滚动
在做了前面这么多铺垫之后,让我们来看看虚拟滚动以及它的实现机制~
长列表滚动是性能优化里面一个常见的命题,这里对于长列表滚动的优化主要是分页 滚动,无限滚动以及虚拟滚动三种实现方式,这里主要讨论虚拟滚动和无限滚动两种实现方式
懒加载
利用scrollHeight和scrollTop可以先加载一些内容,等到触底(或者距离底部某一段距离再触发后续的加载)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.container {
height: 505px;
width: 50%;
background-color: rgba(0, 0, 246, 0.5);
overflow-y: scroll;
border: 1px solid black;
}
.item {
border-bottom: 1px solid black;
height: 50px;
text-align: center;
line-height: 50px;
}
</style>
</head>
<body>
<div class="container">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">...</div>
</div>
<script>
let count = 1
const container = document.querySelector('.container');
container.addEventListener("scroll", () => {
const offset = container.scrollHeight - container.scrollTop;
// console.log('offset',offset);
const delta = 50;
if (offset <= (500+delta)) {
const newDom = document.createElement('div')
newDom.setAttribute('class', 'item')
newDom.innerText = `new DOM ${count++}`
container.appendChild(newDom)
}
})
</script>
</body>
</html>
IntersectionObserver + div 占位
<script>
let count = 1
const container = document.querySelector('.container');
const items = Array.from(container.children) || [];
items.forEach(item => {
const intersectionObserver = new IntersectionObserver(function (entries, observer) {
if (!entries[0].isVisible) {
entries[0].target.backup = entries[0].target.innerHTML || entries[0].target.backup;
entries[0].target.innerHTML = '';
return;
}
entries[0].target.innerHTML =
entries[0].target.backup || entries[0].target.innerHTML;
}, {
threshold: [0, 1],
root:container,
trackVisibility: true,
delay: 100
})
intersectionObserver.observe(item);
})
</script>
通过这种方式IntersectionObserver为元素添加交互观察器,有交叉就显示内容同时缓存,没有的话就清空内容。可以监听最后一个元素的可见性来实现无限滚动,没有dom插入删除,结构相对来说较为稳定
数据截断
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.container {
height: 405px;
width: 50%;
background-color: rgba(0, 0, 246, 0.5);
overflow-y: scroll;
border: 1px solid black;
}
.item {
border-bottom: 1px solid black;
height: 50px;
text-align: center;
line-height: 50px;
}
</style>
</head>
<body>
<div class="container">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>
<div class="item">9</div>
<div class="item">10</div>
</div>
<script>
const data = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20];
const container = document.querySelector('.container');
function addList(data, container) {
data.forEach(item => {
const dom = document.createElement('div');
dom.setAttribute('class', 'item');
dom.innerHTML = item;
container.appendChild(dom)
});
}
container.addEventListener('scroll', () => {
const scrollTop = container.scrollTop;
// 上边空白的高度
const topHeight = scrollTop;
const startIndex = Math.max(Math.ceil(topHeight / 40) - 2, 0);
const endIndex = startIndex + Math.ceil(500 / 40);
const show = data.slice(startIndex, endIndex + 1);
// 计算下边剩余的隐藏区域高度
const dataHeight = data.length * 40;
const bottomHeight = dataHeight - 500 - scrollTop;
const topDom = document.createElement('div');
const bottomDom = document.createElement('div');
topDom.style.height = topHeight + 'px';
bottomDom.style.height = bottomHeight + 'px';
// 还没到底
if (bottomHeight > -100) {
// 清空
container.innerHTML = '';
container.appendChild(topDom);
addList(show, container);
container.appendChild(bottomDom);
}
});
window.onload = function() {
addList(data, container)
}
</script>
</body>
</html>
动态创建上下div块占位,渲染可视区域内的data数据截断
小结
- 算是对于一直困扰我的一些概念简答整理了一下,很多地方依然可能不是很完备,只是目前尽我的能力完善了,不足之处欢迎批评指正。
- 从五月初动笔至今,自己拖拖拉拉写了很久,期间经历了几次面试的失败,一度很失落。幸运收到了一家Offer,思来想去还是放弃打算回到实习的公司。大环境不好,当然自己能力也是不足,希望多动笔总结多实战提升自己的能力,加油啦~
参考文献
- scrollTop、clientHeight、 scrollHeight...学完真的理解了scrollTop、clie - 掘金
- 一篇文章带你彻底分清分辨率、像素、视口以及相关的易混淆概念 - 掘金
- 前端长列表滚动方案探讨,看完不要再说不知道怎么实现虚拟列表了
- 布局视口,视觉视口,理想视口-zhuanlan.zhihu.com
- Viewport concepts - CSS: Cascading Style Sheets | MDN
- Using the viewport meta element - HTML: HyperText Markup Language | MDN
- web.dev/blog/viewpo…
- 《CSS in Depth》