深圳幻海软件技术有限公司 欢迎您!

为什么会存在 1px 问题?怎么解决?

2023-02-28

在项目开发中一直深受1px的困扰,移动端展示的样式不是偏粗就是偏细、甚至无法看清。也许大家都尝试过或正在使用着各种解决方案,可是对于物理像素、逻辑像素、设备像素比等概念到底是什么,为什么会产生1像素等问题始终是一头雾水。。在进行了一番调研后,发现网上对于一些细节原理描述的都不太清晰。故本文结合了个人

在项目开发中一直深受 1px 的困扰,移动端展示的样式不是偏粗就是偏细、甚至无法看清。也许大家都尝试过或正在使用着各种解决方案,可是对于物理像素、逻辑像素、设备像素比等概念到底是什么,为什么会产生 1 像素等问题始终是一头雾水。。在进行了一番调研后,发现网上对于一些细节原理描述的都不太清晰。故本文结合了个人的一些理解,重点对其原理及实现进行探讨,希望能对像素相关问题彻底解惑。

为了便于更好的理解本文,下面对像素相关概念进行梳理。

像素

px

是图像显示的基本单元,相对单位。

设备像素(物理像素)

dp

device pixels,显示屏就是由一个个物理像素点组成,屏幕从工厂出来那天起物理像素点就固定不变了。也就是我们经常看到的手机分辨率所描述的数字。

设备独立像素(逻辑像素)

dip

device-independent pixels,就是我们手机的实际视口大小。是操作系统为了方便开发者而提供的一种抽象。程序与操作系统之间描述长度是以设备独立像素为单位。不随页面缩放、浏览器窗口大小而改变。

CSS像素


在 CSS 中使用的 px 都是指 CSS 像素。不考虑缩放情况下,1个 CSS 像素等于1个设备独立像素。

设备像素比

dpr

devicePixelRatio,是物理像素和设备独立像素的比值。

屏幕尺寸

inch

屏幕对角线长度

屏幕分辨率

Resoution

750*1334,手机屏幕纵、横方向像素点数,单位是px。常说的分辨率指的就是物理像素。相同大小的屏幕而言,屏幕分辨率越高显示的像素越多,单个像素尺寸较小,显示效果就越精细。

像素密度

dpi/ppi

概念


描述

dot per inch(pixels per inch),每英寸像素数,通过屏幕尺寸和分辨率来计算像素密度。也是屏幕出厂时就确定了。

简单来说就是像素单位基本分为三种:设备像素(物理像素)、设备独立像素(逻辑像素)、CSS 像素。下文将会围绕相关概念展开讨论。

话不多说,正文开始~~

为什么使用 1px 会出现问题

自从 2010 年 iPhone4 推出了 Retina 屏开始,移动设备屏幕的像素密度越来越高,于是便有了 2 倍屏、3 倍屏的概念。简单来说,就是手机屏幕尺寸没有发生变化,但屏幕的分辨率却提高了一倍,即同样大小的屏幕上,像素多了一倍。

那么我们获取到的 CSS 像素就不是真实的物理像素点了,于是便有了设备像素比的概念( ​​devicePixelRatio​​ 简称 dpr)。它用来描述屏幕物理像素与逻辑像素的比值。不同手机有不同的设备像素比,可参考 ⇲wiki 百科中对视网膜屏的描述

CSS 中的 1px 并不等于设备的 1px

对于前端来说,在高清屏出现之前,前端代码的 ​​1px​​​ 即等于手机物理像素点的 ​​1px​​​。但有了 dpr 的概念之后,由于前端代码中的使用的是 CSS 像素,手机会根据 dpr 换算成实际的物理像素大小来渲染页面。比如 iPhone6 的设备像素比 ​​dpr = 2​​​ ,相当于一个 CSS 像素等于两个物理像素,即 ​​1px​​ 由 2个物理像素点组成。

