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

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

關(guān)鍵的樣式代碼是css中的svg生成函數(shù)。
SVG即矢量圖,用xml標(biāo)簽寫在html文件中。
通過postcss-write-svg這個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;}
處理過后的樣子
剩余完整代碼
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實現(xiàn)了兩個100px,邊框?qū)挒?的矩形。
高清屏下效果如下。
1598073606858<-- 視口大小--><--矩形大小-->width="100"height="100"fill="transparent"<--svg中所有的單位都是px-->stroke-width="1"stroke="black"/>
stroke-width和border一樣,都將矩形的邊設(shè)為了1px,但是用svg實現(xiàn)的矩形邊框看起來卻更細。關(guān)鍵的地方是使用svg標(biāo)記的視口大小和使用rect標(biāo)記的矩形大小是相同的。
svg中沒有盒模型的概念,它的stroke畫線并不是對應(yīng)css中的border。更像是不占空間的outline。因為不占空間,它會以rect(矩形)的邊界為中心畫線,一條線一半寬度在矩形內(nèi),一半在矩形外。
而因為視口寬度正好等于矩形的大小,看到的線寬就只有一半了。
(用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塊,除中間一塊,其他八塊分別被當(dāng)成邊框使用。接受1-4個參數(shù)(使用類似于padding/margin的尺寸設(shè)置)??梢允前俜直龋ㄏ鄬τ趫D片自身),也可以是數(shù)字(單位是px)。最后的fill決定中間那塊圖片會不會被當(dāng)成background使用。
- border-image-repeat:stretch/round(平鋪)/repeat(重復(fù))上下左右四個正位的圖片怎樣被當(dāng)成border使用。
- round(平鋪)會壓縮,repeat(重復(fù))會剪裁。
border-image必須配合border使用。最終border寬度是border-width。border-style也必須指定,border-color可以不用。
偽類元素方案

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