移动端兼容

(一)响应式界面

1. 什么是响应式设计

响应式布局

响应式界面能够适应不同的设备。描述响应式界面最著名的一句话就是“Content is like water”,翻译成中文便是“如果将屏幕看作容器,那么内容就像水一样”。

2. 响应式界面的四个层次

  • 同一页面在不同大小和比例上看起来都应该是舒适的;
  • 同一页面在不同分辨率上看起来都应该是合理;
  • 同一页面在不同操作方式(如鼠标和触屏)下,体验应该是统一的;
  • 同一页面在不同类型的设备(手机、平板、电脑)上,交互方式应该是符合习惯的。

3. 响应式界面的基本规则

可伸缩的内容区块:内容区块在一定程度上能够自动调整,以确保填满整个页面

image-20210327123558579

适应页面尺寸的边距:到页面尺寸发生更大变化时,区块的边距也应该变化

image-20210327123935190

能够适应比例变化的图片:对于常见的宽度调整,图片在隐去两侧部分时,依旧保持美观可用

image-20210327124052768

能够自动隐藏/部分显示的内容:如在电脑上显示的的大段描述文本,在手机上就只能少量显示或全部隐藏

image-20210327124148764

能自动折叠的导航和菜单:展开还是收起,应该根据页面尺寸来判断

image-20210327124310687

⑥ 放弃使用像素作为尺寸单位:用 dp(对于前端来说,这里可能是 rem) 尺寸等方法来确保页面在分辨率相差很大的设备上,看起来也能保持一致。同时也要求提供的图片应该比预想的更大,才能适应高分辨率的屏幕

简单总结起来,可以概括为:

  • 媒体查询,边界断点的规则设定(Media queries && break point)
  • 内容的可伸缩性效果(Flexible visuals)
  • 流式网格布局 (Fluid grids)
  • 主要内容呈现及图片的高质量 (Main content and high quality)

4. 响应式与自适应

响应式设计是 Responsive Web Design(RWD),自适应设计是 Adaptive Web Design(AWD)。RWD 和 AWD 两者都是为了适配各种不同的移动设备,致力于提升用户体验所产生的的技术。核心思想是用技术来使网页适应从小到大(现在到超大)的不同分辨率的屏幕。通常认为,RWD 是 AWD 的子集

image-20210327124723911

从定义上而言,RWD 是一套代码,适用于所有屏幕。而 AWD 则是多端多套代码。两者的本质都是致力于适配不同设备,更好地提升用户体验。

(二)分辨率

1. 物理像素

在电商网站购买手机时我们都会查看手机的参数,以 京东上的 iPhone7 为例:

image-20210327135258715

可以看到,iPhone7 的分辨率是 1334 x 750,这里描述的就是屏幕实际的物理像素。

物理像素,又称为设备像素。显示屏是由一个个物理像素点组成的,1334 x 750 表示手机分别在垂直和水平上所具有的像素点数。通过控制每个像素点的颜色,就可以使屏幕显示出不同的图像,屏幕从出厂起,其物理像素点就固定不变了,单位为 pt。屏幕分辨率就是指一个屏幕具体由多少个物理像素点组成。

2. 设备独立像素

以 iPhone6/7/8为例,这里我们打开 Chrome 开发者工具:

image-20210327135542887

这里的 375*667 表示的就是设备独立像素 DIP(Device Independent Pixels)。

当我们设定一个宽度为 375px 的 div,刚好可以充满这个设备的一行,配合高度 667px,则 div 的大小刚好可以充满整个屏幕。

3. CSS 像素

在写 CSS 时用到最多的单位是 px,即 CSS 像素,当页面缩放比例为 100% 时,一个 CSS 像素等于一个设备独立像素。但是 CSS 像素是很容易被改变的,当用户对浏览器进行了放大,CSS 像素会被放大,这时一个 CSS 像素会跨越更多的物理像素。

页面的缩放系数 = CSS像素 / 设备独立像素

4. DPR

DPR(Device Pixel Ratio)设备像素比,这个与我们通常说的视网膜屏(多倍屏,Retina 屏)有关。设备像素比描述的是未缩放状态下,物理像素和设备独立像素的初始比例关系。

DPR = 物理像素宽度 / 设备独立像素宽度

套用上面 iPhone7 的数据:iPhone7’s DPR = iPhone7’s 物理像素宽度 / iPhone7’s 设备独立像素宽度 = 2

我们通常说的 H5 手机适配也就是指的这两个维度:

  • 适配不同屏幕大小,也就是适配不同屏幕下的 CSS 像素
  • 适配不同像素密度,也就是适配不同屏幕下 dpr 不一致导致的一些问题

5. PPI

PPI(Pixel Per Inch):每英寸包括的物理像素数。PPI可以用于描述屏幕的清晰度以及一张图片的质量。使用PPI描述图片时,PPI越高,图片质量越高,使用PPI描述屏幕时,PPI越高,屏幕越清晰。

6. DPI

DPI(Dot Per Inch):即每英寸包括的点数。这里的点是一个抽象的单位,它可以是屏幕像素点、图片像素点或者打印机的墨点。平时可能会看到使用DPI来描述图片和屏幕,这时的DPI应该和PPI是等价的,DPI最常用的是用于描述打印机,表示打印机每英寸可以打印的点数。

