该适配方案基于webpack和vue来阐述,但是原理基本大同小异,并不局限于webpack+vue
为方便阐述,直接以vue-cli搭建的脚手架项目开始
该项目引入一个插件postcss-px2rem配合使用,当然该插件也可以自己写

1.快速搭建好一个项目

vue init webpack my-project

安装好依赖之后,就算创建项目成功,此时把项目跑起来npm run dev

2.引入初始化样式

// reset.css
/* http://meyerweb.com/eric/tools/css/reset/ 
   v2.0 | 20110126
   License: none (public domain)
*/

html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed, 
figure, figcaption, footer, header, hgroup, 
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
	margin: 0;
	padding: 0;
	border: 0;
	font-size: 100%;
	font: inherit;
	vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure, 
footer, header, hgroup, menu, nav, section {
	display: block;
}
body {
	line-height: 1;
}
ol, ul {
	list-style: none;
}
blockquote, q {
	quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
	content: '';
	content: none;
}
table {
	border-collapse: collapse;
	border-spacing: 0;
}

上面的初始化样式是来自网上,我们可以根据自己的业务需求将样式写得更为完善:

// reset.css
/* 
* guagnzhul
*  20180930
*/
 *{
  box-sizing: border-box;
}
 html, body, div, span, applet, object, iframe,
 h1, h2, h3, h4, h5, h6, p, blockquote, pre,
 a, abbr, acronym, address, big, cite, code,
 del, dfn, em, img, ins, kbd, q, s, samp,
 small, strike, strong, sub, sup, tt, var,
 b, u, i, center,
 dl, dt, dd, ol, ul, li,
 fieldset, form, label, legend,
 table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed,
 figure, figcaption, footer, header,
 menu, nav, output, ruby, section, summary,
 time, mark, audio, video, input {
   margin: 0;
   padding: 0;
   border: 0;
   font-size: 100%;
   vertical-align: baseline;
 }
 
 /* HTML5 display-role reset for older browsers */
 article, aside, details, figcaption, figure,
 footer, header, menu, nav, section{
   display: block;
 }
 
 body {
   line-height: 1;
 }
 
 blockquote, q {
   quotes: none;
 }
 
 blockquote:before, blockquote:after, q:before, q:after {
   content: none;
 }
 
 table {
   border-collapse: collapse;
   border-spacing: 0;
 }
 
 /* custom */
 a {
   color: #7e8c8d;
   -webkit-backface-visibility: hidden;
   text-decoration: none;
 }
 
 li {
   list-style: none;
 }
 
 body {
   -webkit-text-size-adjust: none;
   -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
 }

/* 消除边框 */
input,img{
    border: none;
}

/* 消除高亮 */
input{
    outline: none;
    border-radius: 0;
}

/* 统一对齐方式,避免间隙, 限定最大宽度,避免溢出 */
img{
    vertical-align: bottom;
    max-width: 100%;
}

/* 方便具体页面设置不同的背景色 */
html, body, #app{
    min-height: 100%;

    /* 解决边界的margin不生效或溢出问题 */
    padding: 1px 0; /* no */
    margin: -1px 0; /* no */
}
body{
  /* 解决 Ios 300毫秒延迟 */
  touch-action: manipulation;
}


初始化的样式放置位置和引入方式可以参考下图:
初始化样式

3.安装插件postcss-plugin-px2rem

    npm install --save-dev postcss-px2rem

4.vue结尾文件引用vue-loader.config.js中的配置

'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
  ? config.build.productionSourceMap
  : config.dev.cssSourceMap
  
/*引入postcss-px2rem 通过require的形式*/ 
var px2rem = require('postcss-px2rem');

module.exports = {
  loaders: utils.cssLoaders({
    sourceMap: sourceMapEnabled,
    extract: isProduction,
    
    /*允许使用usePostCSS*/
    usePostCSS:true,
    
  }),
  cssSourceMap: sourceMapEnabled,
  cacheBusting: config.dev.cacheBusting,
  transformToRequire: {
    video: ['src', 'poster'],
    source: 'src',
    img: 'src',
    image: 'xlink:href'
  },
  
  /*配置remUnit*/
  postcss: function() {
    return [px2rem({remUnit: 75})];
  }
}

