移动端H5页面rem缩放方案flexible.js兼容375px方案的思路

时间:2024-03-15 19:15:56

参考:

移动端高清、多屏适配方案

viewport-and-flexible.js

flexible.js github

一个新的项目复用了一些老页面,老页面是使用375px方案进行移动端适配的,meta[viewport]使用的是<meta name="viewport" content="width=375, user-scalabe=no">,而新页面使用的是flexible.js伸缩方案,动态生成meta[viewport]<meta name="viewport" content="initial-scale=[num], user-scalabe=no">
如何在老页面使用px布局的前提下,新页面使用rem布局,组件也使用rem布局,并且组件可以兼容老页面和新页面是本文的结果。
首先会介绍375px方案和rem方案的实现原理。

375px方案

<meta name="viewport" content="width=375, user-scalabe=no">

        375px方案的页面开发过程对新人非常的友好,利用页面的布局视口(layout viewport)为固定值375px,和移动端浏览器窗口的自动缩放功能(视觉视口==布局视口),可以很好的在大部分移动设备上展示375px宽度的内容。

        具体的开发前提是设计师给到一份750px宽的设计稿,前端同学根据ps量的像素的50%进行css书写。若一个banner宽度量的为750px,在css中编写为width: 375px。至于为什么是2倍的设计稿,可以参考移动端高清、多屏适配方案这篇文章,可以找到答案。

        375px方案相对于把meta[viewport]中的width属性设置成device-width,然后通过媒体查询写几套css规则来说已经是非常方便了。把所有不同屏幕尺寸的手机的布局视口(layout viewport)设置成一个固定的值375px,无需考虑其他屏幕尺寸的情况。

        但375px方案的实现原理在某些安卓原生浏览器上有兼容问题,会产生一个重要bug --- 在某些安卓原生浏览器或webview中会出现视觉视口小于布局视口的情况。直观的显示就是页面会出现左右滑动。如下图:

移动端H5页面rem缩放方案flexible.js兼容375px方案的思路

而在上文也提到了375px方案得以实现就是依靠浏览器的原生能力 --- 迫使视觉视口等于布局视口。我们将这种情况下的document.documentElement.clientWidth(布局视口)window.innerWidth(视觉视口)打印看看。

移动端H5页面rem缩放方案flexible.js兼容375px方案的思路
移动端H5页面rem缩放方案flexible.js兼容375px方案的思路

浏览器的缩放效果没有实现,至于为什么,先看两条关于缩放的总结公式/经验。

一、meta标签内没有设置initial-scale的情况

浏览器计算出的缩放值 = layout viewport width(布局视口) / ideal viewport width(理想视口)

visual viewport width(视觉视口) = 浏览器计算出的缩放值 * ideal viewport width (理想视口)
===》
layout viewport width === visual viewport width // true

经过上述计算会将视觉视口会等于布局视口,布局上的所有内容都会出现在手机屏幕上。出现之前提到的bug的问题出在计算视觉视口上,浏览器会将所有计算出的缩放值都默认等于1,所以不管我们将布局视口设置能375还是其他任意值,视觉视口永远会是1 * ideal viewport width (理想视口)。ps:此款安卓机型的理想视口等于360.

二、meta标签内设置了initial-scale的值的情况

visual viewport width(视觉视口) = initial-scale(meta 标签内设置的初始缩放值) * ideal viewport width(理想视口又称设备独立像素)

layout viewport width = visual viewport width

解释:第二条总结经验正是rem伸缩方案flexiblejs的核心思想,设置了initial-scale后浏览器会计算出视觉视口,继而将布局视口的值自动设置成视觉视口的值。达到在屏幕上完整呈现布局上的内容。

但是在同样的安卓原生浏览器上,不管我们将initial-scale设置成多少,浏览器都默认值为1。所以视觉视口和布局视口永远都等于1 * ideal viewport width这个问题的hack办法在flexible.js里也有所体现。

375px方案就解释到这里,至于为何是375而不是其他的值比如360、320等,可以参考移动端高清、多屏适配方案以及viewport-and-flexible.js, 在后篇文章中也有介绍3种视口的一些概念。

rem方案

rem方案的目标也是用一套相同的度量标准适配所有屏幕大小的移动设备,在不同屏下进行正确的缩放。假设10rem宽在iphone5上是屏幕宽的一半,那么10rem在iphone6、iphone6plus、三星note等等机型上都显示为屏幕宽的一半。

我们知道rem所对应的px值是基于html标签上的font-size值进行换算的。若

html {
    font-size: 20px;
}

10rem === 200px //true