一张图片在屏幕上显示时,它的像素点数是规则排列的,每个像素点都有特定的位置和颜色。当使用打印机进行打印时,打印机可能不会规则的将这些点打印出来,而是使用一个个打印点来呈现这张图像,这些打印点之间会有一定的空隙,这就是DPI所描述的:打印点的密度。打印机的DPI越高,打印图像的精细程度就越高,同时这也会消耗更多的墨点和时间。

image-20210327143056556

(三)适配屏幕方案

适配不同屏幕大小其实只需要遵循一条原则,确保页面元素大小的与屏幕大小保持一定比例。也就是:按比例还原设计稿

1. rem 适配方案(old-fashioned)

rem(font size of the root element),在 CSS Values and Units Module Level 3中的定义就是,根据网页的根元素来设置字体大小,和 em(font size of the element)的区别是,em 是根据其父元素的字体大小来设置,而 rem 是根据网页的根元素(html)来设置字体大小。

rem 布局的本质是等比缩放,一般是基于宽度,假设我们将屏幕宽度平均分成 100 份,每一份的宽度用 x 表示,x = 屏幕宽度 / 100,如果将 x 作为单位,x 前面的数值就代表屏幕宽度的百分比。

1
p {width: 50x} /* 屏幕宽度的 50% */ 

如果想要页面元素随着屏幕宽度等比变化,我们需要上面的 x 单位,不幸的是 css 中并没有这样的单位,幸运的是通过 rem 这个桥梁,可以实现神奇的 x。如果子元素设置 rem 单位的属性,通过更改 html 元素的字体大小,就可以让子元素实际大小发生变化。

1
2
3
4
5
html {font-size: 16px}
p {width: 2rem} /* 32px*/

html {font-size: 32px}
p {width: 2rem} /*64px*/

如果让 html 元素字体的大小,恒等于屏幕宽度的 1/100,那 1rem 和 1x 就等价了。

1
2
html {fons-size: width / 100}
p {width: 50rem} /* 50rem = 50x = 屏幕宽度的50% */

如何让 html 字体大小一直等于屏幕宽度的百分之一呢?可以通过 js 来设置,一般需要在页面 dom ready、resize 和屏幕旋转中设置

1
document.documentElement.style.fontSize = document.documentElement.clientWidth / 100 + 'px'; 

flexible

基于此,淘宝早年推行的一套以 rem 为基准的适配方案:lib-flexible。其核心做法在于:

  • 根据设备的 dpr 动态改写 `` 标签,设置 viewport 的缩放
  • 给 `` 元素添加 data-dpr 属性,并且动态改写 data-dpr 的值
  • 根据 document.documentElement.clientWidth 动态修改 <html> 的 font-size,页面其他元素使用 rem 作为长度单位进行布局,从而实现页面的等比缩放

关于头两点,其实现在的 lib-flexible 库已经不这样做了,不再去缩放 Viewport,字体大小的设定也直接使用了 rem

hotcss

hotcss 不是一个库,也不是一个框架。它是一个移动端布局开发解决方案。使用 hotcss 可以让移动端布局开发更容易。本质的思想与 flexible 完全一致。

对于 rem 方案的一些总结

使用 flexible/hotcss 作为屏幕宽度适配解决方案,是存在一些问题的:

  • 动态修改 Viewport 存在一定的风险的,譬如通过 Viewport 改变了页面的缩放之后,获取到的 innerWidth/innerHeight 也会随之发生变化,如果业务逻辑有获取此类高宽进行其他计算的,可能会导致意想不到的错误;到今天,其实存在很多在 flexible 基础上演化而来的各种 rem 解决方案,有的不会对 Viewport 进行缩放处理,自行处理 1px 边框问题。
  • flexible/hotcss 都并非纯 CSS 方案,需要引入一定的 Javascript 代码
  • rem 的设计初衷并非是用于解决此类问题,用 rem 进行页面的宽度适配多少有一种 hack 的感觉
  • 存在一定的兼容性问题,对于安卓 4.4 以下版本系统不支持 viewport 缩放(当然,flexible 处理 Android 系列时,始终认为其 dpr 为 1,没有进行 viewport 缩放)

2. vw 适配方案

严格来说,使用 rem 进行页面适配其实是一种 hack 手段,rem 单位的初衷本身并不是用来进行移动端页面宽度适配的。到了今天,有了一种更好的替代方案,使用 vw 进行适配。百分比适配方案的核心需要一个全局通用的基准单位,rem 是不错,但是需要借助 Javascript 进行动态修改根元素的 font-size,而 vw/vh的出现则很好弥补 rem 需要 JS 辅助的缺点。

根据 CSS Values and Units Module Level 4:vw等于初始包含块(html 元素)宽度的 1%,也就是

  • 1vw 等于 window.innerWidth 的数值的 1%
  • 1vh 等于 window.innerHeight 的数值的 1%
  • vmin : vwvh 中的较小值
  • vmax : vwvh 中的较大值