上面的代码中postcss: function() { return [px2rem({remUnit: 75})]; }是什么意思?这是我们自己去设定的一个值,为什么用75?它是什么意思?通常设计稿都是以iphone6的尺寸为标准,也就是375个物理像素,我们开发的时候可以把设计稿看做成750px。这样75就是750的十分之一,我们把一个屏幕的十分之一看做1rem,这样的话,我们就可以用750px的设计稿用px的方式去写代码,能够自动兼容所有的机型了。例如,你写一个width: 150px的宽度,那么它会自动计算150/75=2rem,那么你的样式最终生成出来的就是width: 2rem。因为我们的rem的基准是基于html的font-size的大小的,所以接下来我们要去设置这个大小。

5.在index.html引入font-size的设置,设置的比例与remUnit一致

// index.html,其将屏幕宽度分成了10份
<script>document.getElementsByTagName('html')[0].style.fontSize = (document.documentElement.clientWidth ||  document.body.clientWidth) /10 + 'px';</script>

6.适配各机型的原理是什么?

举个例子,iphone屏幕宽度375px(物理像素),分成10份,每份37.5px,即<html style="font-size: 37.5px;">,此时写下width: 150px,它会自动计算150/75=2rem,那么插件转换为width: 2rem,也就是会在该手机显示出width: 70px的效果。
这个时候换了iPad,其屏幕宽度是768px(物理像素),分成10份,每份37.5px,即<html style="font-size: 76.8px;">,此时写下width: 150px,它会自动计算150/75=2rem,那么插件转换为width: 2rem,也就是会在该平板显示出width: 153.59px的效果。
rem举例

7.关于1px的转换

并非所有机型和浏览器都会支持1px以下的像素,例如我们写width:1px,可能会被转换成width: 0.013333rem;,那么如果该浏览器并不支持1px的显示怎么办呢?这时候我们针对该比较特殊的单位,进行一个不编译的做法,也就是直接让它显示1px,而不会进行单位的转换。

// 做法很简单,只需要在该属性后面加上这个注释即可/* no */
h1{
  width: 1px; /* no */
}

8.关于1px会偏粗的问题

比如有的机型,dpr为2,那么我们所写的1px逻辑像素,会在屏幕上显示2个物理像素,那么我们看到线条会比我们想要的1px的粗,这时候我们应该分两类情况处理:
(1)手机支持1px以下像素的显示;
(2)手机不支持1px以下像素的显示。
当手机支持1px以下像素的显示之后,再分三种情况(只要兼容主流的机型的dpr):
(1)dpr为2时,1px除以2,即写0.5px,且不编译,其他尺寸同理;
(2)dpr为3时,1px除以3,即写0.333px,且不编译,其他尺寸同理;
(3)dpr为其他情况的时候,直接取原本的尺寸,即1px,且不编译。
所以我们的关键是,何以判断浏览器是否支持1px以下像素的显示,同时检测到手机屏幕dpr的值(以下为rem兼容代码adaptation.html):

<!-- rem兼容方案 -->
<meta id="__j_viewport_meta_tag__" name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<script>
;(function flexible (window, document) {
  var docEl = document.documentElement
  var dpr = window.devicePixelRatio || 1

  // adjust body font size
  function setBodyFontSize () {
    if (document.body) {
      document.body.style.fontSize = (12 * dpr) + 'px'
    }
    else {
      document.addEventListener('DOMContentLoaded', setBodyFontSize)
    }
  }
  setBodyFontSize();

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

  setRemUnit()

  // reset rem unit on page resize
  window.addEventListener('resize', setRemUnit)
  window.addEventListener('pageshow', function (e) {
    if (e.persisted) {
      setRemUnit()
    }
  })

  // detect 0.5px supports
  if (dpr >= 2) {
    var fakeBody = document.createElement('body')
    var testElement = document.createElement('div')
    testElement.style.border = '.5px solid transparent'
    fakeBody.appendChild(testElement)
    docEl.appendChild(fakeBody)
    if (testElement.offsetHeight === 1) {
      docEl.setAttribute('data-dpr', Math.floor(dpr));
    }
    docEl.removeChild(fakeBody)
  }

   docEl.setAttribute('data-origin-dpr', window.devicePixelRatio);
   
   var oMeta = document.getElementById('__j_viewport_meta_tag__')
   var iphoneXFixed = (osv = window.navigator.userAgent.match(/(iphone|ipad|ipod)\s+os\s+(\d{2})/i)) && osv.length > 0 && +osv[osv.length - 1] > 10 && 812 == screen.height && 375 == screen.width ? ", viewport-fit=cover" : "";
    oMeta.setAttribute("content", oMeta.getAttribute('content') + iphoneXFixed);
}(window, document));
</script>

当我们引入这个文件的时候,就可以忽略第5步提到的index.html引入的那段代码了,反而直接在index.html引入该文件即可,因为该文件代码已经包含了第5步的代码了,我们直接引入到index.html:

