本文是内部的一次分享沉淀,偏向基础但是涉及了一些有意思的细节,文笔有限,才疏学浅,文中若有不正之处,万望告知。
前端的一大工作内容就是去兼容页面在不同内核的浏览器,不同的设备,不同的分辨率下的行为,使页面的能正常工作在各种各样的宿主环境当中。
而本文的主题 -- 移动端开发的兼容适配与性能优化,就是希望能从一些常见的移动端开发问题出发,厘清 Web 移动端开发的前前后后,一些技术的发展过程,一些问题的优化手段以及给出一些常见的兼容性问题的解决方案。
什么是响应式设计
首先先聊聊响应式设计,这个与移动端开发有着密切的联系。
响应式设计即是 RWD,Responsive Web Design。
这里百度或者谷歌一下会有各种各样的答案。这里一段摘自知乎上我觉得很棒的一个答案:什么是响应式布局设计?
根据维基百科及其参考文献,理论上,响应式界面能够适应不同的设备。描述响应式界面最著名的一句话就是“Content is like water”,翻译成中文便是“如果将屏幕看作容器,那么内容就像水一样”。
为什么要设计响应式界面
为什么要费神地尝试统一所有设备呢?
- 即便是PC或Mac用户,有查显示只有一半的人会将浏览器全屏显示,而剩下的一般人使用多大的浏览器,很难预知;
- 台式机、投影、电视、笔记本、手机、平板、手表、VR……智能设备正在不断增加,“主流设备”的概念正在消失;
- 屏幕分辨率正飞速发展,同一张图片在不同设备上看起来,大小可能天差地别;
- 鼠标、触屏、笔、摄像头手势……不可预期的操控方式正在不断出现。
响应式界面的四个层次
- 同一页面在不同大小和比例上看起来都应该是舒适的;
- 同一页面在不同分辨率上看起来都应该是合理;
- 同一页面在不同操作方式(如鼠标和触屏)下,体验应该是统一的;
- 同一页面在不同类型的设备(手机、平板、电脑)上,交互方式应该是符合习惯的。
响应式界面的基本规则
- 可伸缩的内容区块:内容区块的在一定程度上能够自动调整,以确保填满整个页面
可自由排布的内容区块:当页面尺寸变动较大时,能够减少/增加排布的列数
适应页面尺寸的边距:到页面尺寸发生更大变化时,区块的边距也应该变化
能够适应比例变化的图片:对于常见的宽度调整,图片在隐去两侧部分时,依旧保持美观可用
能够自动隐藏/部分显示的内容:如在电脑上显示的的大段描述文本,在手机上就只能少量显示或全部隐藏
能自动折叠的导航和菜单:展开还是收起,应该根据页面尺寸来判断
- 放弃使用像素作为尺寸单位:用dp(对于前端来说,这里可能是rem)尺寸等方法来确保页面在分辨率相差很大的设备上,看起来也能保持一致。同时也要求提供的图片应该比预想的更大,才能适应高分辨率的屏幕
上面一段我觉得已经涵盖了响应式设计的绝大部分,简单总结起来,可以概括为:
- 媒体查询,边界断点的规则设定(Media queries && break point)
- 内容的可伸缩性效果(Flexibel visuals)
- 流式网格布局(Fluid grids)
- 主要内容呈现及图片的高质量(Main content and high quality)
响应式 vs. 自适应
响应式设计是 Responsive Web Design(RWD),自适应设计是 Adaptive Web Design(AWD)。经常有人会将两者混为一谈,或者其实根本也区分不了所谓的响应式与自适应。
其实在我写这篇文章的时候,我也无法很好的去区分两者。
RWD 和 AWD 两者都是为了适配各种不同的移动设备,致力于提升用户体验所产生的的技术。核心思想是用技术来使网页适应从小到大(现在到超大)的不同分辨率的屏幕。通常认为,RWD 是 AWD 的子集。
RWD:Ethan Marcote 的文章是大家认为 RWD 的起源。他提出的 RWD 方案是通过 HTML 和 CSS 的媒体查询技术,配合流体布局实现。RWD 倾向于只改变元素的外观布局,而不大幅度改变内容。Jeffrey Zeldman 总结说,我们就把 RWD 定义为一切能用来为各种分辨率和设备性能优化视觉体验的技术。
AWD:Adaptive Design 是 Aaron Gustafson 的书的标题。他认为 AWD 在包括 RWD 的 CSS 媒体查询技术以外,也要用 Javascript 来操作 HTML 来更适应移动设备的能力。AWD 有可能会针对移动端用户减去内容,减去功能。AWD 可以在服务器端就进行优化,把优化过的内容送到终端上。
一图胜千言。
从定义上而言,RWD 是一套代码,适用于所有屏幕。而 AWD 则是多端多套代码。本文不会过多去纠结响应式与自适应区别,我觉得这两者的本质都是致力于适配不同设备,更好地提升用户体验。
Quora - Responsive Design vs. Adaptive Design?
zhihu -- Responsive design 和 Adaptive design 的区别
渐进增强 vs. 优雅降级
- 渐进增强(progressive enhancement):针对低版本浏览器进行构建页面,保证最基本的功能,然后再针对高级浏览器进行效果、交互等改进和追加功能达到更好的用户体验。
- 优雅降级(graceful degradation):一开始就构建完整的功能,然后再针对低版本浏览器进行兼容。
区别:优雅降级是从复杂的现状开始,并试图减少用户体验的供给,而渐进增强则是从一个非常基础的,能够起作用的版本开始,并不断扩充,以适应未来环境的需要。降级(功能衰减)意味着往回看;而渐进增强则意味着朝前看,同时保证其根基处于安全地带。
渐进增强/优雅降级通常是 AWD 会牵扯到的另一个技术术语。本质上而言即是随着屏幕的大小的改变,功能会一点一点增强。
也通常会用在一些高级 CSS3 属性上,我们对一些 CSS 属性进行特性检测,甚至不进行特性检测直接使用。后果是在支持它的网页上该属性正常展示,而不支持它的网页该属性不生效,但也不影响用户的基本使用。
典型的例子是 CSS3 逐渐被大众认可并被使用,PC端页面开始由 IE678 向兼容性更好的IE9+,chrome,firefox浏览器转变的时期。我们可以对页面元素直接使用阴影,圆角等属性。对于不支持它的低版本 IE 而言,没有什么损失,而对于支持它的高级浏览器而言,带给了用户更好的交互视觉体验,这就是渐进增强。
移动端屏幕适配方案
下面会针对一些具体的案例,展开讲讲。第一个是高保真还原设计稿,也就是如何适配移动端繁杂的屏幕大小。
通常而言,设计师只会给出单一分辨率下的设计稿,而我们要做的,就是以这个设计稿为基准,去适配所有不同大小的移动端设备。
在此之前,有一些基础概念需要理解。
一些概念性的东西,大部分人很难一次性记住,或者记了又忘,我觉得记忆这个东西比较看技巧,比如关联法,想象法,把这些生硬的概念与一些符合我们常识的知识关联在一起记忆,往往能够事半功倍。
设备独立像素
以 iPhone6/7/8为例,这里我们打开 Chrome 开发者工具:
这里的 375 * 667 表示的是什么呢,表示的是设备独立像素(DIP),也可以理解为 CSS 像素,也称为逻辑像素:
设备独立像素 = CSS 像素 = 逻辑像素
如何记忆呢?这里使用 CSS 像素来记忆,也就是说。我们设定一个宽度为 375px 的 div,刚好可以充满这个设备的一行,配合高度 667px ,则 div 的大小刚好可以充满整个屏幕。
物理像素
OK,那么,什么又是物理像素呢。我们到电商网站购买手机,都会看一看手机的参数,以 JD 上的 iPhone7 为例:
可以看到,iPhone7 的分辨率是 1334 x 750,这里描述的就是屏幕实际的物理像素。
物理像素,又称为设备像素。显示屏是由一个个物理像素点组成的,1334 x 750 表示手机分别在垂直和水平上所具有的像素点数。通过控制每个像素点的颜色,就可以使屏幕显示出不同的图像,屏幕从工厂出来那天起,它上面的物理像素点就固定不变了,单位为pt。
设备像素 = 物理像素
DPR(Device Pixel Ratio) 设备像素比
OK,有了上面两个概念,就可以顺理成章引出下一个概念。DPR(Device Pixel Ratio) 设备像素比,这个与我们通常说的视网膜屏(多倍屏,Retina屏)有关。
设备像素比描述的是未缩放状态下,物理像素和设备独立像素的初始比例关系。
简单的计算公式:
DPR = 物理像素 / 设备独立像素
我们套用一下上面 iPhone7 的数据(取设备的物理像素宽度与设备独立像素宽度进行计算):
iPhone7’s DPR = iPhone7’s 物理像素宽度 / iPhone7's 设备独立像素宽度 = 2
750 / 375 = 2
或者是 1334 / 667 = 2
可以得到 iPhone7 的 dpr 为 2。也就是我们常说的视网膜屏幕。
视网膜(Retina)屏幕是苹果公司"发明"的一个营销术语。 苹果公司将 dpr > 1 的屏幕称为视网膜屏幕。
在视网膜屏幕中,以 dpr = 2 为例,把 4(2x2) 个像素当 1 个像素使用,这样让屏幕看起来更精致,但是元素的大小本身却不会改变:
OK,我们再来看看 iPhone XS Max:
它的物理像素如上图是 2688 x 1242,
它的 CSS 像素是 896 x 414,很容易得出 iPhone XS Max 的 dpr 为 3。
上面三个概念(CSS像素、设备独立像素、DPR)是我觉得比较重要的,还有一些其他比较重要的概念 PPI、DPI 不影响后续的内容,可以自行去加深理解。
OK,到这里我们就完成了一个小的里程碑。我们通常说的H5手机适配也就是指的这两个维度:
- 适配不同屏幕大小,也就是适配不同屏幕下的 CSS 像素
- 适配不同像素密度,也就是适配不同屏幕下 dpr 不一致导致的一些问题
适配不同屏幕大小
适配不同屏幕大小,也就是适配不同屏幕下的 CSS 像素。最早移动端屏幕 CSS 像素适配方案是CSS媒体查询。但是无法做到高保真接近 100% 的还原。
适配不同屏幕大小其实只需要遵循一条原则,确保页面元素大小的与屏幕大小保持一定比例。也就是:按比例还原设计稿
假设我们现在拿到标注为 375*667 的大小的设计稿,其中一个元素的标注如下:
以页面宽度为基准的话,那么,
- 元素的宽度为:209/375 = 55.73%
- 元素的高度为:80/375 = 21.33%
- 元素的上左右边距依次计算...
这样,无论屏幕的 CSS 像素宽度是 320px 还是 375px 还是 414px,按照等量百分比还原出来的界面总是正确的。
然而,理想很丰满,现实很骨感。实现上述百分比方案的核心需要一个全局通用的基准单位,让所有百分比展示以它为基准,但是在 CSS 中,根据CSS Values and Units Module Level 4的定义:
百分比值总要相对于另一个量,比如长度。每个允许使用百分比值的属性,同时也要定义百分比值参照的那个量。这个量可以是相同元素的另一个属性的值,也可以是祖先元素的某个属性的值,甚至是格式化上下文的一个度量(比如包含块的宽度)。
具体来说:
- 宽度(width)、间距(maring/padding)支持百分比值,但默认的相对参考值是包含块的宽度;
- 高度(height)百分比的大小是相对其父级元素高的大小;
- 边框(border)不支持百分值;
- 边框圆角半径(border-radius)支持百分比值,但水平方向相对参考值是盒子的宽度,垂直方向相对参考值是盒子的高度;
- 文本大小(font-size)支持百分比值,但相对参考值是父元素的font-size的值;
- 盒阴影(box-shadow)和文本阴影(text-shadow)不支持百分比值;
首先,支持百分比单位的度量属性有其各自的参照基准,其次并非所有度量属性都支持百分比单位。所以我们需要另辟蹊径。
rem 适配方案
在 vw 方案出来之前,最被大众接受的就是使用 rem 进行适配的方案,因为 rem 满足上面说的,可以是一个全局性的基准单位。
rem(font size of the root element),在 CSS Values and Units Module Level 3中的定义就是, 根据网页的根元素来设置字体大小,和 em(font size of the element)的区别是,em 是根据其父元素的字体大小来设置,而 rem 是根据网页的跟元素(html)来设置字体大小。
flexible
基于此,淘宝早年推行的一套以 rem 为基准的适配方案:lib-flexible。其核心做法在于:
- 根据设备的 dpr 动态改写 <meta> 标签,设置 viewport 的缩放
- 给 <html> 元素添加 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 缩放)