image-20210327144751871

  • window.innerHeight:获取浏览器视觉视口高度(包括垂直滚动条)。

  • window.outerHeight:获取浏览器窗口外部的高度。表示整个浏览器窗口的高度,包括侧边栏、窗口镶边和调正窗口大小的边框。

  • window.screen.Height:获取获屏幕取理想视口高度,这个数值是固定的,设备的分辨率/设备像素比

  • window.screen.availHeight:浏览器窗口可用的高度。

  • document.documentElement.clientHeight:获取浏览器布局视口高度,包括内边距,但不包括垂直滚动条、边框和外边距。

  • document.documentElement.offsetHeight:包括内边距、滚动条、边框和外边距。

  • document.documentElement.scrollHeight:在不使用滚动条的情况下适合视口中的所有内容所需的最小宽度。测量方式与clientHeight相同:它包含元素的内边距,但不包括边框,外边距或垂直滚动条。

根据相关的测试,可以使用 vw 进行长度单位的有:

  • 容器大小适配,可以使用 vw
  • 文本大小的适配,可以使用 vw
  • 大于 1px 的边框、圆角、阴影都可以使用 vw
  • 内距和外距,可以使用 vw

vw 页面适配代码演示

vw

vw 看上去不错,但存在一些问题:

  1. 没能很好解决 1px 边框在高清屏下的显示问题,需要自行处理;
  2. 由于 vw 方案是完全的等比缩放,在完全等比还原设计稿的同时无法很好限定一个最大最小宽度值,由于 rem 方案是借助 Javascript 的,所以这一点 rem 比 vw 会更加的灵活;
  3. px转换成vw不一定能完全整除,因此有一定的像素差;
  4. 比如当容器使用vwmargin采用px时,很容易造成整体宽度超过100vw,从而影响布局效果。当然我们也是可以避免的,例如使用padding代替margin,结合calc()函数使用等等。

当然,两个方案现阶段其实都可以使用甚至一起搭配使用,更多详情可以读读:

(四)图片适配及优化

图像通常占据了网页下载资源的绝大部分。优化图像通常可以最大限度地减少从网站下载的字节数以及提高网站性能。一些通用的优化手段如下:

  • 消除多余的图像资源
  • 尽可能利用 CSS3\SVG 矢量图像替代某些光栅图像
  • 谨慎使用字体图标,使用网页字体取代在图像中进行文本编码
  • 选择正确的图片格式
  • 为不同 DPR 屏幕提供最适合的图片尺寸

首先就是上述的第二点,我们平时使用的图片大多数都属于位图(png、jpg...),位图由一个个像素点构成的,每个像素都具有特定的位置和颜色值。理论上,位图的每个像素对应在屏幕上使用一个物理像素来渲染,才能达到最佳的显示效果。而在dpr > 1的屏幕上,位图的一个像素可能由多个物理像素来渲染,然而这些物理像素点并不能被准确分配上对应位图像素的颜色,只能取近似值,所以相同的图片在dpr > 1的屏幕上就会模糊。

image-20210328120230196

尽可能利用 CSS3\SVG 矢量图像替代某些光栅图像。某些简单的几何图标,可以用 CSS3 快速实现的图形,都应该尽量避免使用光栅图像。这样能够保证它们在任何尺寸下都不会失真。其次,实在到了必须使用光栅图像的地步,也是有许多方式能保证图像在各种场景下都不失真。

1. 在不同 dpr 屏幕下使图片不失真

① 无脑多倍图(不可取)

在移动端假设我们需要一张 CSS 像素为 300 x 200 的图像,考虑到现在已经有了 dpr = 3 的设备,那么要保证图片在 dpr = 3 的设备下也正常高清展示,我们最大可能需要一张 900 x 600 的原图。这样,不管设备的 dpr 是否为 3,我们统一都使用 3 倍图。这样即使在 dpr = 1,dpr = 2 的设备上,也能非常好的展示图片。当然这样并不可取,会造成大量带宽的浪费。现代浏览器,提供了更好的方式,让我们能够根据设备 dpr 的不同,提供不同尺寸的图片。

② srcset 配合像素密度描述符

srcset 可以根据不同的 dpr 拉取对应尺寸的图片:

1
2
3
4
5
6
<div class='illustration'>
<img src='illustration-small.png'
srcset='images/illustration-small.png 1x,
images/illustration-big.png 2x'
style='max-width: 500px'/>
</div>

上面 srcset 里的 1x,2x 表示 像素密度描述符,表示

  • 当屏幕的 dpr = 1 时,使用 images/illustration-small.png 这张图
  • 当屏幕的 dpr = 2 时,使用 images/illustration-big.png 这张图

③ srcset 配合 w 宽度描述符

上面 1x,2x 的写法比较容易接受易于理解。除此之外,srcset 属性还有一个 w 宽度描述符,配合 sizes 属性一起使用,可以覆盖更多的面。

以如下代码为例:

1
2
3
4
5
6
7
<img 
sizes = "(min-width: 600px) 600px, 300px"
src = "photo.png"
srcset = "photo@1x.png 300w,
photo@2x.png 600w,
photo@3x.png 1200w,"
>

sizes = “(min-width: 600px) 600px, 300px"

如果屏幕当前的 CSS 像素宽度大于或等于 600px,则图片的 CSS 宽度为 600px,否则图片的 CSS 宽度为 300px。sizes 属性声明了在不同宽度下图片的 CSS 宽度表现。大屏幕下图片宽度为 600px,小屏幕下图片宽度为 300px。注意,这里的 sizes 属性只是声明了在不同屏幕宽度下图片的 CSS 宽度表现,而具体使图片在大于 600px 的屏幕上展示为 600px 宽度的代码需要另外由 CSS 或者 JS 实现。