// index.html
<!DOCTYPE>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title>my-project</title>
</head>

<body>
  <div id="app"></div>
  <!-- built files will be auto injected -->
</body>
<script>
  (function flexible(window, document) {
    var docEl = document.documentElement
    var dpr = window.devicePixelRatio || 1

    // adjust body font size
    function setBodyFontSize() {
      if (document.body) {
        document.body.style.fontSize = (12 * dpr) + 'px'
      } else {
        document.addEventListener('DOMContentLoaded', setBodyFontSize)
      }
    }
    setBodyFontSize();

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

    setRemUnit()

    // reset rem unit on page resize
    window.addEventListener('resize', setRemUnit)
    window.addEventListener('pageshow', function (e) {
      if (e.persisted) {
        setRemUnit()
      }
    })

    // detect 0.5px supports
    if (dpr >= 2) {
      var fakeBody = document.createElement('body')
      var testElement = document.createElement('div')
      testElement.style.border = '.5px solid transparent'
      fakeBody.appendChild(testElement)
      docEl.appendChild(fakeBody)
      if (testElement.offsetHeight === 1) {
        docEl.setAttribute('data-dpr', Math.floor(dpr));
      }
      docEl.removeChild(fakeBody)
    }

    docEl.setAttribute('data-origin-dpr', window.devicePixelRatio);

    var oMeta = document.getElementById('__j_viewport_meta_tag__')
    var iphoneXFixed = (osv = window.navigator.userAgent.match(/(iphone|ipad|ipod)\s+os\s+(\d{2})/i)) && osv.length >
      0 && +osv[osv.length - 1] > 10 && 812 == screen.height && 375 == screen.width ? ", viewport-fit=cover" : "";
    oMeta.setAttribute("content", oMeta.getAttribute('content') + iphoneXFixed);
  }(window, document));

</script>

</html>

核心代码解释:
(1)检测屏幕的dpr并不是什么难事:

// 赋值默认值为1
var dpr = window.devicePixelRatio || 1

(2)设置html的font-size:

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

(3)检测浏览器是否支持1px以下像素显示:

    // detect 0.5px supports
    // 创建一个0.5px去检测,看是否得到浏览器的支持,若支持则认为浏览器支持1px以下像素,否则则认为不支持
  if (dpr >= 2) {
    // 创建一个body是dom节点
    var fakeBody = document.createElement('body')
    // 创建一个div
    var testElement = document.createElement('div')
    // 给该div一个0.5像素的透明border
    testElement.style.border = '.5px solid transparent'
    // div加入body
    fakeBody.appendChild(testElement)
    // document.documentElement加入fakeBody
    docEl.appendChild(fakeBody)
    // 当testElement.offsetHeight即可视高度能够为1的时候,证明该浏览器是可以让0.5px显示出1的效果
    // 从而证明该浏览器可以支持到1px以下像素的显示
    // 于是在docEl赋值自定义属性data-dpr为其dpr
    if (testElement.offsetHeight === 1) {
      alert(testElement.offsetHeight)
      docEl.setAttribute('data-dpr', Math.floor(dpr));
    }
    // 移除该dom
    docEl.removeChild(fakeBody)
  }

原理是借助一个testElement.style.border = '.5px solid transparent'的透明0.5px的dom节点去检测其可视高度testElement.offsetHeight从而得出该浏览器是否支持0.5像素的结论。

9.如何在CSS中运用

对于命中dpr为2和3的分别作处理(样式优先级最高,以覆盖后者默认样式),另外有一个默认的处理用户处理不命中的情况。两处都不能进行编译,因为要用原生来显示。

<style>
    [data-dpr="2"]{
        div {
        	/* 不可以编译且优先级最高 */
             border: 0.5px solid #979797 !important; /* no */
        }
   }

    [data-dpr="3"]{
        div {
             border: 0.333px solid #979797 !important; /* no */
        }
   }
   div {
   			/* 该处始终被执行,但是如果上面的执行了,该处会因为优先级不够高而被覆盖 */
   			border: 1px solid #979797; /* no */
  }
</style>

技巧:当都没有命中dpr为2和3的情况的话,就会触发默认的样式设置,但是此时可能会存在显示的1px比实际上的1px要大,这个时候可以考虑给予一定的透明度让其线条看起来更为纤细,达到视觉的效果。

例如:

   div {
   			/* 给予透明度显得线条更为纤细 */
   			border: 1px solid rgba(151, 151, 151, 0.8); /* no */
  }
Logo

前往低代码交流专区

更多推荐