前端实现单位自动转换

265 阅读9分钟

背景

作为前端开发, UI 设计师给出的设计稿单位都是 px,而移动端开发时,需要适配不同的界面宽度,兼容不同机型。

本文介绍采用通过配置后,在开发中可以直接使用设计图的尺寸开发,项目为我们自动编译,转换单位。

本文主要介绍2种方式,rem 方式和vw 方式,并详细解释其中的原理。

基础知识回顾

先回顾一些 CSS 的基础知识,熟悉的同学可以直接跳过。

css 单位

1、px: 像素单位,固定单位,不会根据其他元素的大小或用户设置而改变

特点:

  • 绝对单位:px 是一个固定单位,不会根据其他元素的大小或用户设置而改变。
  • 使用场景:适用于需要精确控制尺寸的场合,如边框、图像大小等。

2、em:相对单位,基于当前元素的字体大小。

特点:

  • 相对单位:1em 等于当前元素的字体大小。
  • 继承性:em 会继承父元素的字体大小,因此在嵌套元素中,em 的值可能会累积。

任意浏览器的默认字体高都是16px。所有未经调整的浏览器都符合: 1em=16px。那么12px=0.75em,10px=0.625em

为了简化font-size的换算,需要在css中的body选择器中声明 Font-size=62.5%,这就使em值变为 16px*62.5%=10px, 这样12px=1.2em, 10px=1em, 也就是说只需要将你的原来的px数值除以10,然后换上em作为单位就行了。

3、rem:相对单位,基于根元素()的字体大小。

特点:

  • 相对单位:1rem 等于根元素的字体大小。
  • 不继承:与 em 不同,rem 不会累积,始终基于根元素。

4、vh vw:相对于视口的高度 和宽度。1vh 等于1/100的视口高度,1vw 等于1/100的视口宽度。

例如:浏览器高度900px,宽度为750px, 1 vh = 900px/100 = 9 px,1vw = 750px/100 = 7.5 px

100%和100vh/vw的区别:% 是相对于父元素的大小设定的比率。vw (viewport width) vh (viewport height) 是视窗的大小,100vw = 100%视窗宽度 100vh = 100%视窗高度,用vw,vh设定的大小只和视窗大小有关,和他们的父元素高度宽度没关系

CSS 像素、物理像素

CSS像素: 网页设计中使用的单位,通常是我们在CSS中设置的宽度、高度等。

物理像素: 设备屏幕上的实际像素点。

设备像素比(devicePixelRatio) :物理像素与CSS像素的比率。高分辨率设备(如Retina显示屏)通常有较高的DPR,比如2或3。

例如,我们在代码中设置一个 div 的宽度是 1px,在 DPR 是2的设备时,会显示为2个物理像素点。

在JavaScript中,可以通过window.devicePixelRatio获取到当前设备的dpr。

在CSS中,可以通过-webkit-device-pixel-ratio-webkit-min-device-pixel-ratio-webkit-max-device-pixel-ratio进行媒体查询。

meta[viewport] 标签

可以控制视口的大小和比例,告诉浏览器如何规范的渲染Web页面

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

其中,content 参数有以下几种:

value可能值描述
width1-10000 或 device-width正整数:以pixels(像素)为单位, 定义viewport(视口)的宽度 device-width: 100vw,100% 的视口宽度
height1-10000 或 device-height正整数:以pixels(像素)为单位, 定义viewport(视口)的高度 device-height: 100vh,100% 的视口高度
initial-scale0.1 - 10默认1。控制页面首次加载时显示的缩放倍数
maximum-scale0.1 - 10默认1。控制页面允许放大的倍数。设置一个低于 3 的值将不具备无障碍访问性
minimum-scale0.1 - 10默认1。控制页面允许缩小的倍数
user-scalable01yesno控制是否允许页面上的放大和缩小操作。 0、no:不允许 1、yes:允许

rem 方式

设置根节点 font-size

为什么设置根节点 font-size

从前文的基础知识可以了解到,使用rem为元素设定字体大小时,相对的是HTML根元素。这个单位可谓集相对大小和绝对大小的优点于一身,通过它既可以做到只修改根元素就成比例地调整所有字体大小,又可以避免字体大小逐层复合的连锁反应。

