通过flexible解决移动端页面适配问题

序言

移动端屏幕尺寸千奇百怪,因此H5页面的适配对每一个前端工程师来说,都是比较头疼的问题。网上有很多关于移动端适配的解决方案,但都不是很全面。flexible是阿里手淘前端团队用于解决移动端页面适配的方案,经过时间和各种项目的检验,是我认为的一个比较成熟的解决方案。

flexible是什么

lib-flexible是一个js库,用于动态计算与设置页面的缩放比例和html的font-size,实现移动端适配。

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
'use strict';

/**
* @param {Boolean} [normal = false] - 默认开启页面压缩以使页面高清;
* @param {Number} [baseFontSize = 100] - 基础fontSize, 默认100px;
* @param {Number} [fontscale = 1] - 有的业务希望能放大一定比例的字体;
*/
const win = window;
export default win.flex = (normal, baseFontSize, fontscale) => {
const _baseFontSize = baseFontSize || 100;
const _fontscale = fontscale || 1;
const doc = win.document;
const ua = navigator.userAgent;
const matches = ua.match(/Android[\S\s]+AppleWebkit\/(\d{3})/i);
const UCversion = ua.match(/U3\/((\d+|\.){5,})/i);
const isUCHd = UCversion && parseInt(UCversion[1].split('.').join(''), 10) >= 80;
const isIos = navigator.appVersion.match(/(iphone|ipad|ipod)/gi);
let dpr = win.devicePixelRatio || 1;
if (!isIos && !(matches && matches[1] > 534) && !isUCHd) {
// 如果非iOS, 非Android4.3以上, 非UC内核, 就不执行高清, dpr设为1;
dpr = 1;
}
const scale = normal ? 1 : 1 / dpr;
let metaEl = doc.querySelector('meta[name="viewport"]');
if (!metaEl) {
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
doc.head.appendChild(metaEl);
}
metaEl.setAttribute('content', `width=device-width,user-scalable=no,initial-scale=${scale},maximum-scale=${scale},minimum-scale=${scale}`);
doc.documentElement.style.fontSize = normal ? '50px' : `${_baseFontSize / 2 * dpr * _fontscale}px`;
};

源码(压缩版)

1
<script>!function(e){function t(a){if(i[a])return i[a].exports;var n=i[a]={exports:{},id:a,loaded:!1};return e[a].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var i={};return t.m=e,t.c=i,t.p="",t(0)}([function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=window;t["default"]=i.flex=function(normal,e,t){var a=e||100,n=t||1,r=i.document,o=navigator.userAgent,d=o.match(/Android[\S\s]+AppleWebkit\/(\d{3})/i),l=o.match(/U3\/((\d+|\.){5,})/i),c=l&&parseInt(l[1].split(".").join(""),10)>=80,p=navigator.appVersion.match(/(iphone|ipad|ipod)/gi),s=i.devicePixelRatio||1;p||d&&d[1]>534||c||(s=1);var u=normal?1:1/s,m=r.querySelector('meta[name="viewport"]');m||(m=r.createElement("meta"),m.setAttribute("name","viewport"),r.head.appendChild(m)),m.setAttribute("content","width=device-width,user-scalable=no,initial-scale="+u+",maximum-scale="+u+",minimum-scale="+u),r.documentElement.style.fontSize=normal?"50px": a/2*s*n+"px"},e.exports=t["default"]}]);  flex(false,100, 1);</script>

flexible是如何实现页面适配的

适配的基准:设备像素比(device pixel ratio)

这里涉及到一个很重要的概念即设备像素比,简称dpr。计算公式为设备像素比 = 物理像素 / 设备独立像素

物理像素(physical pixel)

物理像素又称设备像素,指的是设备中使用的物理像素。
比如iPhone 6的分辨率为1334x750像素。

设备独立像素(density-independent pixel)

独立于设备的用于逻辑上衡量像素的单位。这是一个总体的概念,包括了CSS像素。

在JavaScript中,可以通过window.devicePixelRatio获取到当前设备的dpr。
在CSS中,可以通过-webkit-device-pixel-ratio-webkit-min-device-pixel-ratio-webkit-max-device-pixel-ratio进行媒体查询,对不同dpr的设备,做一些样式适配(这里只针对webkit内核的浏览器和webview)。

dpr对页面显示效果的影响

对比Retina屏(dpr = 2)和普通屏(dpr = 1),css像素所呈现的大小(物理尺寸)是一致的,不同的是1个css像素所对应的物理像素个数是不一致的。

在retina屏幕下,1个css像素对应1个物理像素(1:1)。 在普通屏幕下,1个css像素对应4个物理像素(1:4)。如图:

理论上,1个css像素对应于1个物理像素,才能得到完美清晰的展示。

因此对于dpr = 2的屏幕,需要将屏幕缩放到原来的1/2才能达到最完美的显示效果,dpr = 3,则需要缩放为原来的1/3。

常见手机的dpr

iPhone 4以上都是用的是Retina屏。
其中4, 4s, 5, 5s, 6, 6s的dpr都是2,6+和6s+的dpr是3。

该方案中并未对安卓设备进行适配,默认dpr均为1。主要原因有如下几点:

  • viewport content 的写法要兼容各种奇葩 Android 设备,兼容测试的成本很高
  • 部分机型修改viewport之后产生屏幕抖动
  • 部分机型 WebView Width 与屏幕实际宽度不一致
  • 还有很多无法预料到的奇葩BUG存在

通过dpr动态设置相关属性

根据dpr设置页面缩放比例

1
2
3
...
metaEl.setAttribute('content', `width=device-width,user-scalable=no,initial-scale=${scale},maximum-scale=${scale},minimum-scale=${scale}`);
...

这里涉及到了一个东西叫viewport

一般移动设备的浏览器都默认设置了一个viewport元标签,定义一个虚拟的layout viewport(布局视口),用于解决早期的页面在手机上显示的问题。

移动设备的浏览器就是把页面放在一个虚拟的“窗口”(viewport)中,窗口可大于或小于手机的可视区域,一般手机默认viewport大于可视区域。这样不会破坏没有针对手机浏览器优化的网页的布局,用户可以通过平移和缩放来看网页的其他部分。如图:

一般移动设备浏览器的viewportwidth默认为980px,所以我们只能看见部分区域的页面,需要滚动页面才能看见其他部分。所以我们需要将width = device-width,将可视窗口(viewport)的宽度设为当前设备的宽度,然后将initial-scale = 1,保持原来的比例。就能把一个页面适应的移动设备上。

一个常见的viewport类型的meta标签:

<meta name=”viewport” content=”width=device-width, initial-scale=1, maximum-scale=1″>

根据dpr设置html的font-size

此方案默认以640px的设计稿为标准。因此dpr = 2时,htmlfont-size为100px。依此类推。

如何使用flexible

在页面head标签里引入第一节中的js代码。页面渲染之前会执行该代码,给页面设置好缩放比例和html的font-size。
然后我们根据设计稿(通常以640px为基准,1rem = 100px),通过rem设置相应元素的宽高即可。


tip:貌似flexible已经发布了2.0版本了,是基于vw方案(Github地址),下回分解吧- -。相比之下两个方案各有利弊吧,都可以解决移动端适配的大部分问题了,感谢大佬们的贡献!