srcset = “photo@1x.png 300w, photo@2x.png 600w, photo@3x.png 1200w

里面的 300w,600w,900w 叫宽度描述符。假设当前屏幕 dpr = 2,屏幕 CSS 宽度为 375px,图片 CSS 宽度为 300px,则分别用上述 3 个宽度描述符的数值除以 300,得:

  • 300 / 300 = 1
  • 600 / 300 = 2
  • 1200 / 300 = 4

上式计算得到的结果即算出的有效像素密度,换算成和 x 描述符等价的值。这里 600w 算出的 2 满足 dpr = 2 的情况,选择此张图。此方案考虑到了响应性布局的复杂性与屏幕的多样性,利用上述规则,可以一次适配 PC 端大屏幕和移动端高清屏

④ media 查询

使用media查询判断不同的设备像素比来显示不同精度的图片(只适用于背景图)

1
2
3
4
5
6
7
8
9
10
11
12
13
.avatar{
background-image: url(conardLi_1x.png);
}
@media only screen and (-webkit-min-device-pixel-ratio:2){
.avatar{
background-image: url(conardLi_2x.png);
}
}
@media only screen and (-webkit-min-device-pixel-ratio:3){
.avatar{
background-image: url(conardLi_3x.png);
}
}

⑤ image-set

使用image-set(只适用于背景图):

1
2
3
.avatar {
background-image: -webkit-image-set( "conardLi_1x.png" 1x, "conardLi_2x.png" 2x );
}

⑥ 使用 svg

SVG的全称是可缩放矢量图(Scalable Vector Graphics)。不同于位图的基于像素,SVG 则是属于对图像的形状描述,所以它本质上是文本文件,体积较小,且不管放大多少倍都不会失真。

(五)字体适配方案

浏览器有最小字体限制:

  • PC 上最小 font-size=12px
  • 手机上最小 font-size=8px

如果小于最小字体,那么字体默认就是最小字体。其次,很多早期的文章规范都建议不要使用奇数级单位来定义字体大小(如 13px,15px…),容易在一些低端设备上造成字体模糊,出现锯齿。

字体的选择展示

在字体适配上面,我们需要从性能展示效果两个维度去考虑。

完整的一个字体资源实在太大了,所以应尽可能使用用户设备上已有的字体,而不是额外下载字体资源,从而使加载时间明显加快。而从展示效果层面来说,使用系统字体能更好与当前操作系统使用的相匹配,得到最佳的展示效果。所以我们在字体使用方面,有一个应该尽量去遵循的原则,也是现在大部分网站在字体适配上使用的策略:使用各个支持平台上的默认系统字体

兼顾各个操作系统

常见的操作系统有 Windows、Windows Phone、Mac OS X、iPhone、Android Phone、Linux。当然对于普通用户而言,无须关注 Linux 系统。

下面就以 CSS-Trick 网站最新的 font-family 为例,看看他们是如何在字体选择上做到适配各个操作系统的

1
2
3
4
5
6
{
font-family:
system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,
Helvetica,Arial,
sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol;
}

font-family 关键字

对于 CSS 中的 font-family 而言,它有两类取值。

  1. 一类是类似这样的具体的字体族名定义:font-family: Arial 这里定义了一个具体的字体样式,字体族名为 Arial;
  2. 一类是通用字体族名,它是一种备选机制,用于在指定的字体不可用时给出较好的字体,类似这样:font-family: sans-serif

其中,sans-serif 表无衬线字体族,例如, “Open Sans”, “Arial” “微软雅黑” 等等。

关于通用字体族名,在 CSS Fonts Module Level 3 – Basic Font Properties 中,定义了 5 个,也就是我们熟知的几个通用字体族名:

  1. serif 衬线字体族
  2. sans-serif 非衬线字体族
  3. monospace 等宽字体,即字体中每个字宽度相同
  4. cursive 草书字体
  5. fantasy 主要是那些具有特殊艺术效果的字体

新增通用字体族关键字

而在 CSS Fonts Module Level 4 – Generic font families 中,新增了几个关键字:

  • system-ui 系统默认字体
  • emoji 用于兼容 emoji 表情符号字符
  • math 适用于数学表达式
  • fangsong 此字体系列用于中文的(仿宋)字体。

我们看看用的最多的 system-ui

system-ui

简单而言,font-family: system-ui 的目的就是在不同的操作系统的 Web 页面下,自动选择本操作系统下的默认系统字体。

默认使用特定操作系统的系统字体可以提高性能,因为浏览器或者 webview 不必去下载任何字体文件,而是使用已有的字体文件。 font-family: system-ui 字体设置的优势之处在于它与当前操作系统使用的字体相匹配,对于文本内容而言,它可以得到最恰当的展示。

(六)前端布局的兼容适配

前端工程师的一大工作内容就是页面布局。无论在 PC 端还是移动端,页面布局的兼容适配都是重中之重。在整个前端发展的历程中,布局的方法也在不断的推陈出新。

布局发展历程

简单来说,前端的布局发展历程经历了下面几个过程:

表格布局 –> 定位布局 –> 浮动布局 –> flexbox 布局 –> grid box 布局

