fengshi123/blog

移动端适配方案指南

fengshi123 opened this issue · 0 comments

在移动端虽然整体来说大部分浏览器内核都是 webkit,而且大部分都支持 css3 的所有语法。但是,由于手机屏幕尺寸不一样,分辨率不一样,或者你需要考虑横竖屏的问题,这时候你也就不得不解决在不同手机上,不同情况下的展示效果,所以就需要一个开箱即用并且行之有效的移动端适配方案。

一、基本知识点

工欲善其事必先利其器,在具体介绍适配方案前,在本章我们会学习下适配相关的知识点,便于后续适配方案的直接上手接收。如果你对 像素 以及 viewport 的概念还不是很熟悉,可以查看前置文章《移动端响应式设计》

二、方案选择

在前端滚滚潮流的历史发展中的不同时期分别出现了一些极具代表性的适配方案,以下分别进行简单介绍。

2.1、使用 css 的媒体查询 @media

基于 css 的媒体查询属性 @media 分别为不同屏幕尺寸的移动设备编写不同尺寸的 css 属性,示例如下所示。虽然此方法能在一定程度上解决移动设备适配的问题,但我们也可以看出其存在以下问题,所以其已几乎被历史潮流淘汰。

  • 页面上所有的元素都得在不同的 @media 中定义一遍不同的尺寸,这个代价有点高;
  • 如果再多一种屏幕尺寸,就得多写一个 @media 查询块;
@media only screen and (min-width: 375px) {
  .logo {
    width : 62.5px;
  }
}

@media only screen and (min-width: 360px) {
  .logo {
    width : 60px;
  }
}

@media only screen and (min-width: 320px) {
  .logo {
    width : 53.3333px;
  }
}

2.2、使用 rem 单位

rem(font size of the root element)是指相对于根元素的字体大小的单位,如果我们设置 html 的 font-size 为 16px,则如果需要设置元素字体大小为 16px,则写为 1rem。但是其还是必须得借助 @media 属性来为不同大小的设备设置不同的 font-size,相对上一种方案,可以减少重复编写相同属性的代价,简单示例如下所示。
我们也能看到该方案存在以下问题:

  • 不同的尺寸需要写多个 @media
  • 所有涉及到使用 rem 的地方,全部都需要调用方法 calc() ,这个也挺麻烦的;
@media only screen and (min-width: 375px) {
  html {
    font-size : 375px;
  }
}

@media only screen and (min-width: 360px) {
  html {
    font-size : 360px;
  }
}

@media only screen and (min-width: 320px) {
  html {
    font-size : 320px;
  }
}

//定义方法:calc
@function calc($val){
    @return $val / 1080;
}

.logo{
	width : calc(180rem);
}

2.3、flexible 适配方案

在 rem 方案上进行改进,我们可以使用 js 动态来设置根字体,这种方案的典型代表就是 flexible 适配方案

2.3.1、 使用 rem 模拟 vw 特性适配多种屏幕尺寸

它的核心代码如下所示

// set 1rem = viewWidth / 10
function setRemUnit () {
    var rem = docEl.clientWidth / 10
    docEl.style.fontSize = rem + 'px'
}
setRemUnit();

上面的代码中,将 html 节点的 font-size 设置为页面 clientWidth(布局视口)的 1/10,即 1rem 就等于页面布局视口的 1/10,这就意味着我们后面使用的 rem 都是按照页面比例来计算的。

2.3.2、控制 viewport 的 width 和 scale 值适配高倍屏显示

设置 viewport 的 width 为 device-width,改变浏览器 viewport(布局视口和视觉视口)的默认宽度为理想视口宽度,从而使得用户可以在理想视口内看到完整的布局视口的内容。
等比设置 viewport 的 initial-scale、maximum-scale、minimum-scale 的值,从而实现 1 物理像素=1 css像素,以适配高倍屏的显示效果(就是在这个地方规避了大家熟知的“1px 问题”)

var metaEL= doc.querySelector('meta[name="viewport"]');
var dpr = window.devicePixelRatio;
var scale = 1 / dpr
metaEl.setAttribute('content', 'width=device-width, initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no'); 

2.3.3、flexible 的缺陷

不可否认 flexible 在兼容性不友好的某个时期还是极大帮助来成千上万的开发者,但是该方案自身是存在一些问题的。
(1)由于其缩放的缘故,video 标签的视频频播放器的样式在不同 dpr 的设备上展示差异很大;
(2)如果你去研究过 lib-flexible 的源码,那你一定知道 lib-flexible 对安卓手机的特殊处理,即:一律按 dpr = 1 处理;

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;
}