那么问题来了,以 iPhone6 为例,其 ​​dpr = 2​​​、屏幕尺寸(CSS 像素) 为 ​​375x667​​​,一般设计稿提供 2 倍图尺寸为 ​​750x1334​​​ 。那么设计稿中的 ​​1px​​​,对应屏幕尺寸其实应该写成 ​​0.5px​​​。再由 dpr 计算公式可知,​​0.5 * 2 = 1px​​ 物理像素。

此时你应该已经发现了,设计稿要实现 ​​1px​​​ 细线、​​1px​​​ 边框,为什么前端实现总是偏粗的?那是因为如果你在代码中直接写成 ​​1px​​​,再通过 dpr 计算之后其实是 ​​2px​​ 物理像素,并不符合设计稿的要求。

其实设计稿本质上要实现的是 CSS 像素的 !

那么当 ​​dpr=2​​​ 时,代码中直接写成 ​​0.5px​​ 就解决问题了吗?

小数点像素 0.5px 的兼容性问题

其实在项目中,我们已经采用 rem 单位进行了设计稿与屏幕尺寸的换算,即把 ​​1px​​​ 换算成了 ​​0.5px​​ 。但这种方案其实有各种各样的兼容性问题。

PC端

先上结论,在 PC 端浏览器的最小识别像素为 ​​1px​​。

所以在开发阶段,当在开发者工具上进行页面调试时,可以看到即便代码中是 ​​0.5px​​​ ,但默认会被浏览器识别并渲染为 ​​1px​​。所以在浏览器看来总是偏粗了。如果你习惯用 PC 端的页面来进行视觉走查,那么结果可想而知...


上图中两个元素 ​​width:200px;height:100px​​​,分别为 ​​border:0.5px​​​、​​border:1px​​​ ,检查元素时可以看到页面上 ​​border=0.5px​​​ 的元素计算大小后和 ​​1px​​​ 效果是一样的,均是 ​​width:202px;height:102px​​​。说明浏览器都识别成 ​​1px​​ 了。



由上面的 gif 图也可以看到,因为设备 ​​dpr=2​​​,所以放大后 ​​1px​​​ 的确是使用了2个物理像素点来渲染。并不是我们想实现的 ​​0.5px​​。

移动端

在手机端,不同手机浏览器对小数点像素的处理效果就更千奇百怪了。

首先我们先来看一下采用 REM 布局方式下,代码中的 0.01rem 到底被换算成了多少?

这里简单说下 REM 实现原理

​rem(font size of the root element)​​​,即根据网页的根元素(​​html​​​)来设置字体大小。和 ​​em(font size of the element)​​​ 的区别是,​​em​​ 是根据其父元素的字体大小来进行设置。

简单来说,rem 布局实现移动端适配的思想是,由于 rem 单位是根据页面根元素的 ​​fontSize​​​ 来计算的,那么将 ​​fontSize​​​ 设置成屏幕宽度 ​​clientWidth​​​ 与设计稿宽度 ​​750​​​ 的比值,那么我们按照设计稿的尺寸来重构页面的时候,使用 rem 单位即自动乘以 ​​fontSize​​ 计算出了适配不同屏幕的尺寸。

// 以750设计稿为例,计算rem font-size
let clientWidth = document.documentElement.clientWidth || document.body.clientWidth;
let ft = (clientWidth / 7.5).toFixed(2);
// 设置页面根字号大小
document.documentElement.style.fontSize = ft + "px";
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

由上面的计算方式可知,不同屏幕宽度会计算出不同 ​​fontSize​​​ ,那么 ​​0.01rem​​ 到底被换算成了多少呢?下面举例计算了几个机型的“1像素”大小

由表格可看出,不同手机计算的“1px”大小差别很大,而且手机本身对小数点的处理情况就存在较大的兼容性问题。

比如 IOS8+ 系列都已经支持 ​​0.5px​​​ 了,可以借助媒体查询来处理,但是安卓手机对小数像素的表现形式却各不相同。网上关于不同型号手机浏览器对小数点的处理情况的资料较少,只知道在一些低版本的系统里,​​0.5px​​​ 将会被显示为 ​​0px​​​;有的能够画出半个像素的边,有的大于 ​​0.55px​​​ 当成 ​​1px​​​,有的大于 ​​0.75px​​​ 当成 ​​1px​​,从表格计算结果来看是很难直接实现适配的。

