移動(dòng)端1px問(wèn)題解決方案
高清屏中1px線(xiàn)問(wèn)題
在移動(dòng)端web開(kāi)發(fā)中,UI設(shè)計(jì)稿中設(shè)置邊框?yàn)?像素,前端在開(kāi)發(fā)過(guò)程中如果出現(xiàn)border:1px,測(cè)試會(huì)發(fā)現(xiàn)在retina屏機(jī)型中,1px會(huì)比較粗,即是較經(jīng)典的移動(dòng)端1px像素問(wèn)題。
為什么高清屏下1px更寬
高清屏(retina屏)是指高dpr的設(shè)備,其物理像素的密度更大。又分為有兩倍屏,三倍屏。
dpr:物理像素/css像素
在普通屏,1個(gè)css像素對(duì)應(yīng)1個(gè)物理像素;2倍屏中,一個(gè)css像素對(duì)應(yīng)4個(gè)物理像素;三倍屏中則是9個(gè)。

按照這樣的置換規(guī)則后一張相同的圖片在不同的設(shè)備上才會(huì)顯示相同的大小。
1px的線(xiàn)在高清屏下本應(yīng)不需要做特殊處理。兩倍屏下會(huì)自動(dòng)用兩排物理像素去展示‘1px’的細(xì)線(xiàn),普通屏用一排物理像素去展示‘1px’的細(xì)線(xiàn),他們應(yīng)該看起來(lái)是相同的。但是,就像數(shù)學(xué)中的概念:線(xiàn)是沒(méi)有寬度的,點(diǎn)是沒(méi)有大小的。像素同樣是沒(méi)有大小的。
兩倍屏的物理像素密度是普通屏的兩倍,并不是每一個(gè)物理像素是普通屏的1/4大小,而是物理像素的間距是普通屏間距的1/2。
用兩倍屏下用兩排像素去展示,自然會(huì)比普通屏中用一排像素去展示看起來(lái)更粗。
如何修正高清屏下的1px問(wèn)題
要解決1px問(wèn)題,本質(zhì)就是讓高清屏用一個(gè)物理像素去展示一個(gè)css像素。
可以按照不同屏幕的dpr作出轉(zhuǎn)換,比如在2倍屏下將1px的細(xì)線(xiàn)寫(xiě)成border:0.5px。但這種方法只在ios上支持,安卓上會(huì)顯示成被當(dāng)成0px處理。
更通用的方案中,有svg和偽類(lèi)元素兩種。
SVG方案
這種方案本質(zhì)上border并沒(méi)有變細(xì),但是boder被一分為二,靠?jī)?nèi)側(cè)的是透明的。