每一种布局在特定时期都发挥了重要的作用,而每一种新的布局方式的出现,往往都是因为现有的布局方式已经在该时期已经无法很好的满足开发者的需求,无法满足越来越潮流的页面布局的方式。

以 Flexbox 的出现为例子,在 Flexbox 被大家广为接受使用之前。我们一直在使用定位 + 浮动的布局方式。像下面这个布局:

image-20210327163627097

容器宽度不定,内部三个元素,均分排列且占满整个空间,并且垂直居中。如果使用定位 + 浮动的布局方式,你无法很快想到最佳的解决方式。三个元素并排那么必然需要浮动或者绝对定位,容器宽度不定且中间元素始终居中,需要顾虑的方面就很多了。也许使用 text-align: justify 可以 hack 实现,等等等等。

然而,使用 flexbox 布局的话,只需要:

1
2
3
4
5
.container {
display: flex;
justify-content: space-between;
align-items: center;
}

flexbox 的出现,一次性解决了流动布局,弹性布局,排列方式等多个问题。并且它是简洁的,可控的。

再来看一个例子,水平垂直居中一个元素。使用 flexbox 也许是最便捷的:

1
2
3
4
5
6
7
.container {
display: flex;
}

.item {
margin: auto;
}

最便捷的垂直居中方式

CSS Grid Layout

OK,flexbox 已经足够优秀了,为什么 gird 网格布局的出现又是为什么?它解决了什么 flex 布局无法很好解决的问题?

看看下面这张图:

css grid layout

flexbox 是一维布局,他只能在一条直线上放置你的内容区块;而 grid 是一个二维布局。它除了可以灵活的控制水平方向之外,还能轻易的控制垂直方向的布局模式。对于上图那样的九宫格布局,它就可以轻而易举的完成。

一图以蔽之,flexbox:

flexbox

grid box:

gridbox

在现阶段,移动端布局应当更多使用 flexbox 去完成(相对那些还在使用 float 布局的),而考虑到未来页面布局的推陈出新。对于 Grid 布局我们应当像前几年对待 flexbox 一样,重视起来,随着兼容性的普及,Grid 布局也会慢慢成为主流。

(七)视口

视口 (viewport) 代表当前可见的计算机图形区域。在Web浏览器术语中,通常与浏览器窗口相同,但不包括浏览器的UI,菜单栏等,即你正在浏览的文档的那一部分。一般我们所说的视口共包括三种:布局视口、视觉视口和理想视口,它们在屏幕适配中起着非常重要的作用。

1. 布局视口

首先,移动设备上的浏览器认为自己必须能让所有的网站都正常显示,即使是那些不是为移动设备设计的网站。但如果以浏览器的可视区域作为 viewport 的话,因为移动设备的屏幕都不是很宽,所以那些为桌面浏览器设计的网站放到移动设备上显示时,必然会因为移动设备的 viewport 太窄,而挤作一团,甚至布局什么的都会乱掉。也许有人会问,现在不是有很多手机分辨率都非常大吗,比如 768×1024,或者 1080×1920 这样,那这样的手机用来显示为桌面浏览器设计的网站是没问题的吧?前面我们已经说了,css 中的 1px 并不是代表屏幕上的 1px,你分辨率越大,css 中 1px 代表的物理像素就会越多,device Pixel Ratio 的值也越大,这很好理解,因为你分辨率增大了,但屏幕尺寸并没有变大多少,必须让 css 中的 1px 代表更多的物理像素,才能让 1px 的东西在屏幕上的大小与那些低分辨率的设备差不多,不然就会因为太小而看不清。所以在 1080x1920 这样的设备上,在默认情况下,也许你只要把一个 div 的宽度设为 300 多 px(视 device Pixel Ratio 的值而定),就是满屏的宽度了。回到正题上来,如果把移动设备上浏览器的可视区域设为 viewport 的话,某些网站就会因为 viewport 太窄而显示错乱,所以这些浏览器就决定默认情况下把 viewport 设为一个较宽的值,比如 980px,这样的话即使是那些为桌面设计的网站也能在移动浏览器上正常显示了。这个浏览器默认的 viewport 叫做 layout viewport(布局视口)。

image-20210327213112704

当我们以百分比来指定元素的大小时,它的值是由这个元素的包含块计算而来的。当这个元素是最顶级的元素时,它就是基于布局视口来计算的。所以,布局视口是网页布局的基准窗口,在PC浏览器上,布局视口就等于当前浏览器的窗口大小(不包括bordersmargins、滚动条)。在移动端,布局视口被赋予一个默认值,大部分为980px,这保证PC的网页可以在手机浏览器上呈现,但是非常小,用户可以手动对网页进行放大。

我们可以通过调用document.documentElement.clientWidth / clientHeight来获取布局视口大小。

2. 视觉视口

然而,layout viewport 的宽度是大于浏览器可视区域的宽度的,所以我们还需要一个 viewport 来代表浏览器可视区域的大小,这个 viewport 叫做 **visual viewport(视觉视口)**,即用户通过屏幕真实看到的区域,默认等于当前浏览器的窗口大小(包括滚动条宽度)。visual viewport 的宽度指的是浏览器可视区域的宽度。

image-20210327213406398