(3)不再兼容 @media 的响应式布局,因为 @media 语法中涉及到的尺寸查询语句,查询的尺寸依据是当前设备的物理像素,和 flexible 的布局理论(即针对不同 dpr 设备等比缩放视口的 scale 值,从而同时改变布局视口和视觉视口大小)相悖,因此响应式布局在“等比缩放视口大小”的情境下是无法正常工作的;

其实 flexible 方案是在 模拟 viewport 功能,只是随着浏览器的发展及兼容性增强,viewport 已经能兼容绝大部分主流浏览器,并且 flexible 方案自身存在的问题,所有其也已几乎退出历史潮流。引用 lib-flexible 的 github 主页的原话:

由于 viewport 单位得到众多浏览器的兼容,lib-flexible 这个过渡方案已经可以放弃使用,不管是现在的版本还是以前的版本,都存有一定的问题。建议大家开始使用 viewport 来替代此方案。

2.4、viewport 适配方案

vw 作为布局单位,从底层根本上解决了不同尺寸屏幕的适配问题,因为每个屏幕的百分比是固定的、可预测、可控制的。 viewport 相关概念如下:

  • vw:是 viewport's width 的简写,1vw 等于 window.innerWidth 的 1%;
  • vh:和 vw 类似,是 viewport's height 的简写,1vh 等于 window.innerHeihgt 的 1%;
  • vmin:vmin 的值是当前 vw 和 vh 中较小的值;
  • vmax:vmax 的值是当前 vw 和 vh 中较大的值;

假设我们拿到的视觉稿宽度为 750px,视觉稿中某个字体大小为 75px,则我们的 css 属性只要如下这么写,不需要额外的去用 js 进行设置,也不需要去缩放屏幕等;

.logo {
  font-size: 10vw; // 1vw = 750px * 1% = 7.5px
}

2.4.1、设置 meta 标签

在 html 头部设置 mata 标签如下所示,让当前 viewport 的宽度等于设备的宽度,同时不允许用户手动缩放。

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">

2.4.2、px 自动转换为 vw

设计师一般给宽度大小为 375px 或 750px 的视觉稿,我们采用 vw 方案的话,需要将对应的元素大小单位 px 转换为 vw 单位,这是一项影响开发效率(需要手动计算将 px 转换为 vw)且不利于后续代码维护(css 代码中一堆 vw 单位,不如 px 看的直观)的事情;好在社区提供了 postcss-px-to-viewport 插件,来将 px 自动转换为 vw,相关配置步骤如下:
(1)安装插件

npm install postcss-px-to-viewport --save-dev

(2)webpack 配置
官网是使用 glup 进行配置,但是我们项目模版中是使用 webpack 进行 postcss 插件以及相关样式插件的配置,所以我们就使用 webpack 进行配置使用,不需要额外引入 gulp 编译;webpack 相关配置如下,且每个属性表示的意义进行了备注:

module.exports = {
  plugins: {
    // ...
    'postcss-px-to-viewport': {
      // options
      unitToConvert: 'px',    // 需要转换的单位,默认为"px"
      viewportWidth: 750,     // 设计稿的视窗宽度
      unitPrecision: 5,       // 单位转换后保留的精度
      propList: ['*', '!font-size'],        // 能转化为 vw 的属性列表
      viewportUnit: 'vw',     // 希望使用的视窗单位
      fontViewportUnit: 'vw', // 字体使用的视窗单位
      selectorBlackList: [],  // 需要忽略的 CSS 选择器,不会转为视窗单位,使用原有的 px 等单位
      minPixelValue: 1,       // 设置最小的转换数值,如果为 1 的话,只有大于 1 的值会被转换
      mediaQuery: false,      // 媒体查询里的单位是否需要转换单位
      replace: true,          // 是否直接更换属性值,而不添加备用属性
      exclude: undefined,     // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
      include: /\/src\//,     // 如果设置了include,那将只有匹配到的文件才会被转换
      landscape: false,       // 是否添加根据 landscapeWidth 生成的媒体查询条件
      landscapeUnit: 'vw',    // 横屏时使用的单位
      landscapeWidth: 1125,   // 横屏时使用的视窗宽度
    },
  },
};