比如 HUAWAI P30 的 ​​0.01rem​​​ 计算后为 ​​0.48px​​​ ,这种较小的小数像素其 ​​border​​ 已经无法正常展示了。

那么如何实现 1px 的效果?

在进行一番调研之后,发现目前的实现方案都离不开以下三种。

  1. 使用​​伪元素 + CSS3``缩放​​的方式
  2. 使用​​动态 viewport + rem 布局​​​ 的方式(即​​Flexible​​ 实现方案)
  3. 新方案:使用​​vw 单位​​适配方案(将来推荐的一种方案,但目前项目中没有实际应用,故本文不做讨论)

1. 伪元素 + CSS3缩放

其实这种方案也是大家在项目中经常使用的方式。文本主要对其实现原理进行分析。

前面已经讨论过要实现设计稿中的 ​​1px​​​,其实代码中要实现 ​​0.5px​​ 。缩放的方式就是避免了直接写小数像素带来的不同手机的兼容性处理不同。先上代码:

// 通过伪元素实现 0.5px border
.border::after {
    content: "";
    box-sizing: border-box; // 为了与原元素等大
    position: absolute;
    left: 0;
    top: 0;
    width: 200%; 
    height: 200%; 
    border: 1px solid gray;
    transform: scale(0.5); 
    transform-origin: 0 0;
}

// 通过伪元素实现 0.5px 细线
.line::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 200%;
    height: 1px;
    background: #b3b4b8;
    transform: scale(0.5);
    transform-origin: 0 0;
}

// dpr适配可以这样写
@media (-webkit-min-device-pixel-ratio: 2)  {
    .line::after {
        ...
        height: 1px;
        transform: scale(0.5);
        transform-origin: 0 0;
    }
}

@media (-webkit-min-device-pixel-ratio: 3)  {
    .line::after {
        ...
        height: 1px;
        transform: scale(0.333);
        transform-origin: 0 0;
    }
}
  • 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.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.

为什么要先放大 200% 再缩小 0.5?

为了只缩放 ​​border​​​ 1px 的粗细,而保证 ​​border​​​ 的大小不变。如果直接 ​​scale(0.5)​​​ 的话 ​​border​​​ 整体大小也会变成二分之一,所以先放大 200%(放大的时候 ​​border​​ 的粗细是不会被放大的)再缩放,就能保持原大小不变了。

为什么采用缩放的方式,就可以解决手机对小数点处理的兼容性问题?

此处我是这样理解的。首先代码中处理的是 ​​1px​​​ ,避免了直接操作小数像素的问题;当 ​​dpr=2​​​ 时,换算成物理像素为 2px,此时去缩放 ​​scale(0.5)​​​、当 ​​dpr=3​​​ 时,换算成物理像素为 3px,此时缩放 ​​scale(0.3)​​​ 后,手机均会默认使用最小物理像素 ​​1px​​​ 来渲染。按照 CSS3 ​​transform​​​ 的 ​​scale​​ 定义,边框可以任意细,理论上可以实现任意细的缩放效果。

该方案的优点在于,针对老项目使用缩放的形式可以快速实现 ​​1px​​ 的效果。

需要注意的是,我们是在 ​​1px​​ 的基础上进行缩放!

  • 如果项目中使用了 rem 单位的话,此处的​​1px​​ 是不能用 rem 单位的,否则根据 rem 换算后再进行缩放,会使得边框变得更细。
  • 如果项目中使用了​​postcss-pxtorem​​​ 插件进行编译的话,记得不要对​​1px​​ 进行编译。配置文档参考⇲postcss-pxtorem