用户正在看到的网页的区域。用户可以通过缩放来查看网站的内容。如果用户缩小网站,我们看到的网站区域将变大,此时视觉视口也变大了,同理,用户放大网站,我们能看到的网站区域将缩小,此时视觉视口也变小了。不管用户如何缩放,都不会影响到布局视口的宽度。

我们可以通过调用window.innerWidth / innerHeight来获取视觉视口大小。

3. 理想视口

现在我们已经有两个 viewport 了:layout viewport 和 visual viewport。但浏览器觉得还不够,因为现在越来越多的网站都会为移动设备进行单独的设计,所以必须还要有一个能完美适配移动设备的 viewport。所谓的完美适配指的是,首先不需要用户缩放和横向滚动条就能正常的查看网站的所有内容;第二,显示的文字的大小合适,比如一段 14px 大小的文字,不会因为在一个高密度像素的屏幕里显示得太小而无法看清,理想的情况是这段 14px 的文字无论是在何种密度屏幕,何种分辨率下,显示出来的大小都是差不多的。当然,不只是文字,其他元素像图片什么的也是这个道理。viewport 叫做 ideal viewport(理想视口)**,移动设备的理想 viewport,ideal viewport 可通过window. screen. width**获取。

ideal viewport 并没有一个固定的尺寸,不同的设备拥有有不同的 ideal viewport。所有的 iphone 的 ideal viewport 宽度都是 320px,无论它的屏幕宽度是 320 还是 640,也就是说,在 iphone 中,css 中的 320px 就代表 iphone 屏幕的宽度。

但是安卓设备就比较复杂了,有 320px 的,有 360px 的,有 384px 的等等,关于不同的设备 ideal viewport 的宽度都为多少,可以到 http://viewportsizes.com 去查看一下,里面收集了众多设备的理想宽度。

移动设备上的 viewport 分为 layout viewport、visual viewport 和 ideal viewport 三类,其中的 ideal viewport 是最适合移动设备的 viewport,ideal viewport 的宽度等于移动设备的屏幕宽度,只要在 css 中把某一元素的宽度设为 ideal viewport 的宽度 (单位用 px),那么这个元素的宽度就是设备屏幕的宽度了,也就是宽度为 100% 的效果。ideal viewport 的意义在于,无论在何种分辨率的屏幕下,那些针对 ideal viewport 而设计的网站,不需要用户手动缩放,也不需要出现横向滚动条,都可以完美地呈现给用户。

1、在桌面浏览器上,浏览器窗口与视口的宽度一致,而视口(CSS 标准文档中称为“初始包含块”)是 CSS 百分比宽度推算的根源,因此,浏览器窗口是约束 CSS 布局的视口;
2、在手机上,有两个视口,布局视口会限制 CSS 布局;视觉视口决定用户看到的网站内容;
3、移动端浏览器还有个理想视口,它是对特定设备上的特定浏览器的布局视口的一个理想尺寸;
4、可以把布局视口尺寸定义为理想视口。这也是响应式设计的基础。

4. Meta Viewport

meta 用来表示不能由其它 HTML 元相关元素之一表示的任何元数据信息,它可以高速浏览器如何解析页面。我们可以借助

元素的 viewport 来帮助我们设置视口、缩放等,从而让移动端得到更好的展示效果。

1
<meta name="viewport" content="width=device-width; initial-scale=1; maximum-scale=1; minimum-scale=1; user-scalable=no;">

上面是viewport的一个配置,我们来看看它们的具体含义:

