為什麼會存在 1px 問題?怎麼解決?

語言: CN / TW / HK

在專案開發中一直深受 1px 的困擾,移動端展示的樣式不是偏粗就是偏細、甚至無法看清。也許大家都嘗試過或正在使用著各種解決方案,可是對於物理畫素、邏輯畫素、裝置畫素比等概念到底是什麼,為什麼會產生 1 畫素等問題始終是一頭霧水。。在進行了一番調研後,發現網上對於一些細節原理描述的都不太清晰。故本文結合了個人的一些理解,重點對其原理及實現進行探討,希望能對畫素相關問題徹底解惑。

為了便於更好的理解本文,下面對 畫素相關概念 進行梳理。

概念 描述
畫素 px 是影象顯示的基本單元,相對單位。
裝置畫素(物理畫素) dp device pixels,顯示屏就是由一個個物理畫素點組成,螢幕從工廠出來那天起物理畫素點就固定不變了。也就是我們經常看到的手機解析度所描述的數字。
裝置獨立畫素(邏輯畫素) dip device-independent pixels,就是我們手機的實際視口大小。是作業系統為了方便開發者而提供的一種抽象。程式與作業系統之間描述長度是以裝置獨立畫素為單位。不隨頁面縮放、瀏覽器視窗大小而改變。
CSS畫素
在 CSS 中使用的 px 都是指 CSS 畫素。不考慮縮放情況下,1個 CSS 畫素等於1個裝置獨立畫素。
裝置畫素比 dpr devicePixelRatio,是物理畫素和裝置獨立畫素的比值。
螢幕尺寸 inch 螢幕對角線長度
螢幕解析度 Resoution 750*1334,手機螢幕縱、橫方向畫素點數,單位是px。常說的解析度指的就是物理畫素。相同大小的螢幕而言,螢幕解析度越高顯示的畫素越多,單個畫素尺寸較小,顯示效果就越精細。
畫素密度 dpi/ppi
dot per inch(pixels per inch),每英寸畫素數,通過螢幕尺寸和解析度來計算畫素密度。也是螢幕出廠時就確定了。

簡單來說就是畫素單位基本分為三種: 裝置畫素(物理畫素)、裝置獨立畫素(邏輯畫素)、CSS 畫素 。下文將會圍繞相關概念展開討論。

話不多說,正文開始~~

# 為什麼使用 1px 會出現問題

自從 2010 年 iPhone4 推出了 Retina 屏開始,移動裝置螢幕的畫素密度越來越高,於是便有了 2 倍屏、3 倍屏的概念。簡單來說,就是手機螢幕尺寸沒有發生變化,但螢幕的解析度卻提高了一倍,即同樣大小的螢幕上,畫素多了一倍。

那麼我們獲取到的 CSS 畫素就不是真實的物理畫素點了,於是便有了裝置畫素比的概念( devicePixelRatio 簡稱 dpr)。它用來描述螢幕物理畫素與邏輯畫素的比值。不同手機有不同的裝置畫素比,可參考 ⇲wiki 百科中對視網膜屏的描述

CSS 中的 1px 並不等於裝置的 1px

對於前端來說,在高清屏出現之前,前端程式碼的 1px 即等於手機物理畫素點的 1px 。但有了 dpr 的概念之後,由於前端程式碼中的使用的是 CSS 畫素,手機會根據 dpr 換算成實際的物理畫素大小來渲染頁面。比如 iPhone6 的裝置畫素比 dpr = 2 ,相當於一個 CSS 畫素等於兩個物理畫素,即 1px 由 2個物理畫素點組成。

那麼問題來了,以 iPhone6 為例,其 dpr = 2 、螢幕尺寸(CSS 畫素) 為 375x667 ,一般設計稿提供 2 倍圖尺寸為 750x1334 。那麼設計稿中的 1px ,對應螢幕尺寸其實應該寫成 0.5px 。再由 dpr 計算公式可知, 0.5 * 2 = 1px 物理畫素。

此時你應該已經發現了,設計稿要實現 1px 細線、 1px 邊框,為什麼前端實現總是偏粗的?那是因為如果你在程式碼中直接寫成 1px ,再通過 dpr 計算之後其實是 2px 物理畫素,並不符合設計稿的要求。

其實設計稿本質上要實現的是 CSS 畫素的 !

