高清屏 1px 邊框問題解決《轉自微信公眾號前端大全》
原文連結 :https://mp.weixin.qq.com/s/IfstO7MxoVNUNNuAFW7xMQ
高清屏中1px線問題
在移動端web開發中,UI設計稿中設定邊框為1畫素,前端在開發過程中如果出現border:1px,測試會發現在retina屏機型中,1px會比較粗,即是較經典的移動端1px畫素問題。
為什麼高清屏下1px更寬
高清屏(retina屏)是指高dpr的裝置,其物理畫素的密度更大。又分為有兩倍屏,三倍屏。
dpr:物理畫素/css畫素
在普通屏,1個css畫素對應1個物理畫素;2倍屏中,一個css畫素對應4個物理畫素;三倍屏中則是9個。
按照這樣的置換規則後一張相同的圖片在不同的裝置上才會顯示相同的大小。
1px的線在高清屏下本應不需要做特殊處理。兩倍屏下會自動用兩排物理畫素去展示‘1px’的細線,普通屏用一排物理畫素去展示‘1px’的細線,他們應該看起來是相同的。但是,就像數學中的概念:線是沒有寬度的,點是沒有大小的。畫素同樣是沒有大小的。
兩倍屏的物理畫素密度是普通屏的兩倍,並不是每一個物理畫素是普通屏的1/4大小,而是物理畫素的間距是普通屏間距的1/2。
用兩倍屏下用兩排畫素去展示,自然會比普通屏中用一排畫素去展示看起來更粗。
如何修正高清屏下的1px問題
要解決1px問題,本質就是讓高清屏用一個物理畫素去展示一個css畫素。
可以按照不同螢幕的dpr作出轉換,比如在2倍屏下將1px的細線寫成border:0.5px
更通用的方案中,有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.js
const 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屬性一定要使用縮寫。(不然相容性會更差相容性測試)