相关配置属性,通过注释一目了然其作用,其中需要强调的点为 propList 属性,我们配置了 font-size 不进行转换 vw,也就是说在不同手机屏幕尺寸下的字体大小是一样的。
其中 font-size 是否需要根据屏幕大小做适配,或者怎么做,一直是个争论不休的话题;考虑到我们移动端没有平板的需求,且咨询过团队业务设计师的意见,所以对模版进行以上默认配置;当然如果你的视觉要求你的项目要做字体大小适配,修改 propList 属性的配置即可。

(3)效果展示
我们在项目代码中,进行如下 css 编码:

.hello {
  color: #333;
  font-size: 28px;
}

启动项目,我们可以看到浏览器渲染的页面中,postcss-px-to-viewport 已经帮我们做进行了 px -> vw 的转换;如下所示:
image.png

2.4.3、标注不需要转换的属性

在项目中,如果设计师要求某一场景不做自适配,需为固定的宽高或大小,这时我们就需要利用 postcss-px-to-viewport 插件的 Ignoring 特性,对不需要转换的 css 属性进行标注,示例如下所示:

  • /* px-to-viewport-ignore-next */ —> 下一行不进行转换.
  • /* px-to-viewport-ignore */ —> 当前行不进行转换
/* example input: */
.class {
  /* px-to-viewport-ignore-next */
  width: 10px;
  padding: 10px;
  height: 10px; /* px-to-viewport-ignore */
}

/* example output: */
.class {
  width: 10px; 
  padding: 3.125vw;
  height: 10px;
}

2.4.4、Retina 屏预留坑位

考虑 Retina 屏场景,可能对图片的高清程度、1px 等场景有需求,所以我们预留判断 Retina 屏坑位。
相关方案如下:在入口的 html 页面进行 dpr 判断,以及 data-dpr 的设置;然后在项目的 css 文件中就可以根据 data-dpr 的值根据不同的 dpr 写不同的样式类;
(1)index.html 文件

// index.html 文件
const dpr = devicePixelRatio >= 3? 3: devicePixelRatio >= 2? 2: 1;
document.documentElement.setAttribute('data-dpr', dpr);

(2)样式文件

[data-dpr="1"] .hello {
  background-image: url(image@1x.jpg);

[data-dpr="2"] .hello {
  background-image: url(image@2x.jpg);
}
  
[data-dpr="3"] .hello {
  background-image: url(image@3x.jpg);
}

三、若干特定场景最佳实践

3.1、行内样式的场景

场景:当你需要写行内样式的代码(style)时,postcss-px-to-viewport 插件 无法进行 px 单位无法转换,需要自己手动计算好 vw;
最佳实践:通过添加、修改、删除 className 的方式进行处理此类场景,不直接操作行内样式,这更符合将 js 和 css 隔离开的更佳实践。

3.2、1px 的问题

retina 屏下 1px 问题是个常谈的问题,相比较普通屏,retina 屏的 1px 线会显得比较粗,设计美感欠缺;在视觉设计师眼里的 1px 是指设备像素 1px,而如果我们直接写 css 的大小 1px,那在 dpr = 2 时,则等于 2px 设备像素,dpr = 3 时,等于 3px 设备像素。所以对于要求处理 1px 的场景,我们要进行特殊处理。以下介绍常用的几种方法

3.2.1、transform: scale(0.5)

可以使用 transform: scale(0.5) 进行 X、Y 轴的缩放,如下示例所示

.class1 {
  height: 1px; 
  transform: scaleY(0.5);
}

优点是编写简单,但是如果实现上下左右四条边框会比较难搞,并且如果有嵌套存在的话,会对包含的元素产生影响,所以结合 :before 和 :after 来使用。

3.2.2、transform: scale(0.5) + :before / :after (推荐)

此种方式能解决例如 标签上下左右边框 1px 的场景,以及有嵌套元素存在的场景,比较通用,示例如下所示

.calss1 {
  position: relative;
  &::after {
    content:"";
    position: absolute;
    bottom:0px;
    left:0px;
    right:0px;
    border-top:1px solid #666;
    transform: scaleY(0.5);
  }
}

3.2.3、box-shadow

利用 css 对阴影处理来模拟边框,示例如下所示,底部一条线,缺点是存在阴影不好看。

  .class1 {
    box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.5);
  }

3.2.4、其它

还有如下等方式处理 1px 问题,但不推荐,了解即可

  • viewport: 将页面进行缩小处理;
  • border-image:切个 1px 图片来模拟;
  • background-image:切个 1px 图片来模拟;
  • linear-gradient:通过线性渐变,来实现移动端 1px 的线;
  • svg:基于矢量图形(svg) 在不同设备屏幕特性下具有伸缩性。

