高清屏中1px线问题
在移动端web开发中,UI设计稿中设置边框为1像素,前端在开发过程当中若是出现border:1px,测试会发如今retina屏机型中,1px会比较粗,便是较经典的移动端1px像素问题。javascript
为何高清屏下1px更宽
高清屏(retina屏)是指高dpr的设备,其物理像素的密度更大。又分为有两倍屏,三倍屏。css
dpr:物理像素/css像素html
在普通屏,1个css像素对应1个物理像素;2倍屏中,一个css像素对应4个物理像素;三倍屏中则是9个。前端

按照这样的置换规则后一张相同的图片在不一样的设备上才会显示相同的大小。java
1px的线在高清屏下本应不须要作特殊处理。两倍屏下会自动用两排物理像素去展现‘1px’的细线,普通屏用一排物理像素去展现‘1px’的细线,他们应该看起来是相同的。可是,就像数学中的概念:线是没有宽度的,点是没有大小的。像素一样是没有大小的。webpack
两倍屏的物理像素密度是普通屏的两倍,并非每个物理像素是普通屏的1/4大小,而是物理像素的间距是普通屏间距的1/2。ios
用两倍屏下用两排像素去展现,天然会比普通屏中用一排像素去展现看起来更粗。web
如何修正高清屏下的1px问题
要解决1px问题,本质就是让高清屏用一个物理像素去展现一个css像素。面试
能够按照不一样屏幕的dpr做出转换,好比在2倍屏下将1px的细线写成border:0.5px
。但这种方法只在ios上支持,安卓上会显示成被当成0px处理。浏览器
更通用的方案中,有svg和伪类元素两种。
SVG方案
这种方案本质上border并无变细,可是boder被一分为二,靠内侧的是透明的。

关键的样式代码是css中的svg生成函数。
SVG即矢量图,用xml标签写在html文件中。
经过postcss-write-svg
这个postcss插件将css中svg函数生成的图像处理成base64。这样就能够在css文件直接调用svg函数。
/* src/index.css */@svg custom-name { width: 4px; height: 4px; @rect { fill: transparent; width: 100%; height: 100%; stroke-width: 1; stroke: var(--color, black); }}.svg-retina-border { border: 1px solid; border-image: svg(custom-name param(--color green)) 1 repeat;}.normal-border { border: 1px solid green;}
处理事后的样子
剩余完整代码
import './index.css'const root = document.getElementById('root')const div2 = document.createElement('div')div2.innerHTML = 'SVG-retina-border'div2.className = 'svg-retina-border'root.append(div2)root.append(document.createElement('br'))const div3 = document.createElement('div')div3.innerHTML = 'normal-border'div3.className = 'normal-border'root.append(div3)
<!-- src/index.html --><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <div id="root"></div> </body></html>
// webpack.config.jsconst path = require('path')const HtmlPlugin = require('html-webpack-plugin')module.exports = { mode: 'development', entry: { entry1: './src/index.js' }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' }, module: { rules: [{ test: /\.css$/, use: ['style-loader', 'css-loader', 'postcss-loader'] }] }, plugins: [ new HtmlPlugin({ template: './src/index.html' }) ], devServer: { contentBase: path.resolve(__dirname, 'dist'), host: '0.0.0.0', port: 3005, compress: true, disableHostCheck: true }}
SVG
分别直接用xml的svg标签和css实现了两个100px,边框宽为1的矩形。
高清屏下效果以下。

<-- 视口大小--> <svg xmlns="custom-namespace" width="100" height="100"> <rect <--矩形大小--> width="100" height="100" fill="transparent" <--svg中全部的单位都是px--> stroke-width="1" stroke="black" /> </svg> <div style="width: 100px;height: 100px;border: 1px solid black;box-sizing: border-box;"></div>
stroke-width
和border
同样,都将矩形的边设为了1px,可是用svg实现的矩形边框看起来却更细。关键的地方是使用svg标记的视口大小和使用rect标记的矩形大小是相同的。
svg中没有盒模型的概念,它的stroke画线并不是对应css中的border。更像是不占空间的outline。由于不占空间,它会以rect(矩形)的边界为中心画线,一条线一半宽度在矩形内,一半在矩形外。
而由于视口宽度正好等于矩形的大小,看到的线宽就只有一半了。
(用svg画一个100px大小+1px边宽的方形)
(用css画一个100px大小+1px边框的方形border-box)
若是把矩形缩小一点,不占满视口,这时候看到的border是完整的,因此和没处理过的1px同样粗。