为了适配缩放所有设备,就要写个脚本动态设置html的fontSize值。同时要对页面的布局视口(layout viewport)和设计稿做一个划分约定,这里就约定这个值为10。(理论上可以任何值)

移动端H5页面rem缩放方案flexible.js兼容375px方案的思路

移动端H5页面rem缩放方案flexible.js兼容375px方案的思路

由以上两幅图可以知道,设计稿的一个区块对应1rem,布局视口的一个区块也对应1rem。而每个机型的布局视口该如何确定,flexible.js利用了上面提到的公式:

visual viewport width(视觉视口) = initial-scale(meta 标签内设置的初始缩放值) * ideal viewport width(理想视口又称设备独立像素)

浏览器自动将 layout viewport width = visual viewport width

之前也提到了initial-scale不为1的情况下部分安卓机型有bug,所以这里可以将initial-scale规定设置成1。

拿iphone6为例:理想视口为375px,经计算布局视口和视觉视口都等于375,html的fontSize等于37.5px。若设计稿量的是10个区块大小,那么在编写css时就写10rem,对应的width等于37。5 * 10 = 375px正好布满整个布局视口。

在flexible.js中对iphone设备的initial-scale值进行动态设置。

var isAndroid = win.navigator.appVersion.match(/android/gi);
var isIPhone = win.navigator.appVersion.match(/iphone/gi);
var devicePixelRatio = win.devicePixelRatio;
if (isIPhone) {
    // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
    if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {                
        dpr = 3;
    } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
        dpr = 2;
    } else {
        dpr = 1;
    }
} else {
    // 其他设备下,仍旧使用1倍的方案
    dpr = 1;
}
scale = 1 / dpr; // initial-scale

此处将initial-scale根据dpr动态设置是为了解决retina屏下border: 1px问题。而将安卓设备的dpr全部设置成1就是hack了之前提到的initial-scale在部分安卓机子上只能为1的bug。

回到border: 1px问题,有些设计师在750px的设计稿中设计了一条1px的边框线,而这个1px的边框线在各机型上最好的效果实际是占设备物理像素的1px。

拿iphone5和初代iphone为例:若设置initial-scale=1,那么布局视口为320px,但是iphone5的物理像素宽为640px,那就代表了1个css像素包含了4个物理像素(2x2)。而初代iphone的布局视口和物理像素都为320px如图:

移动端H5页面rem缩放方案flexible.js兼容375px方案的思路

在两者手机上显示的效果1px是一模一样的,但是iphone5包含着4倍的物理像素,就取上下高度而言,在iphone5上只用编写border: 0.5px就可以了。

但是部分机型对于0.5是不识别的,会直接赋值0。而我们想要做的就是令border的宽等于1个物理像素。我们可以这样做,根据window.devicePixelRatio属性动态缩放layout viewport,使之与屏幕的物理像素相同。这样我们只用在css里编写border: 1px,对应的就是物理像素的1px,border:1问题完美解决。

两种方案如何兼容

首先要考虑到组件也用rem进行布局,并且组件要在px布局和rem布局中都能兼容。那么就要全局head引入flexible.js的js脚本。

在375px布局方案里meta[viewport]已经设置,那么在flexible脚本中就要进行判断,已经设置viewport的就沿用,不动态创建meta[viewport]。

组件的rem布局要依赖html标签的font-size值,在新页面rem布局中已经实现。而在375px布局中,flexiblejs根据固定layout viewport值-- 375进行计算,那么所有屏幕尺寸下的页面html标签font-size值都为37.5px

还有一个问题,在375px布局中,全局css环境中设置了

html, body {
    font-size: 100%
}

而所有的浏览器实现默认字体大小为16px,所以在老页面中有些字没有设置大小,默认是16px,引入了flexible后html上的font-size为37.5px,body标签上的字体大小就会变成37.5 * 100% = 37.5px,而没有设置字体大小的字体就会变成37.5px,需要在flexible.js中设置针对这种情况。

if (doc.readyState === 'complete') {
        doc.body.style.fontSize = 16 + 'px';
    } else {
        doc.addEventListener('DOMContentLoaded', function(e) {
            doc.body.style.fontSize = 16 + 'px';
        }, false);
    }

最后的结果就是:

  • 全局引入flexible.js文件。

  • 375px布局的老页面上html标签的font-size固定为37.5px。body上的font-size固定为16px。

  • rem布局的新页面上html标签的font-size随不同机型而不同。

  • 组件编写一律按照rem布局,设计稿为750px,兼容新老页面。

对我以上的理解有疑问和意见的欢迎找我私聊~微博-写前端的暹罗