.ignore {
border: 1Px solid; // ignored
border-width: 2PX; // ignored }
  • 1.
  • 2.
  • 3.

​Px​​​ or ​​PX​​​ is ignored by ​​postcss-pxtorem​​ but still accepted by browsers。

2. 动态 Viewport + REM 方式

第二种实现方案是采用动态设置 ​​viewport + rem​​​ 布局,该方案其实是参考了阿里早期开源的一个移动端适配解决方案 ​​flexible​​​ ,本文进行了一些改进。该方案不仅解决了移动端适配的问题,同时也较好的解决了 ​​1px​​ 的问题。

在理解它的实现原理之前,我们先来了解几个关键概念 ​​viewport视口​​​ 及 ​​meta 标签​​​ 及 页面缩放 ​​initial-sacle​​。

视口

就是浏览器上(或者是一个 APP 中的 ​​webview​​​ )用来显示网页的那部分区域,但 ​​viewport​​ 又不局限于浏览器可视区域的大小,它可能比浏览器的可视区域要大,也可能比浏览器的可视区域要小。

网上关于 ​​viewport​​ 的介绍比较经典的就是 ⇲Peter-Paul Koch⇲A tale of two viewports 。它阐述了三种 ​​viewport​​​,我们一般最常用的是 ​​layout viewport​​​ (浏览器默认的 ​​viewport​​)。默认宽度大于浏览器可视区域的宽度,所以浏览器默认会出现横向滚动条。

const clientWidth = document.documentElement.clientWidth || document.body.clientWidth
  • 1.

通过 meta 标签设置

如果不设置 ​​meta​​​ 标签的话,由于 ​​viewport​​​ 默认宽度是大于浏览器可视区域的,所以需要通过设置 ​​viewport​​​ 的宽度等于屏幕宽 ​​width=device-width​​ 来避免出现横向滚动条。

<meta 
  name="viewport" 
  content="
    width=device-width,  // 设置viewport的宽等于屏幕宽
    initial-scale=1.0,  // 初始缩放为1
    maximum-scale=1.0, 
    user-scalable=no,  // 不允许用户手动缩放
    viewport-fit=cover // 缩放以填充满屏幕
    " 
>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • ​name​​​ 设置元数据的名称,​​content​​​ 设置元数据的值。​​name​​​ 属性值为​​viewport​​ 时,表示设置有关视口初始大小的提示,仅供移动端使用
  • 同时设置​​width=device-width,initial-scale=1.0​​ 是为了兼容 iOS 和 IE 浏览器

关于页面缩放

​initial-scale​​​ 缩放值越大,当前 ​​viewport​​ 的宽度就会越小,反之亦然。

 比如屏幕宽度是 ​​320px​​​ 的话,如果我们设置 ​​initial-scale=2​​​ ,此时 ​​viewport​​​ 的宽度会变为只有 ​​160px​​​ 了。这也好理解,放大了一倍嘛,就是原来 ​​1px​​​ 的东西变成 ​​2px​​​ 了,但是并不是把原来的 ​​320px​​​ 变为 ​​640px​​​ ,而是在实际宽度不变的情况下,​​1px​​​ 变得跟原来的 ​​2px​​ 的长度一样了。

所以缩放页面的时候,实际上改变了 CSS 像素的大小,而数量不变。所以原来需要 ​​320px​​​ 才能填满的宽度现在只需要 ​​160px​​ 就做到了。

在开篇的表格中对

CSS 像素的定义是,不考虑缩放情况下,1个 CSS 像素等于1个设备独立像素。页面放大 200% 时,CSS

像素个数不变,大小变为二倍,相当于一个 CSS 像素在横纵向上会覆盖两个设备独立像素。浏览器窗口可容纳的设备独立像素数量是不变的,所以可视区域内

CSS 像素数量变少。

Flexible 适配方案及问题

有了上面几个概念,下面我们来说说 ​​flexible​​ 方案的实现原理及历史遗留问题。

Flexible 的大致实现思路是,首先根据 dpr 来动态修改 ​​meta​​​ 标签中 ​​viewport​​​ 中的 ​​initial-scale​​​ 的值,以此来动态改变 ​​viewport​​​ 的大小;然后页面上统一使用 rem 来布局,​​viewport​​​ 宽度变化会动态影响 ​​html​​​ 中的​​font-size​​ 值,以此来实现适配。

为什么不直接引用 flexible 库来进行移动端适配呢?

因为 ​​lib-flexible​​​ 这个库目前基本被弃用,由于该方案诞生较早,官方也是认为 ​​flexible​​​ 已经完成了它的历史使命,比如当时它只处理了 iOS 不同 dpr的场景,安卓设备下默认都设置为 ​​dpr = 1​​ 等,这是有问题的。有关于这方面的详细使用可以阅读早期整理的文章 ⇲使用Flexible实现手淘H5页面的终端适配

在日常的业务场景中,虽然我们不会去使用 ​​flexible​​​ 库,但其实大多还是沿用 ​​flexible​​ 实现的原理来进行移动端适配,并从中进行了一些改进来达到适配的目的。

下面为简单实现

<head>
    <meta
      name="viewport"
      content="width=device-width,user-scalable=no,initial-scale=1,
                minimum-scale=1,maximum-scale=1,viewport-fit=cover"
    />
    <script type="text/javascript">
      // 动态设置 viewportinitial-scale
      var viewport = document.querySelector("meta[name=viewport]");
      var dpr = window.devicePixelRatio || 1;
      var scale = 1 / dpr;
      viewport.setAttribute(
        "content",
        "width=device-width," +
          "initial-scale=" +
          scale +
          ", maximum-scale=" +
          scale +
          ", minimum-scale=" +
          scale +
          ", user-scalable=no"
      );
      // 计算 rem font-size
      var clientWidth = 
        document.documentElement.clientWidth || document.body.clientWidth;
      clientWidth > 750 && (clientWidth = 750);
      var ft = (clientWidth / 7.5).toFixed(2); // 以750设计稿为例
      document.documentElement.style.fontSize = ft + "px";

    </script>
  </head>
  • 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.

为什么页面缩放比例 initial-scale 设置为 1 / dpr ?

这也是为什么前文大篇幅去阐述设备像素比 ​​dpr​​、缩放等概念了。

通过设置页面缩放比例为 ​​1/dpr​​​,可将 ​​viewport​​​ 的宽度扩大 ​​dpr​​​ 倍。还是以 iPhone6 手机为例,不进行页面缩放时 ​​viewport​​​ 宽度 ​​375px​​​、​​dpr=2​​​。由于 ​​dpr​​ 的存在使得一个 CSS 像素需要两个物理像素来渲染。

当设置 ​​initial-scale = 1 / dpr = 0.5​​​ 时,获取到的 ​​viewport​​​ 宽度 ​​clientWidth = 750px​​​ ,被扩大了 ​​dpr​​​ 倍,就正好是设备物理像素的宽度。简单推导一下就是,当 ​​scale=0.5​​​ 时,由于 ​​viewport​​ 内可容纳的 CSS 像素数量的增多,相当于一个设备独立像素在横纵向上会覆盖两个 CSS 像素,

CSS像素个数 =  设备独立像素个数 /  scale   = ( 物理像素个数 / dpr )/ scale
scale = 1 / dpr 
// 所以
CSS像素个数 = 物理像素个数
  • 1.
  • 2.
  • 3.
  • 4.

此时我们写的 ​​1px​​​ 其实正好是一个物理像素的大小,并且可以较好的画出 ​​1px​​​ 的边框,从而提高显示精度,从此我们就可以愉快地直接写 ​​1px​​​ 啦!同时这个方案也较好的解决了只使用 rem 进行布局时,出现计算后的各种 ​​0.5px、0.55px​​ 等问题。完美~

#实战对比

下面为 border 的几种实现方式在不同测试机的对比图。

测试机型为 ​​iPhone6​​​、​​iPhone6Plus​​​、​​iPhoneXR​​​、​​HUAWEI P30​​​,以 ​​750​​​ 的设计稿为例,设置 ​​fontSize = clientWidth / 7.5 + 'px'​​。

4.1 首先采用第一种解决方案即 缩放的形式

❎ 按钮1:直接写 ​​1px​​,根据 dpr 计算可知,效果总是偏粗的。

❎ 按钮2:rem 布局下,不改变 ​​viewport​​​ 的缩放比,即 ​​initial-scale= 1​​​,​​fontSize​​​ 计算后范围在 ​​48px~55px​​​ 不等,​​0.01rem​​​ 计算后 ​​iPhone6​​​ 为 ​​0.5px​​​ ,​​dpr=2​​​,显示效果较好;而 ​​Huawei P30​​​ 的 ​​0.01rem = 0.48px​​,此时边框已经展示不清楚了。总体来说效果展示偏细。

✔️ 按钮3:也是 rem 布局下,​​initial-scale= 1​​,使用了第一种解决方案, 缩放 0.5 后,总体效果展示较好。

4.2 采用第二种解决方案,即动态设置 布局方式的对比图

设置 rem 布局下,

✔️ 按钮1:直接写 ​​1px​

✔️ 按钮4:使用 ​​0.01rem​​​,不同机型 ​​fontSize​​​ 在 ​​100px~144px​​​之间,​​0.01rem​​​ 计算后基本都大于等于 ​​1px​​。

分析下计算过程:

  • ​iPhone6​​​ 屏幕宽​​375px​​​,​​dpr = 2​​​、​​initial-scale= 0.5​​​ 时,​​clientWidth​​​ 变为​​750px​​​,根元素​​fontSize = 100px​​​,那么​​0.01rem​​​ 正好等于​​1px​​,并且大小和设计稿一致,展示效果理论上应该是最好的。
  • ​Huawei P30​​​ 屏幕宽​​360px​​​,​​dpr = 3​​​、​​initial-scale= 0.3​​​ 时,​​clientWidth​​​ 变为​​1080px​​​,根元素​​fontSize = 144px​​​,那么​​0.01rem = 1.44px​​​。其实这个时候我们实现的已经大于​​1px​​ 了。相当于大于1个物理像素来渲染。

两种方式效果展示都比较好,说明在此方案下,我们可以直接写 ​​1px​​​ 或者 ​​0.01rem​​ 都是可以的。

一些手机的屏幕分辨率整理

设备型号

屏幕分辨率/物理像素px

设备像素比dpr

独立像素   /CSS像素

屏幕尺寸-inch

像素密度ppi

iPhone4

640x960

2

320x480

3.5

326

QQ">Iphone5s

640x1136

2

320x568

4

326

Iphone6

750x1334

2

375x667

4.7

326

iphone6 Plus

1080x1920   (1242x2208)

3

414x736

5.5

401

iphoneX

1125x2436

3

375x812

5.8

458

iphoneXR

828x1792

2

414x896

6.1

326

iphoneXs Max

1242x2688

3

414x896??

6.5

458

Huawei P30

1080x2340

3

360x780

6.1

422

Huawei mate30

1080x2340

3

360x780

6.62

388

总结

移动端适配主要就分为两方面,一方面要适配不同机型的屏幕尺寸,一方面是对细节像素的处理过程。如果你在项目中直接写了 ​​1px​​ ,由于 dpr 的存在展示导致渲染偏粗,其实是不符合设计稿的要求。

如果你使用了 rem 布局计算出了对应的小数值,不同手机又有明显的兼容性问题。此时老项目的话整体修改 ​​viewport​​​ 成本过高,可以采用第一种实现方案进行 ​​1px​​​ 的处理;新项目的话可以采用动态设置 ​​viewport​​ 的方式,一键解决所有适配问题。

其实移动端对 ​​1px​​ 的渲染适配实现起来配置简单、代码简短,能够快速上手。但文本通过大量篇幅去阐述其原理,旨在能够对其真正理解并彻底解惑,希望能够对大家有所帮助。