border-image
border-image是三个属性的缩写
border-image-source: url('https://misc.aotu.io/leeenx/border-image/box.png');border-image-slice: 33% 20% 3 fill;border-image-repeat: stretch;
-
border-image-source:图片连接或base64; -
border-image-slice:图片切割的四个位置。把图片切成9块,除中间一块,其余八块分别被当成边框使用。接受1-4个参数(使用相似于padding/margin的尺寸设置)。能够是百分比(相对于图片自身),也能够是数字(单位是px)。最后的fill决定中间那块图片会不会被当成background使用。 -
border-image-repeat:stretch/round(平铺)/repeat(重复)上下左右四个正位的图片怎样被当成border使用。 -
round(平铺)会压缩,repeat(重复)会剪裁。
border-image必须配合border使用。最终border宽度是border-width。border-style也必须指定,border-color能够不用。
伪类元素方案

完整代码
// index.html<!DOCTYPE html><html lang="en"> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" type="text/css" href="./index.css" /> </head> <body> <div class="retina-border">retina border</div> <br /> <div class="normal-border">normal border</div> </body></html>
// index.css.retina-border { position: relative;}.retina-border::before { content: ''; position: absolute; width: 100%; height: 100%; transform-origin: left top; box-sizing: border-box; pointer-events: none; border-width: 1px; border-style: solid; border-color: #333;}@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) { .retina-border::before { width: 200%; height: 200%; transform: scale(0.5); }}@media (-webkit-min-device-pixel-ratio: 3), (min-resolution: 3dppx) { .retina-border::before { width: 300%; height: 300%; transform: scale(0.33); }}.normal-border { border: 1px solid #333;}
具体实现
以两倍屏为例
.retina-border { position: relative;}.retina-border::before { content: ''; position: absolute; top: 0px; right: 0px; width: 200%; height: 200%; transform: scale(0.5); transform-origin: left top; box-sizing: border-box; pointer-events: none; border-width: 1px; border-style: solid; border-color: #333;}
经过一个伪类选择器在retinaborder元素中加了一个子元素

border-width: 1px
将边框的宽度设为1px。
width:200%
而后将伪类元素的宽高都设置成父元素的2倍。(可是边框仍是1px)
transform:scale(0.5)
将伪类元素的x,y轴方向都缩放到0.5倍。
经过两次尺寸的设置,使这个伪类子元素保持内容的大小仍是和父元素同样,可是border:0.5px
的效果。
pointer-events: none
当有元素的层级重叠时,鼠标点击是没法穿透的。即绝对定位的伪类元素的层级更高,它底下的元素(即文字:retina border)没法被事件触发。置为none时,绝对定位的元素不触发事件,底下的那层才能被选中。
其余css样式做用
-
伪类元素默认的
display:inline
。而position:absolute
会使元素display:block
。只有块级元素的尺寸(宽/高)设置才是有效的。 -
其中伪类选择器中
content
是必填项,否则没法生效 -
transform-origin的缩放的中心点,默认是元素中心,
-
transform-origin的缩放的中心点,默认是元素中心,和绝对定位的top,right同样,相对的是padding+content部分整个空间的位置
-
绝对定位的元素其top和right值是相对于padding+content的,默认值是从content开始,因此要规定都是0,不然当父元素有padding时,border就移位了
(若是删去position:absolute)
(若是删去position:absolute+display:block)
当使用百分比时,其父元素的高度必须显式指定,(20px/20view)不能是由子元素撑开的,可是宽度是能够的。
两种方案比较
兼容性
svg方案通过postcss处理,最终会影响浏览器兼容性的是border-image属性
伪类元素元素:方案最终影响兼容性的是transform属性

结论:svg方案的兼容性更好。
灵活性
因为svg只能画出特定的形状,因此没法实现圆角边框。而伪类元素方案能够。
学习成本
svg方案所用到的border-image属性、svg特性的理解成本较高,而且须要postcss-write-svg处理。伪类元素方案相较简单。
总结
一般状况,伪类元素方案更好,不管是从成本仍是灵活性出发。若是是为了更高的兼容性选择svg方案,border-image属性必定要使用缩写。(否则兼容性会更差兼容性测试)
关注我
我是山月,正致力于天天用五分钟可以看完的简短答案回答一个大厂高频面试题。扫码添加个人微信,备注进群,加入高级前端进阶群.

欢迎关注公众号【互联网大厂招聘】,定时推送大厂内推信息及面试题简答,天天学习五分钟,半年进入大厂中

本文分享自微信公众号 - 全栈成长之路(shanyue-road)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。