所以当我们根据设备的分辨率,来动态设置根节点 font-size 以后,那么页面中所有单位是 rem 的都会自动调整了。

如何设置根节点 font-size

  1. 引入 lib-flexible

    npm i lib-flexible
    
  1. 在 main.js 入口处引入

    import 'lib-flexible';
    
  1. 启动项目,看根节点是否已经有 font-size 了

    1

lib-flexible 解析

第一步,获取 dpr。优先从 meta[viewport] 标签读取 initial-scale ,生成 dpr,如果没有 meta[viewport],就根据 window.devicePixelRatio 等生成 dpr,并插入 meta[viewport] 标签。

从 meta[viewport] 标签读取:

const metaEl = document.querySelector('meta[name="viewport"]');
let dpr = 0;
let scale = 0;
if (metaEl) {
    const match = metaEl.getAttribute('content').match(/initial-scale=([\d.]+)/);
    if (match) {
        scale = parseFloat(match[1]);
        dpr = parseInt(1 / scale);
    }
}

移动端一般会在 html 的head 中添加如下标签,其中 initial-scale 控制页面首次加载时显示的缩放倍数。

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

所以这一段的目的,其实就是看页面的开发者是否依据了网页的内容最佳展示,设置了缩放倍数,如果有的话,就把它取出来,得到后问需要的 scaledpr

但如果网页的开发者并没有设置这个标签呢,也会通过 window.devicePixelRatio 做适配,可以看到只针对 iPhone 做了处理,针对 Android 或其他设备,都统一用了 1 处理

if (!dpr && !scale) {
    const isIPhone = window.navigator.appVersion.match(/iphone/gi);
    const devicePixelRatio = window.devicePixelRatio || 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;
    }
    scale = 1 / dpr;
}

如果没有 meta 标签,也会插入一个meta 标签

if (!metaEl) {
    metaEl = doc.createElement('meta');
    metaEl.setAttribute('name', 'viewport');
    metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
    if (docEl.firstElementChild) {
        docEl.firstElementChild.appendChild(metaEl);
    } else {
        var wrap = doc.createElement('div');
        wrap.appendChild(metaEl);
        doc.write(wrap.innerHTML);
    }
}

第二步,根据 dpr 和 页面宽度,设置根节点font-size

function refreshRem(){
    var width = docEl.getBoundingClientRect().width;
    if (width / dpr > 540) {
        width = 540 * dpr;
    }
    var rem = width / 10;
    docEl.style.fontSize = rem + 'px';
    flexible.rem = win.rem = rem;
}

针对 540的限制,我也查了相关资料,以下是作者解答😂。关于页面宽度更好的解决方案,目前还没有找到。总之,记住根节点字体大小是页面宽度的 1/10,比如页面宽度是460px,则根节点 font-size 设置为 46px;页面宽度是540px,则根节点 font-size 设置为 54px;页面宽度是600px,则根节点 font-size 设置为 54px。

1

这里添加一个小插曲,解释下 getBoundingClientRect().width 和 document.documentElement.clientWidth 的区别

getBoundingClientRect().widthdocument.documentElement.clientWidth
返回元素的边框矩形的宽度,包括滚动条的宽度(如果有的话)返回文档视口的宽度,不包括滚动条的宽度
返回的是一个浮点数,可能包含小数部分返回的是一个整数
计算的是元素在视口中的实际显示宽度,受 CSS 变换(如缩放、旋转等)影响,如果有缩放,获取的是缩放后的宽度仅考虑视口的宽度,不受 CSS 变换影响。如果有缩放,获取的仍然是原来的宽度

第3步,在页面调整时,重新执行设置根节点 font-size

win.addEventListener('resize', function() {
    clearTimeout(tid);
    tid = setTimeout(refreshRem, 300);
}, false);
win.addEventListener('pageshow', function(e) {
    if (e.persisted) {
        clearTimeout(tid);
        tid = setTimeout(refreshRem, 300);
    }
}, false);

自动转换 px 为 rem

