在討論適配方案之前,先熟悉幾個概念:
設備像素/物理像素:設備實際物理像素點,是顯示設備中一個最微小的物理部件,每個像素可以根據操作系統設置自己的顏色和亮度。任何設備的物理像素的數量都是固定的。例如(iphone6 750 * 1337) (iphone6plus 1960 * 1080)。注意:設備像素不等於CSS像素
像素密度:屏幕上每英寸可以顯示的像素點的數量,單位是PPI
屏幕分辨率:設備屏幕橫縱向上的物理像素點數的乘積。
CSS像素:CSS像素是一個抽象的單位,主要使用在瀏覽器上,用來精確的度量(確定)Web頁面上的內容。CSS像素被稱爲與設備無關的像素(device-independent像素),簡稱爲“DIPs”,單位px
設備獨立像素(PD/PID):密度無關像素,表示用幾個實際物理像素表示的一個虛擬像素,這個虛擬像素就叫設備獨立像素,如(iphone6 375 * 667) (iphone6P 414 * 736) (iphoneX 375 * 812)。實際開發中常可以認爲“ 設備獨立像素=CSS像素 ”
設備像素比(dpr):設備物理像素與設備獨立像素的比例,可以在JS中獲取window.devicePixelRatio。現在的智能手機dpr一般都大於1,例如iphone6/7/8的dpr=2
佈局視口(layout viewport):網頁佈局的基準窗口,PC瀏覽器上,佈局視口就等於當前瀏覽器的窗口大小(不包括 borders 、 margins、滾動條);移動端,佈局視口被賦予一個默認值,大部分爲 980px。document.documentElement.clientWidth/clientHeight獲取佈局視口的寬高。通過在頁面中添加meta的viewport width值設置佈局視口的尺寸,一般爲device-width
視覺視口(visual viewport):用戶通過屏幕真實看到的區域,默認等於當前瀏覽器的窗口大小。通過window.innerWidth/innerHeight來獲取視覺視口大小
理想視口(ideal viewport):網站頁面在移動端展示的理想大小,上面說到的375 * 667指的就是理想視口尺寸。當頁面不進行縮放時,視覺視口=理想視口。
移動端適配解決的就是讓佈局視口和視覺視口無限接近於理想視口
HTML標籤的viewport說明:
- width決定佈局視口的寬度,device-width是理想視口的寬度,設置 width=device-width就相當於讓佈局視口等於理想視口;
- initial-scale=理想視口寬度/視覺視口寬度,設置 initial-scale=1 就相當於讓視覺視口等於理想視口。此時,1個 CSS像素=1個設備獨立像素;如果initial-scale=0.5,且屏幕dpr=2,此時1個CSS像素=1個物理像素
- width取理想視口和視覺視口中較大的值,如果initial-scale<1(即頁面進行縮小),視覺視口大於理想視口,則width的值是取決於視覺視口
方案一:flexible + rem
- rem:相對html文檔根節點字體大小的相對單位。Chrome瀏覽器默認字體大小爲16px,即1rem=16px。如果設置html字體大小爲20px,則1rem=20px;
- 實際開發中爲了方便計算,直接設置1rem=10px。對於iphone6/7/8(750 * 1337)而言,物理像素是750,整個屏幕寬度就是750=10rem,html的font-size=75px
適配步驟:
- 安裝lib-flexible
$ cnpm i lib-flexible --save-dev
//main.js
import 'lib-flexible/flexible';
或者直接在html文件中通過淘寶cdn引入
<script src="http://g.tbcdn.cn/mtb/lib-flexible/0.3.4/??flexible_css.js,flexible.js"></script>
flexible會根據當前設備動態計算:
- 動態改寫標籤,Retina屏(initial-scale=0.5),非Retina屏(initial-scale=1),flexible只對iPhone進行適配
var devicePixelRatio = win.devicePixelRatio;
if (isIPhone) {
// iOS下,對於2和3的屏,用2倍的方案,其餘的用1倍方案
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
dpr = 2;
} else {
dpr = 1;
}
} else {
// 其他設備下,仍舊使用1倍的方案
dpr = 1;
}
var scale = 1 / dpr;
---
var scale = isRetina ? 0.5:1;
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
- 給元素添加data-dpr屬性,並且動態改寫data-dpr的值;
- 給元素添加font-size屬性,並且動態改寫font-size的值。
- 把css中的px全部改寫成rem
- 利用Sass函數修改px;
- 利用PostCSS的px2rem動態編譯px到rem
//1,安裝px2rem-loader
$ cnpm install px2rem-loader --save-dev
//2,將px2rem-loader添加到cssLoaders中(build/util.js)
const px2remLoader = {
loader: 'px2rem-loader',
options: {
remUnit: 75
}
}
//同時在generateLoaders方法中添加px2remLoader
function generateLoaders (loader, loaderOptions) {
const loaders = [cssLoader, px2remLoader];
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
}
原理:
- 動態獲取設備屏幕佈局視口的寬度,將html的font-size設置成width/10。例如iphone6的佈局視口寬度是375,則1rem=37.5px
let rem = document.documentElement.getBoundingClientRect().width / 10;
document.documentElement.style.fontSize= rem + 'px';
- 動態獲取設備屏幕的dpr(設備像素比),設置標籤中頁面的放縮比(initial-scale、maximum-scale和minimum-scale的值)
window.devicePixelRatio
FAQ
文本在Retina屏幕下變小,出現13px等奇數像素問題
文本不要採用rem作單位,而應該採用px作單位,使用[data-dpr]屬性來區分不同dpr下的文本字號大小
//定義混入
@mixin font-dpr($font-size){
font-size: $font-size;
[data-dpr="2"] & {
font-size: $font-size * 2;
}
[data-dpr="3"] & {
font-size: $font-size * 3;
}
}
//使用混入
@include font-dpr(16px);
1px(物理像素)邊框特殊處理
設計師要的1px是一個物理像素,也就是所能設計出來的最細的線,而css中的1px是一個點,dpr爲2的話,那麼就是兩個物理像素了,這就不是設計師要的細線了。也就是1px的border不需要自適應,所以不能用rem,只能用px。蘋果支持0.5px,但是Android不支持,0.5px會被直接忽略。
解決方法:
- 藉助 PostCSS的 postcss-write-svg 使用 border-image 和 background-image 創建 svg 的 1px邊框
border_1px {
height: 2px;
@rect {
fill: var(--color, black);
width: 100%;
height: 50%;
}
}
.example { border: 1px solid transparent; border-image: svg(border_1px param(--color #00b1ff)) 2 2 stretch; }
- 動態修改scale的值,實現頁面放縮(flexible的設計原理)
const scale = 1 / window.devicePixelRatio;
const viewport = document.querySelector('meta[name="viewport"]');
viewport.setAttribute('content', 'width=device-width,user-scalable=no,initial-scale=' + scale +',maximum-scale='+scale + ',minimum-scale=' + scale);
方案二:viewport
vh、vw方案即將視覺視口寬度 window.innerWidth和視覺視口高度 window.innerHeight 等分爲 100 份。如果視覺視口爲 375px,那麼 1vw=3.75px,這時 UI給定一個元素的寬爲 75px(設備獨立像素),我們只需要將它設置爲 75/3.75=20vw。
適配步驟:
- 設置標籤
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
佈局視口=視覺視口=理想視口,此時對於dpr=2的屏幕,1個CSS像素=1個設備獨立像素=2個物理像素。建議UI設計按照設備獨立像素來
2. CSS中的所有px轉換成vw實現自適應
藉助 PostCSS 的 postcss-px-to-viewport 實現px到vw的換算。這樣一來,我們可以在css中依然採用px設置元素尺寸,插件會自動把px轉換成vw。
//1,安裝postcss-px-to-viewport
$ cnpm install postcss-px-to-viewport postcss-write-svg postcss-cssnext postcss-aspect-ratio-mini --save-dev
//2,根目錄下創建.postcssrc.js PostCSS配置文件
module.exports = {
"plugins": {
"postcss-px-to-viewport": {
unitToConvert: "px", // 默認值`px`,需要轉換的單位
viewportWidth: 375, // 視窗的寬度,對應設計稿寬度(按照設備獨立像素的設計稿,如果設計稿是750,此處也應該是750)
viewportHeight: 667, // 視窗的高度, 根據375設備的寬度來指定,一般是667,也可不配置
unitPrecision: 3, // 指定`px`轉換爲視窗單位值的小數位數,默認值是5
propList: ["*"], // 轉化爲vw的屬性列表
viewportUnit: "vw", // 指定需要轉換成視窗單位
fontViewportUnit: "vw", // 字體使用的視窗單位
selectorBlaskList: [".ignore-"], // 指定不需要轉換爲視窗單位的類
mediaQuery: false, // 允許在媒體查詢中轉換`px`
minPixelValue: 1, // 小於或等於`1px`時不轉換爲視窗單位
replace: true, // 是否直接更換屬性值而不添加備用屬性
exclude: [], // 忽略某些文件夾下的文件或特定文件,如node_modules
landscape: false, // 是否添加根據landscapeWidth生成的媒體查詢條件 @media (orientation: landscape)
landscapeUnit: "vw", // 橫屏時使用的單位
landscapeWidth: 1134 // 橫屏時使用的視窗寬度
},
"postcss-write-svg": { //用來處理1pxborder的問題
uft8: false
},
"postcss-cssnext": {}, //自動添加樣式前綴
"postcss-aspect-ratio-mini": {}, //用來處理元素容器寬高比
}
}
缺陷:
- px轉換成 vw不一定能完全整除,會存在一定的計算誤差;
- 容器設置margin時採用px會造成元素溢出,可以用padding代替margin
適配iphoneX
適配iPhoneX底部小黑條:
- meta的viewport屬性viewport-fit設置成cover,即網頁內容完全覆蓋可視窗口;
- body設置padding-bottom: constant(safe-area-inset-bottom)和padding-bottom: env(safe-area-inset-bottom);
橫屏豎屏適配:
@media screen and (orientation: portrait){
/*豎屏...*/
}
@media screen and (orientation: landscape){
/*橫屏...*/
}