3.3、图片高清的问题

图片高清的问题:
(1)适用普通屏的图片在 retina 屏中,图片展示就会显得模糊;
(2)适用 retina 屏的图片在普通屏中,图片展示就会缺少色差、没有锐利度,并且浪费带宽;
所以如果对性能、美观要求很高的场景,需要根据 dpr 区分使用对应的图片,我们在文章 viewport 适配方案中针对 retina 屏预留了 dpr 方案,相关 css 写法如下:

[data-dpr="1"] .hello {
  background-image: url(image@1x.jpg);

[data-dpr="2"] .hello {
  background-image: url(image@2x.jpg);
}
  
[data-dpr="3"] .hello {
  background-image: url(image@3x.jpg);
}

四、iPhoneX 适配方案

iPhoneX 取消了物理按键,改成底部小黑条,这一改动导致网页出现了比较尴尬的屏幕适配问题。对于网页而言,顶部(刘海部位)的适配问题浏览器已经做了处理,所以我们只需要关注底部与小黑条的适配问题即可(即常见的吸底导航、返回顶部等各种相对底部 fixed 定位的元素)。关于 iPhoneX 的适配方案,可以查看笔者的另一篇文章《 iPhoneX 适配方案》

五、VW 兼容方案

Android 4.4 之下和 iOS 8 以下的版本有一定的兼容性问题(ps:几乎绝迹,大家可以统计下你们的用户使用的系统版本占比),但是社区提供了兼容性解决方案,其为 viewport 的 buggyfill:Viewport Units Buggyfill,可以访问其 github 官网查看;我们也做了对应的实践,但是考虑到性能,我们项目模版中不会进行引入,有兴趣的同学可以查看以下实践总结;

5.1、Viewport Units Buggyfill 引入

viewport-units-buggyfill 主要有两个 JavaScript 文件:viewport-units-buggyfill.js 和 viewport-units-buggyfill.hacks.js。你只需要在你的 HTML 文件中引入这两个文件,比如在 react 项目中的 index.html 引入它们;

<script src="//g.alicdn.com/fdilab/lib3rd/viewport-units-buggyfill/0.6.2/??viewport-units-buggyfill.hacks.min.js,viewport-units-buggyfill.min.js"></script>

第二步,在HTML文件中调用 viewport-units-buggyfill,比如:

<script>
    window.onload = function () {
        window.viewportUnitsBuggyfill.init({
            hacks: window.viewportUnitsBuggyfillHacks
        });
    }
</script>

但是为保证 Viewport Units Buggyfill 起作用,我们必须在我们样式文件中用到了viewport 的单位(vw、vh、vmin 或 vmax )地方添加 content,如下所示:

.my-viewport-units-using-thingie {
  width: 50vmin;
  height: 50vmax;
  top: calc(50vh - 100px);
  left: calc(50vw - 100px);

  /* hack to engage viewport-units-buggyfill */
  content: 'viewport-units-buggyfill; width: 50vmin; height: 50vmax; top: calc(50vh - 100px); left: calc(50vw - 100px);';
}

5.2、postcss-viewport-units 引入

在 1 步骤中,我们人肉引入 content 属性,效率是非常低下的,好在社区提供了 postcss-viewport-units 插件,帮我们自动处理 content:

5.2.1、postcss-viewport-units 安装配置

我们执行以下命令,进行 postcss-viewport-units 插件的安装:

tnpm i postcss-viewport-units --save-dev

在我们的项目配置文件 webpack.config.js 中进行对应的插件引入配置:

{
  loader: 'postcss-loader',
  options: {
    ident: 'postcss',
    plugins: () => [
      // 我们加的配置
      require('postcss-viewport-units'),
    ],
    sourceMap: isProductionEnv,
  },
},

5.2.2、效果展示

我们在项目代码中,进行如下编码:

.hello {
  color: #333;
  font-size: 28px;
}

展示的页面中,postcss-viewport-units 已经帮我们添加了 content 属性;如下所示:
image.png

六、参考文献:

  1. 响应式设计 - 理解设备像素、设备独立像素和 css 像素
  2. 移动前端开发之viewport的深入理解
  3. 使用 Flexible 实现手淘 H5 页面的终端适配
  4. VW: 是时候放弃 REM 布局了
  5. lib-flexible
  6. postcss-px-to-viewport
  7. 网页适配 iPhoneX