经过以上的设置,已经在根节点添加了 font-size,接下来需要做的就是编译时,实现自动转换 px 为 rem。

如何设置

1、安装插件

npm install postcss-pxtorem autoprefixer -D

2、配置 postcss.config.js

const autoprefixer = require('autoprefixer');
const pxtorem = require('postcss-pxtorem');module.exports = {
  plugins: [
    pxtorem({ rootValue: 75, unitPrecision: 5, propList: ['*'] }),
    autoprefixer({ browsers: ['Android >= 4.0', 'iOS >= 7'] }),
  ]
};

3、通过以下命令构建的vue应用在编译时会自动查找配置文件 postcss.config.js (内部基于 webpack 配置)

如果没有通过以下命令构建应用,请自行查阅如何配置 postcss 使用

npm create vue@latest # 注意去掉 package.json 中 type 配置

4、启动,在控制台可以看到 根节点有 font-size ,并且 px 已经被自动转为 rem了

npm run dev

2

pxtorem 解析

pxtorem

function createPxReplace(rootValue, unitPrecision, minPixelValue) {
  return (m, $1) => {
    if (!$1) return m;
    const pixels = parseFloat($1);
    if (pixels < minPixelValue) return m;
    const fixedVal = toFixed(pixels / rootValue, unitPrecision);
    return fixedVal === 0 ? "0" : fixedVal + "rem";
  };
}

关注关键逻辑,可以看出,使用了配置的 rootValue 属性。

例如:项目代码中写的了 height: 150px,而 pxtorem 配置的 rootValue 为75,则编译后 height: 2rem

解释 rootValue 配置

实际就是1rem等于多少px的转换单位,默认是75,可以按照实际项目使用的 UI 设计稿的尺寸配置。

和前面的 font-size 结合理解一下,前文的根节点 font-size,我们是按照页面宽度的 1/10 设置的,那么举个例子说明:

IPhone14 Pro Max 尺寸:430

// html
font-size: 43px;

UI 设计稿:

查看设计稿选择页面宽度:750px 2倍
且设计稿展示的某卡片高度:150 px

那么我们将 pxtorem 的 rootValue 设置为 75:

pxtorem({ rootValue: 75 })

则我们在编写代码时候,这个卡片的高度,就直接按照设计稿的尺寸书写:

// css
height:150 px;

经过编译后:150 / 75

height:2rem;

rem 方式总结

1、根节点设置 font-size

2、添加 pxtorem ,编译时,自动将 px 转换为 rem 单位

3、页面开发时,根据 UI 设计师给出的尺寸,直接书写 px 开发

vw 布局方式

通过上文 rem 方式,相信大家也能看到 rem 的方式实现存在一些问题,比如设置font-size 的时候,作者使用了540作临时限制,导致在一些场景下是没法实现自适应兼容的。所以介绍第2种方式 vw 单位。

原理

目前UI设计师出设计稿,基本使用750px宽度的,那么100vw = 750px,即1vw = 7.5px。那么可以根据设计图上的px值直接转换成对应的vw值。在实际写代码过程,不需要进行任何的计算,直接在代码中写px,比如:

.test {
    border: .5px solid black;
    border-bottom-width: 4px;
    font-size: 14px;
    line-height: 20px;
    position: relative;
}
[w-188-246] {
    width: 188px;
}

编译出来的CSS:

.test {
    border: .5px solid #000;
    border-bottom-width: .533vw;
    font-size: 1.867vw;
    line-height: 2.667vw;
    position: relative;
}
[w-188-246] {
    width: 25.067vw;
}

如何设置

1、安装插件

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

2、配置 postcss.config.js

const autoprefixer = require('autoprefixer');
const pxToViewport = require('postcss-px-to-viewport');module.exports = {
    plugins: [
        autoprefixer({ browsers: ['Android >= 4.0', 'iOS >= 7'] }),
        pxToViewport({
            viewportWidth: 750, // 参考的设计稿的尺寸
            unitPrecision: 5,
        }),
    ]
};

3、启动,在控制台可以看到 px 已经被自动转为 vm 了

3

reference