那麼當 dpr=2 時,程式碼中直接寫成 0.5px 就解決問題了嗎?

# 小數點畫素 0.5px 的相容性問題

其實在專案中,我們已經採用 rem 單位進行了設計稿與螢幕尺寸的換算,即把 1px 換算成了 0.5px 。但這種方案其實有各種各樣的相容性問題。

# PC端

先上結論,在 PC 端瀏覽器的最小識別畫素為 1px

所以在開發階段,當在開發者工具上進行頁面除錯時,可以看到即便程式碼中是 0.5px ,但預設會被瀏覽器識別並渲染為 1px 。所以在瀏覽器看來總是偏粗了。如果你習慣用 PC 端的頁面來進行視覺走查,那麼結果可想而知...

上圖中兩個元素 width:200px;height:100px ,分別為 border:0.5pxborder:1px ,檢查元素時可以看到頁面上 border=0.5px 的元素計算大小後和 1px 效果是一樣的,均是 width:202px;height:102px 。說明瀏覽器都識別成 1px 了。

由上面的 gif 圖也可以看到,因為裝置 dpr=2 ,所以放大後 1px 的確是使用了2個物理畫素點來渲染。並不是我們想實現的 0.5px

# 移動端

在手機端,不同手機瀏覽器對小數點畫素的處理效果就更千奇百怪了。

首先我們先來看一下采用 REM 佈局方式下,程式碼中的 0.01rem 到底被換算成了多少?

這裡簡單說下 REM 實現原理

rem(font size of the root element) ,即根據網頁的根元素( html )來設定字型大小。和 em(font size of the element) 的區別是, em 是根據其父元素的字型大小來進行設定。

簡單來說,rem 佈局實現移動端適配的思想是,由於 rem 單位是根據頁面根元素的 fontSize 來計算的,那麼將 fontSize 設定成螢幕寬度 clientWidth 與設計稿寬度 750 的比值,那麼我們按照設計稿的尺寸來重構頁面的時候,使用 rem 單位即自動乘以 fontSize 計算出了適配不同螢幕的尺寸。

// 以750設計稿為例,計算rem font-size
let clientWidth = document.documentElement.clientWidth || document.body.clientWidth;
let ft = (clientWidth / 7.5).toFixed(2);
// 設定頁面根字號大小
document.documentElement.style.fontSize = ft + "px";

由上面的計算方式可知,不同螢幕寬度會計算出不同 fontSize ,那麼 0.01rem 到底被換算成了多少呢?下面舉例計算了幾個機型的“1畫素”大小

由表格可看出,不同手機計算的“1px”大小差別很大,而且手機本身對小數點的處理情況就存在較大的相容性問題。

比如 IOS8+ 系列都已經支援 0.5px 了,可以藉助媒體查詢來處理,但是安卓手機對小數畫素的表現形式卻各不相同。網上關於不同型號手機瀏覽器對小數點的處理情況的資料較少,只知道在一些低版本的系統裡, 0.5px 將會被顯示為 0px ;有的能夠畫出半個畫素的邊,有的大於 0.55px 當成 1px ,有的大於 0.75px 當成 1px ,從表格計算結果來看是很難直接實現適配的。

比如 HUAWAI P30 的 0.01rem 計算後為 0.48px ,這種較小的小數畫素其 border 已經無法正常展示了。

# 那麼如何實現 1px 的效果?

在進行一番調研之後,發現目前的實現方案都離不開以下三種。

  1. 使用 偽元素 + CSS3``縮放 的方式

  2. 使用 動態 viewport + rem 佈局 的方式(即 Flexible 實現方案)

  3. 新方案:使用 vw 單位 適配方案(將來推薦的一種方案,但目前專案中沒有實際應用,故本文不做討論)

# 1. 偽元素 + CSS3縮放

其實這種方案也是大家在專案中經常使用的方式。文字主要對其實現原理進行分析。

前面已經討論過要實現設計稿中的 1px ,其實程式碼中要實現 0.5px 。縮放的方式就是避免了直接寫小數畫素帶來的不同手機的相容性處理不同。先上程式碼:

// 通過偽元素實現 0.5px border
.border::after {
content: "";
box-sizing: border-box; // 為了與原元素等大
position: absolute;
left: 0;
top: 0;
width: 200%;
height: 200%;
border: 1px solid gray;
transform: scale(0.5);
transform-origin: 0 0;
}