Value可能值描述
width正整数或device-widthpixels(像素)为单位,定义布局视口的宽度。
height正整数或device-heightpixels(像素)为单位,定义布局视口的高度。
initial-scale0.0 - 10.0定义页面初始缩放比率。
minimum-scale0.0 - 10.0定义缩放的最小值;必须小于或等于maximum-scale的值。
maximum-scale0.0 - 10.0定义缩放的最大值;必须大于或等于minimum-scale的值。
user-scalable一个布尔值(yes或者no如果设置为 no,用户将不能放大或缩小网页。默认值为 yes。

首先,如果不设置 meta viewport 标签,那么移动设备上浏览器默认的宽度值为 800px,980px,1024px 等这些,总之是大于屏幕宽度的。这里的宽度所用的单位 px 都是指 CSS 像素,而不是物理像素。

第二、每个移动设备浏览器中都有一个理想的宽度,这个理想的宽度是指 css 中的宽度,跟设备的物理宽度没有关系,在 css 中,这个宽度就相当于 100% 的所代表的那个宽度。我们可以用 meta 标签把 viewport 的宽度设为那个理想的宽度,如果不知道这个设备的理想宽度是多少,那么用 device-width 这个特殊值就行了,同时 initial-scale=1 也有把 viewport 的宽度设为理想宽度的作用。所以,我们可以使用

1
<meta name="viewport" content="width=device-width, initial-scale=1">

来得到一个理想的 viewport(也就是前面说的 ideal viewport)。

为什么需要有理想的 viewport 呢?比如一个分辨率为 320x480 的手机理想 viewport 的宽度是 320px,而另一个屏幕尺寸相同但分辨率为 640x960 的手机的理想 viewport 宽度也是为 320px,那为什么分辨率大的这个手机的理想宽度要跟分辨率小的那个手机的理想宽度一样呢?这是因为,只有这样才能保证同样的网站在不同分辨率的设备上看起来都是一样或差不多的。实际上,现在市面上虽然有那么多不同种类不同品牌不同分辨率的手机,但它们的理想 viewport 宽度归纳起来无非也就 320、360、384、400 等几种,都是非常接近的,理想宽度的相近也就意味着我们针对某个设备的理想 viewport 而做出的网站,在其他设备上的表现也不会相差非常多甚至是表现一样的。

5. 移动端适配

为了在移动端让页面获得更好的显示效果,我们必须让布局视口、视觉视口都尽可能等于理想视口。device-width就等于理想视口的宽度,所以设置width=device-width就相当于让布局视口等于理想视口。

缩放是相对于 ideal viewport 来缩放的,缩放值越大,当前 viewport 的宽度就会越小,反之亦然。例如在 iphone 中,ideal viewport 的宽度是 320px,如果我们设置 initial-scale=2,此时 viewport 的宽度会变为只有 160px 了,这也好理解,放大了一倍嘛,就是原来 1px 的东西变成 2px 了,但是 1px 变为 2px 并不是把原来的 320px 变为 640px 了,而是在实际宽度不变的情况下,1px 变得跟原来的 2px 的长度一样了,所以放大 2 倍后原来需要 320px 才能填满的宽度现在只需要 160px 就做到了。因此initial-scale = 理想视口宽度 / 视觉视口宽度,所以我们设置initial-scale=1;就相当于让视觉视口等于理想视口。这时,1 个CSS像素就等于 1 个设备独立像素,而且我们也是基于理想视口来进行布局的,所以呈现出来的页面布局在各种设备上都能大致相似。

值得注意的是,安卓自带的 webkit 浏览器只有在 initial-scale = 1 以及没有设置 width 属性时才是表现正常的,也就相当于这理论在它身上基本没用;而 IE 则根本不甩 initial-scale 这个属性,无论你给他设置什么,initial-scale 表现出来的效果永远是 1。

再来说下 initial-scale 的默认值问题,就是不写这个属性的时候,它的默认值会是多少呢?很显然不会是 1,因为当 initial-scale = 1 时,当前的 layout viewport 宽度会被设为 ideal viewport 的宽度,但前面说了,各浏览器默认的 layout viewport 宽度一般都是 980 啊,1024 啊,800 啊等等这些个值,没有一开始就是 ideal viewport 的宽度的,所以 initial-scale 的默认值肯定不是 1。安卓设备上的 initial-scale 默认值好像没有方法能够得到,或者就是干脆它就没有默认值,一定要你显示的写出来这个东西才会起作用,我们不管它了,这里我们重点说一下 iphone 和 ipad 上的 initial-scale 默认值。

根据测试,我们可以在 iphone 和 ipad 上得到一个结论,就是无论你给 layout viewport 设置的宽度是多少,而又没有指定初始的缩放值的话,那么 iphone 和 ipad 会自动计算 initial-scale 这个值,以保证当前 layout viewport 的宽度在缩放后就是浏览器可视区域的宽度,也就是说不会出现横向滚动条。比如说,在 iphone 上,我们不设置任何的 viewport meta 标签,此时 layout viewport 的宽度为 980px,但我们可以看到浏览器并没有出现横向滚动条,浏览器默认的把页面缩小了。根据上面的公式,当前缩放值 = ideal viewport 宽度 / visual viewport 宽度,我们可以得出:当前缩放值 = 320 / 980
也就是当前的 initial-scale 默认值应该是 0.33 这样子。当你指定了 initial-scale 的值后,这个默认值就不起作用了。

总之记住这个结论就行了:在 iphone 和 ipad 上,无论你给 viewport 设的宽度是多少,如果没有指定默认的缩放值,则 iphone 和 ipad 会自动计算这个缩放值,以达到当前页面不会出现横向滚动条 (或者说 viewport 的宽度就是屏幕的宽度) 的目的。

6. 缩放

上面提到width可以决定布局视口的宽度,实际上它并不是布局视口的唯一决定性因素,设置initial-scale也有可能影响到布局视口,因为布局视口宽度取的是width和视觉视口宽度的最大值。

例如:若手机的理想视口宽度为400px,设置width=device-widthinitial-scale=2,此时视觉视口宽度 = 理想视口宽度 / initial-scale200px,布局视口取两者最大值即device-width 400px

若设置width=device-widthinitial-scale=0.5,此时视觉视口宽度 = 理想视口宽度 / initial-scale800px,布局视口取两者最大值即800px

(八)1px 问题

为了适配各种屏幕,我们写代码时一般使用设备独立像素来对页面进行布局。而在设备像素比大于1的屏幕上,我们写的1px实际上是被多个物理像素渲染,这就会出现1px在有些屏幕上看起来很粗的现象。

1. border-image

1
2
3
4
5
6
7
8
9
10
.border_1px{
border-bottom: 1px solid #000;
}
@media only screen and (-webkit-min-device-pixel-ratio:2){
.border_1px{
border-bottom: none;
border-width: 0 0 1px 0;
border-image: url(../img/1pxline.png) 0 0 2 0 stretch;
}
}

2. background-image

border-image类似,准备一张符合条件的边框背景图,模拟在背景上。

1
2
3
4
5
6
7
8
9
.border_1px{
border-bottom: 1px solid #000;
}
@media only screen and (-webkit-min-device-pixel-ratio:2){
.border_1px{
background: url(../img/1pxline.png) repeat-x left bottom;
background-size: 100% 1px;
}
}

上面两种都需要单独准备图片,而且圆角不是很好处理,但是可以应对大部分场景。

3. 伪元素+transform

基于media查询判断不同的设备像素比对线条进行缩放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.border_1px::before{
content: '';
position: absolute;
top: 0;
height: 1px;
width: 100%;
background-color: #000;
transform-origin: 50% 0%;
}
@media only screen and (-webkit-min-device-pixel-ratio:2){
.border_1px::before{
transform: scaleY(0.5);
}
}
@media only screen and (-webkit-min-device-pixel-ratio:3){
.border_1px::before{
transform: scaleY(0.33);
}
}

这种方式可以满足各种场景,如果需要满足圆角,只需要给伪类也加上border-radius即可。

4. svg

上面我们border-imagebackground-image都可以模拟1px边框,但是使用的都是位图,还需要外部引入。

借助PostCSSpostcss-write-svg我们能直接使用border-imagebackground-image创建svg1px边框:

1
2
3
4
5
6
7
8
9
@svg border_1px { 
height: 2px;
@rect {
fill: var(--color, black);
width: 100%;
height: 50%;
}
}
.example { border: 1px solid transparent; border-image: svg(border_1px param(--color #00b1ff)) 2 2 stretch; }

编译后:

1
.example { border: 1px solid transparent; border-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='2px'%3E%3Crect fill='%2300b1ff' width='100%25' height='50%25'/%3E%3C/svg%3E") 2 2 stretch; }

该方法基本可以满足所有场景,而且不需要外部引入,这是比较推荐的一种方案。

5. 设置 viewport

通过设置缩放,让CSS像素等于真正的物理像素。

例如:当设备像素比为3时,我们将页面缩放1/3倍,这时1px等于一个真正的屏幕像素。

1
2
3
4
5
6
7
8
const scale = 1 / window.devicePixelRatio;
const viewport = document.querySelector('meta[name="viewport"]');
if (!viewport) {
viewport = document.createElement('meta');
viewport.setAttribute('name', 'viewport');
window.document.head.appendChild(viewport);
}
viewport.setAttribute('content', 'width=device-width,user-scalable=no,initial-scale=' + scale + ',maximum-scale=' + scale + ',minimum-scale=' + scale);

实际上,上面这种方案是早先flexible采用的方案。

当然,这样做是要付出代价的,这意味着你页面上所有的布局都要按照物理像素来写。这显然是不现实的,这时,我们可以借助flexiblevw、vh来帮助我们进行适配。

(九)适配 iphone X

iPhoneX的出现将手机的颜值带上了一个新的高度,它取消了物理按键,改成了底部的小黑条,但是这样的改动给开发者适配移动端又增加了难度。

1. 安全区域

iPhoneX发布后,许多厂商相继推出了具有边缘屏幕的手机。

img

这些手机和普通手机在外观上无外乎做了三个改动:圆角(corners)、刘海(sensor housing)和小黑条(Home Indicator)。为了适配这些手机,安全区域这个概念变诞生了:安全区域就是一个不受上面三个效果的可视窗口范围。

为了保证页面的显示效果,我们必须把页面限制在安全范围内,但是不影响整体效果。

2. viewport-fit

viewport-fit是专门为了适配iPhoneX而诞生的一个属性,它用于限制网页如何在安全区域内进行展示。

img
  • contain: 可视窗口完全包含网页内容
  • cover:网页内容完全覆盖可视窗口

默认情况下或者设置为autocontain效果相同。

3. env, constant

img

我们需要将顶部和底部合理的摆放在安全区域内,iOS11新增了两个CSS函数env、constant,用于设定安全区域与边界的距离。

函数内部可以是四个常量:

  • safe-area-inset-left:安全区域距离左边边界距离
  • safe-area-inset-right:安全区域距离右边边界距离
  • safe-area-inset-top:安全区域距离顶部边界距离
  • safe-area-inset-bottom:安全区域距离底部边界距离

注意:我们必须指定viweport-fit后才能使用这两个函数:

1
<meta name="viewport" content="viewport-fit=cover">

constantiOS < 11.2的版本中生效,enviOS >= 11.2的版本中生效,这意味着我们往往要同时设置他们,将页面限制在安全区域内:

1
2
3
4
body {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}

当使用底部固定导航栏时,我们要为他们设置padding值:

1
2
3
4
{
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}

(十)横屏适配

image-20210328113924163

很多视口我们要对横屏和竖屏显示不同的布局,所以我们需要检测在不同的场景下给定不同的样式:

1. JavaScript 检测横屏

window.orientation:获取屏幕旋转方向

1
2
3
4
5
6
7
8
9
10
window.addEventListener("resize", ()=>{
if (window.orientation === 180 || window.orientation === 0) {
// 正常方向或屏幕旋转 180 度
console.log('竖屏');
};
if (window.orientation === 90 || window.orientation === -90 ){
// 屏幕顺时钟旋转 90 度或屏幕逆时针旋转 90 度
console.log('横屏');
}
});

2. CSS 检测横屏

1
2
3
4
5
6
@media screen and (orientation: portrait) {
/*竖屏...*/
}
@media screen and (orientation: landscape) {
/*横屏...*/
}