本文只是基于「一物理像素边框」这一主题随意写的一些碎碎念,其中并不会详细讲解各种实现方式及其原理,如果想要知道这些内容,你在网上可以检索到很多,比如大漠的 再谈Retina下1px的解决方案 就很不错。

像素单位

首先,先聊下我们最常用的 CSS 单位 —— 'px'。

按照 CSS 的规定,单位 px 应当是一个绝对长度值;但这里所说的「绝对」,并不是指 1px 严格对等于显示设备上的一个物理像素点(虽然早年间确实如此),而是应当约等于 0.0213° 视角投射在屏幕上的长度(在假设屏幕距离用户眼睛为 71cm 的情况下,1px 应约等于 0.26mm)。

如此定义的目的,是因为这可以确保用户在正常使用任何显示设备时所看到的内容的大小都是大致相同的。

只不过,这个名字却容易引发歧义,因为当我们单独说到 'Pixel' 时,通常所指的是设备的物理像素点,也就是 'Dot',比如在 iOS 及 Android 系统中,px 所指的都是物理像素。(而对应的,iOS 的 pt 单位及 Android 的 dp 单位倒是与 CSS 中的 px 相似)

为什么需要一物理像素边框

接着,我们再说回边框的事。

通常,我们在进行布局和排版时,px(CSS)就已经足够使用了,几乎用不到物理像素。但有一种情况下,我们总是会需要比 1px 更小的长度值,这就是边框。

在移动端的使用场景中,其屏幕通常距离用户的眼睛会比较近,而且一般屏幕设备的像素密度都会比较高,因此即便是 1 物理像素的边框,用户也能看清而不会感到费力。另外,1 物理像素的边框相比会更加纤细,在用户眼中的视觉比重会更低,相应的会更加突出内容。所以在移动端,实现 1 物理像素的边框通常是有必要的。

实现

那么,CSS 中有比 px 更小的长度单位吗?抱歉,没有。我们可以在网上找到一些关于这件事的讨论,比如这个,但通常没有结果。

所以,我们只能通过各种花招来模拟实现它。常见的就有背景渐变、缩小伪元素、border-image 和 0.5px border 及 box-shadow 这几种,额外的还有像淘宝的 Flexible 方案这种因为缩放了整个页面所以顺便就实现了一物理像素边框的。这些方案的具体实现在网上都有很多,这里不再赘述。

0.5px border 或 box-shadow

相对来说,0.5px 边框和 box-shadow 是这些方案中最容易使用的两个。各用一行代码就可以实现,比如:

.hairline {
    border: 0.5px solid #000;
}

/* or */

.hairline {
    box-shadow: inset 0 0 0 0.5px #000;
}

不过虽然简单,但兼容性却着实让人头疼。

对于 0.5px 边框,iOS Safari 支持地非常好,目前苹果移动设备的设备像素比都是 2x 或 3x,在这些设备上,Safari 都能将 0.5px 的边框以 1 物理像素宽渲染。但是在 Android 中,新版系统中的内置浏览器引擎会将边框以 1px 宽进行渲染,而在老版系统中,压根就不会渲染。

而 box-shadow 的兼容性情况正好相反:Android 中可以正确处理 0.5px 的内扩散,Safari 却不会显示。

所以如果想要确保 iOS 和 Android 都能正常实现,就需要结合 JS 做一些特性检测。

背景渐变

相应的,背景渐变方案的兼容性要好的多,性能也很不错,结合 SCSS 用着也很方便:

.hairline {
    background: top center/100% 1px no-repeat linear-gradient(180deg, #000, #000 50%, transparent 50%),
                bottom center/100% 1px no-repeat linear-gradient(0deg, #000, #000 50%, transparent 50%),
                center left/1px 100% no-repeat linear-gradient(90deg, #000, #000 50%, transparent 50%),
                center right/1px 100% no-repeat linear-gradient(270deg, #000, #000 50%, transparent 50%);
}

不过它不支持圆角,使用场景比较有限就是了。

缩小伪元素

而至于缩小伪元素的方式,实现起来倒也不麻烦:

.hairline {
    position: relative;
}

.hairline:after {
    content: "";
    
    position: absolute;
    left: 0;
    top: 0;
    
    width: 200%;
    height: 200%;

    transform: scale(0.5);
    transform-origin: top left;
    
    border: 1px solid #000;
    border-radius: inherit;

    pointer-events: none;
}

不过通常并不推荐使用,一来它会占用至少一个宝贵的伪元素,二来像 input、select 和 img 这种不支持伪元素的还需要再在外面包一层其它元素,有时不太优雅。

其它实现方式

我在目前工作中一般只用到过以上三种方式,其它方式比如 border-image 等倒没怎么实际用过。不过看大漠讲的 PostCSS Write SVG 的方案可行性貌似还比较不错,之后有时间的话可以试试。

最后

在当前,所有实现一物理像素边框的方案或多或少都会有些问题,所以也没有什么一劳永逸的方案,我们只能是在实际场景中去挑选合适的方案去使用。

之后还是希望各浏览器能尽快全都兼容 0.5px 的 border 或 box-shadow。而物理像素单位的话,一来加入规范的可能性不大,二来在未来出现更高像素比的屏幕时其可用性如何还不好说,所以希望不大。