// 通過偽元素實現 0.5px 細線
.line::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 200%;
height: 1px;
background: #b3b4b8;
transform: scale(0.5);
transform-origin: 0 0;
}

// dpr適配可以這樣寫
@media (-webkit-min-device-pixel-ratio: 2) {
.line::after {
...
height: 1px;
transform: scale(0.5);
transform-origin: 0 0;
}
}

@media (-webkit-min-device-pixel-ratio: 3) {
.line::after {
...
height: 1px;
transform: scale(0.333);
transform-origin: 0 0;
}
}

為什麼要先放大 200% 再縮小 0.5?

為了只縮放 border 1px 的粗細,而保證 border 的大小不變。如果直接 scale(0.5) 的話 border 整體大小也會變成二分之一,所以先放大 200%(放大的時候 border 的粗細是不會被放大的)再縮放,就能保持原大小不變了。

為什麼採用縮放的方式,就可以解決手機對小數點處理的相容性問題?

此處我是這樣理解的。首先程式碼中處理的是 1px ,避免了直接操作小數畫素的問題;當 dpr=2 時,換算成物理畫素為 2px,此時去縮放 scale(0.5) 、當 dpr=3 時,換算成物理畫素為 3px,此時縮放 scale(0.3) 後,手機均會預設使用最小物理畫素 1px 來渲染。按照 CSS3 transformscale 定義,邊框可以任意細,理論上可以實現任意細的縮放效果。

該方案的優點在於,針對老專案使用縮放的形式可以快速實現 1px 的效果。