關(guān)鍵的樣式代碼是css中的svg生成函數(shù)。
SVG即矢量圖,用xml標(biāo)簽寫(xiě)在html文件中。
通過(guò)postcss-write-svg這個(gè)postcss插件將css中svg函數(shù)生成的圖像處理成base64。這樣就可以在css文件直接調(diào)用svg函數(shù)。
/* 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;}
處理過(guò)后的樣子
剩余完整代碼
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)
Document
// 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標(biāo)簽和css實(shí)現(xiàn)了兩個(gè)100px,邊框?qū)挒?的矩形。
高清屏下效果如下。
1598073606858<-- 視口大小--><--矩形大小-->width="100"height="100"fill="transparent"<--svg中所有的單位都是px-->stroke-width="1"stroke="black"/>
stroke-width和border一樣,都將矩形的邊設(shè)為了1px,但是用svg實(shí)現(xiàn)的矩形邊框看起來(lái)卻更細(xì)。關(guān)鍵的地方是使用svg標(biāo)記的視口大小和使用rect標(biāo)記的矩形大小是相同的。
svg中沒(méi)有盒模型的概念,它的stroke畫(huà)線(xiàn)并不是對(duì)應(yīng)css中的border。更像是不占空間的outline。因?yàn)椴徽伎臻g,它會(huì)以rect(矩形)的邊界為中心畫(huà)線(xiàn),一條線(xiàn)一半寬度在矩形內(nèi),一半在矩形外。
而因?yàn)橐暱趯挾日玫扔诰匦蔚拇笮。吹降木€(xiàn)寬就只有一半了。
(用svg畫(huà)一個(gè)100px大小+1px邊寬的方形)
(用css畫(huà)一個(gè)100px大小+1px邊框的方形border-box)
如果把矩形縮小一點(diǎn),不占滿(mǎn)視口,這時(shí)候看到的border是完整的,所以和沒(méi)處理過(guò)的1px一樣粗。

border-image
border-image是三個(gè)屬性的縮寫(xiě)
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:圖片切割的四個(gè)位置。把圖片切成9塊,除中間一塊,其他八塊分別被當(dāng)成邊框使用。接受1-4個(gè)參數(shù)(使用類(lèi)似于padding/margin的尺寸設(shè)置)。可以是百分比(相對(duì)于圖片自身),也可以是數(shù)字(單位是px)。最后的fill決定中間那塊圖片會(huì)不會(huì)被當(dāng)成background使用。
- border-image-repeat:stretch/round(平鋪)/repeat(重復(fù))上下左右四個(gè)正位的圖片怎樣被當(dāng)成border使用。
- round(平鋪)會(huì)壓縮,repeat(重復(fù))會(huì)剪裁。
border-image必須配合border使用。最終border寬度是border-width。border-style也必須指定,border-color可以不用。
偽類(lèi)元素方案

完整代碼
// index.htmlretina border
normal border
// 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;}
具體實(shí)現(xiàn)
以?xún)杀镀翞槔?/p>
.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;}
通過(guò)一個(gè)偽類(lèi)選擇器在retinaborder元素中加了一個(gè)子元素

border-width: 1px將邊框的寬度設(shè)為1px。
width:200%然后將偽類(lèi)元素的寬高都設(shè)置成父元素的2倍。(但是邊框還是1px)
transform:scale(0.5)將偽類(lèi)元素的x,y軸方向都縮放到0.5倍。
通過(guò)兩次尺寸的設(shè)置,使這個(gè)偽類(lèi)子元素保持內(nèi)容的大小還是和父元素一樣,但是border:0.5px的效果。
pointer-events: none當(dāng)有元素的層級(jí)重疊時(shí),鼠標(biāo)點(diǎn)擊是無(wú)法穿透的。即絕對(duì)定位的偽類(lèi)元素的層級(jí)更高,它底下的元素(即文字:retina border)無(wú)法被事件觸發(fā)。置為none時(shí),絕對(duì)定位的元素不觸發(fā)事件,底下的那層才能被選中。
其他css樣式作用
偽類(lèi)元素默認(rèn)的
display:inline。而position:absolute會(huì)使元素display:block。只有塊級(jí)元素的尺寸(寬/高)設(shè)置才是有效的。其中偽類(lèi)選擇器中
content是必填項(xiàng),不然無(wú)法生效transform-origin的縮放的中心點(diǎn),默認(rèn)是元素中心,
transform-origin的縮放的中心點(diǎn),默認(rèn)是元素中心,和絕對(duì)定位的top,right一樣,相對(duì)的是padding+content部分整個(gè)空間的位置
絕對(duì)定位的元素其top和right值是相對(duì)于padding+content的,默認(rèn)值是從content開(kāi)始,所以要規(guī)定都是0,否則當(dāng)父元素有padding時(shí),border就移位了
(如果刪去position:absolute)
(如果刪去position:absolute+display:block)
當(dāng)使用百分比時(shí),其父元素的高度必須顯式指定,(20px/20view)不能是由子元素?fù)伍_(kāi)的,但是寬度是可以的。
兩種方案比較
兼容性
svg方案經(jīng)過(guò)postcss處理,最終會(huì)影響瀏覽器兼容性的是border-image屬性
偽類(lèi)元素元素:方案最終影響兼容性的是transform屬性
1598076296220結(jié)論:svg方案的兼容性更好。
靈活性
由于svg只能畫(huà)出特定的形狀,所以無(wú)法實(shí)現(xiàn)圓角邊框。而偽類(lèi)元素方案可以。
學(xué)習(xí)成本
svg方案所用到的border-image屬性、svg特性的理解成本較高,并且需要postcss-write-svg處理。偽類(lèi)元素方案相較簡(jiǎn)單。
總結(jié)
通常情況,偽類(lèi)元素方案更好,無(wú)論是從成本還是靈活性出發(fā)。如果是為了更高的兼容性選擇svg方案,border-image屬性一定要使用縮寫(xiě)。(不然兼容性會(huì)更差兼容性測(cè)試)