需要注意的是,我們是在 1px 的基礎上進行縮放!

  • 如果專案中使用了 rem 單位的話,此處的 1px 是不能用 rem 單位的,否則根據 rem 換算後再進行縮放,會使得邊框變得更細。
  • 如果專案中使用了 postcss-pxtorem 外掛進行編譯的話,記得不要對 1px 進行編譯。配置文件參考 ⇲postcss-pxtorem

    .ignore {

    border: 1Px solid; // ignored

    border-width: 2PX; // ignored }

Px or PX is ignored by postcss-pxtorem but still accepted by browsers。

# 2. 動態 Viewport + REM 方式

第二種實現方案是採用動態設定 viewport + rem 佈局,該方案其實是參考了阿里早期開源的一個移動端適配解決方案 flexible ,本文進行了一些改進。該方案不僅解決了移動端適配的問題,同時也較好的解決了 1px 的問題。

在理解它的實現原理之前,我們先來了解幾個關鍵概念 viewport視口meta 標籤 及 頁面縮放 initial-sacle

視口

就是瀏覽器上(或者是一個 APP 中的 webview )用來顯示網頁的那部分割槽域,但 viewport 又不侷限於瀏覽器可視區域的大小,它可能比瀏覽器的可視區域要大,也可能比瀏覽器的可視區域要小。

網上關於 viewport 的介紹比較經典的就是 ⇲Peter-Paul Koch⇲A tale of two viewports 。它闡述了三種 viewport ,我們一般最常用的是 layout viewport (瀏覽器預設的 viewport )。預設寬度大於瀏覽器可視區域的寬度,所以瀏覽器預設會出現橫向滾動條。

const clientWidth = document.documentElement.clientWidth || document.body.clientWidth

通過 meta 標籤設定

如果不設定 meta 標籤的話,由於 viewport 預設寬度是大於瀏覽器可視區域的,所以需要通過設定 viewport 的寬度等於螢幕寬 width=device-width 來避免出現橫向滾動條。

<meta 
name="viewport"
content="
width=device-width, // 設定viewport的寬等於螢幕寬
initial-scale=1.0, // 初始縮放為1
maximum-scale=1.0,
user-scalable=no, // 不允許使用者手動縮放
viewport-fit=cover // 縮放以填充滿螢幕
"
>
  • name 設定元資料的名稱, content 設定元資料的值。 name 屬性值為 viewport 時,表示設定有關視口初始大小的提示,僅供移動端使用

  • 同時設定 width=device-width,initial-scale=1.0 是為了相容 iOS 和 IE 瀏覽器

關於頁面縮放

initial-scale 縮放值越大,當前 viewport 的寬度就會越小,反之亦然。

比如螢幕寬度是 320px 的話,如果我們設定 initial-scale=2 ,此時 viewport 的寬度會變為只有 160px 了。這也好理解,放大了一倍嘛,就是原來 1px 的東西變成 2px 了,但是並不是把原來的 320px 變為 640px ,而是在實際寬度不變的情況下, 1px 變得跟原來的 2px 的長度一樣了。

所以縮放頁面的時候,實際上改變了 CSS 畫素的大小,而數量不變。所以原來需要 320px 才能填滿的寬度現在只需要 160px 就做到了。

在開篇的表格中對 CSS 畫素的定義是,不考慮縮放情況下,1個 CSS 畫素等於1個裝置獨立畫素。頁面放大 200% 時,CSS 畫素個數不變,大小變為二倍,相當於一個 CSS 畫素在橫縱向上會覆蓋兩個裝置獨立畫素。瀏覽器視窗可容納的裝置獨立畫素數量是不變的,所以可視區域內 CSS 畫素數量變少。

# Flexible 適配方案及問題

有了上面幾個概念,下面我們來說說 flexible 方案的實現原理及歷史遺留問題。

Flexible 的大致實現思路是,首先根據 dpr 來動態修改 meta 標籤中 viewport 中的 initial-scale 的值,以此來動態改變 viewport 的大小;然後頁面上統一使用 rem 來佈局, viewport 寬度變化會動態影響 html 中的 font-size 值,以此來實現適配。

為什麼不直接引用 flexible 庫來進行移動端適配呢?

因為 lib-flexible 這個庫目前基本被棄用,由於該方案誕生較早,官方也是認為 flexible 已經完成了它的歷史使命,比如當時它只處理了 iOS 不同 dpr的場景,安卓裝置下預設都設定為 dpr = 1 等,這是有問題的。有關於這方面的詳細使用可以閱讀早期整理的文章 ⇲使用Flexible實現手淘H5頁面的終端適配

在日常的業務場景中,雖然我們不會去使用 flexible 庫,但其實大多還是沿用 flexible 實現的原理來進行移動端適配,並從中進行了一些改進來達到適配的目的。

下面為簡單實現

<head>
<meta
name="viewport"
content="width=device-width,user-scalable=no,initial-scale=1,
minimum-scale=1,maximum-scale=1,viewport-fit=cover"
/>
<script type="text/javascript">
// 動態設定 viewport 的 initial-scale
var viewport = document.querySelector("meta[name=viewport]");
var dpr = window.devicePixelRatio || 1;
var scale = 1 / dpr;
viewport.setAttribute(
"content",
"width=device-width," +
"initial-scale=" +
scale +
", maximum-scale=" +
scale +
", minimum-scale=" +
scale +
", user-scalable=no"
);
// 計算 rem font-size
var clientWidth =
document.documentElement.clientWidth || document.body.clientWidth;
clientWidth > 750 && (clientWidth = 750);
var ft = (clientWidth / 7.5).toFixed(2); // 以750設計稿為例
document.documentElement.style.fontSize = ft + "px";

</script>
</head>

為什麼頁面縮放比例 initial-scale 設定為 1 / dpr ?

這也是為什麼前文大篇幅去闡述裝置畫素比 dpr 、縮放等概念了。

通過設定頁面縮放比例為 1/dpr ,可將 viewport 的寬度擴大 dpr 倍。還是以 iPhone6 手機為例,不進行頁面縮放時 viewport 寬度 375pxdpr=2 。由於 dpr 的存在使得一個 CSS 畫素需要兩個物理畫素來渲染。

當設定 initial-scale = 1 / dpr = 0.5 時,獲取到的 viewport 寬度 clientWidth = 750px ,被擴大了 dpr 倍,就正好是裝置物理畫素的寬度。簡單推導一下就是,當 scale=0.5 時,由於 viewport 內可容納的 CSS 畫素數量的增多,相當於一個裝置獨立畫素在橫縱向上會覆蓋兩個 CSS 畫素,

CSS畫素個數 =  裝置獨立畫素個數 /  scale   = ( 物理畫素個數 / dpr )/ scale
scale = 1 / dpr
// 所以
CSS畫素個數 = 物理畫素個數

此時我們寫的 1px 其實正好是一個物理畫素的大小,並且可以較好的畫出 1px 的邊框,從而提高顯示精度,從此我們就可以愉快地直接寫 1px 啦!同時這個方案也較好的解決了只使用 rem 進行佈局時,出現計算後的各種 0.5px、0.55px 等問題。完美~

# 實戰對比

下面為 border 的幾種實現方式在不同測試機的對比圖。

測試機型為 iPhone6iPhone6PlusiPhoneXRHUAWEI P30 ,以 750 的設計稿為例,設定 fontSize = clientWidth / 7.5 + 'px'

4.1 首先採用第一種解決方案即 縮放的形式

:negative_squared_cross_mark: 按鈕1:直接寫 1px ,根據 dpr 計算可知,效果總是偏粗的。

:negative_squared_cross_mark: 按鈕2:rem 佈局下,不改變 viewport 的縮放比,即 initial-scale= 1fontSize 計算後範圍在 48px~55px 不等, 0.01rem 計算後 iPhone60.5pxdpr=2 ,顯示效果較好;而 Huawei P300.01rem = 0.48px ,此時邊框已經展示不清楚了。總體來說效果展示偏細。

:heavy_check_mark: 按鈕3:也是 rem 佈局下, initial-scale= 1使用了第一種解決方案, 縮放 0.5 後,總體效果展示較好。

4.2 採用第二種解決方案,即動態設定 佈局方式的對比圖

設定 rem 佈局下,

:heavy_check_mark: 按鈕1:直接寫 1px

:heavy_check_mark: 按鈕4:使用 0.01rem ,不同機型 fontSize100px~144px 之間, 0.01rem 計算後基本都大於等於 1px

分析下計算過程:

  • iPhone6 螢幕寬 375pxdpr = 2initial-scale= 0.5 時, clientWidth 變為 750px ,根元素 fontSize = 100px ,那麼 0.01rem 正好等於 1px ,並且大小和設計稿一致,展示效果理論上應該是最好的。

  • Huawei P30 螢幕寬 360pxdpr = 3initial-scale= 0.3 時, clientWidth 變為 1080px ,根元素 fontSize = 144px ,那麼 0.01rem = 1.44px 。其實這個時候我們實現的已經大於 1px 了。相當於大於1個物理畫素來渲染。

兩種方式效果展示都比較好,說明在此方案下,我們可以直接寫 1px 或者 0.01rem 都是可以的。

# 一些手機的螢幕解析度整理

裝置型號 螢幕解析度/物理畫素px 裝置畫素比dpr 獨立畫素   /CSS畫素 螢幕尺寸-inch 畫素密度ppi
iPhone4 640x960 2 320x480 3.5 326
Iphone5s 640x1136 2 320x568 4 326
Iphone6 750x1334 2 375x667 4.7 326
iphone6 Plus 1080x1920   (1242x2208) 3 414x736 5.5 401
iphoneX 1125x2436 3 375x812 5.8 458
iphoneXR 828x1792 2 414x896 6.1 326
iphoneXs Max 1242x2688 3 414x896?? 6.5 458
Huawei P30 1080x2340 3 360x780 6.1 422
Huawei mate30 1080x2340 3 360x780 6.62 388

# 總結

移動端適配主要就分為兩方面,一方面要適配不同機型的螢幕尺寸,一方面是對細節畫素的處理過程。如果你在專案中直接寫了 1px ,由於 dpr 的存在展示導致渲染偏粗,其實是不符合設計稿的要求。

如果你使用了 rem 佈局計算出了對應的小數值,不同手機又有明顯的相容性問題。此時老專案的話整體修改 viewport 成本過高,可以採用第一種實現方案進行 1px 的處理;新專案的話可以採用動態設定 viewport 的方式,一鍵解決所有適配問題。

其實移動端對 1px 的渲染適配實現起來配置簡單、程式碼簡短,能夠快速上手。但文字通過大量篇幅去闡述其原理,旨在能夠對其真正理解並徹底解惑,希望能夠對大家有所幫助。

參考

⇲移動前端開發之viewport的深入理解

⇲使用Flexible實現手淘H5頁面的終端適配 ( ⇲Git地址 )

⇲剖析 iOS 11 網頁適配問題

⇲viewports剖析

⇲再聊移動端頁面的適配

⇲再談Retina下1px的解決方案

作者: SDYZ
https://juejin.cn/post/6870691193353666568

- EOF -

覺得本文對你有幫助?請分享給更多人

關注「大前端技術之路」加星標,提升前端技能

點贊和在看就是最大的支援 :heart: