大家有沒有發現淘寶的H5移動端沒有使用任何rem和vw單位,而是和web端專案一樣,使用的是px單位。雖然是px但它也很完美的將整個頁面渲染了出來。那淘寶的FE是怎麼實現的呢?
最近在研究關於佈局的設計方案,通過學習理解阿里的fusion.design的設計思想並結合手機淘寶H5版的px佈局問題。逐漸有了一些想法,這裡進行綜合整理,也算是拋磚引玉吧。
1、rem和vw
rem和vw都是為了解決移動端適配問題。rem方案中最成功的就是淘寶的lib-flexible了,它是通過javascript將整個佈局分割成10份,從而進行有效佈局。不過有計算dpr的問題,在一些dpr比較怪異的手機上會出現脫相的問題。後來又產生了vw佈局,使用了vw之後,也再無需通過javascript的幫助進行佈局的切分,而是自動的將整個佈局切割為等分的100份,也就是1vw = 1%的頁面寬度。
1.1、 rem的問題
- 在奇葩的dpr裝置上表現效果不太好,比如 一些華為的高階機型 用rem佈局會出現錯亂。
- 設定根字型大小的方式有兩種,一種是媒體查詢,優點:不需要額外使用js去更改html的字型,缺點:不連續,或者說並能完* 全實現對所有裝置的佈局規範統一;
- 另一種是js動態更改html字型,優點:連續;缺點:不如直接寫媒體查詢的體驗好;
- 不支援css3 calc的需要大量密集的 @media hack;
- 使用iframe引用也會出現問題;
- 需要解決在ios上的1px邊框問題,但是這個在lib-flexible中已經解決:(1px變2px, 又被 initial-scale=0.5 縮小了一半
- rem需要引入一個lib庫
- html的font-size設定到12px以下還是會按照12px=1rem來計算,這樣所有使用了rem單位的尺寸都是錯的
1.2、vw的問題
- 支援程度不太好,安卓4.4以下都不支援
1.3、它們共同的問題
- 都需要計算以達到適配的目的
- 額外引入工具,在編譯階段完成轉換
- UI的迴歸測試不友好。畢竟設計稿是px,而頁面是rem或vm。
- 都是相對單位。rem 的比例是可以通過控制 html 字型大小來控制的,而vw的比例是固定的。
- 無法和web專案共用統一套工程化方案,因為web專案不需要使用rem和vw單位。
2、移動端佈局的初衷
- 可以輕鬆搞定任意佈局
- 通過設計稿,可以讓應用在不同的裝置上有完美的體驗效果
雖然rem和vw可以很好完成它們的初衷,不過同時它們也是有代價的(就是它們存在的問題)。那有沒有一種方案可以規避掉以上rem和vw的問題又可以很好的完成初衷哪?
2.1、一個新的Units單位(該小節摘自https://fusion.design/design/doc/16)
DP為UI設計中的唯一可用單位
由於DP在不同裝置中的叫法不同,且用於描述字號的單位有所不同(如SP,PT),但其基本計算方法和原則相同且通用,所以在設計過程中,我們考慮到嚴謹性,統一採用只寫數字不帶單位的方法書寫。
選用DP的原因
畫素密度PPI:指每英寸包含的畫素(Px)個數
如圖同一物理尺寸(肉眼所見尺寸)下,低密度顯示器的畫素個數明顯小於高密度顯示器的畫素個數,所以畫素(PX)在多變的裝置和解析度下不是一個穩定可用的單位
與密度無關的畫素(DP):裝置獨立畫素
如圖,DP與PX的對比可見,DP可以自適應螢幕的密度,不管螢幕密度怎麼變化,實際顯示的物理尺寸相同,DP可以保證物理尺寸的一致性,DP是目前最適合UI設計的單位,同時也是使程式碼語言相通的尺寸。
轉換公式 當螢幕的PPI為160時,1DP=1PX;例:Iphone4,Iphone5,Iphone6,PPI為326,在這些螢幕之下1DP=2PX
DP=(PX160)/PPI PX=DP(PPI/160)
切圖規則 DP是與開發程式碼共用的語言,但一些需要置入的jpg,png等圖片非向量,依舊採用px作為單位,這個時候我們需要將圖片適配到不同PPI的螢幕中去。
圖示,為一塊banner適配到不同解析度螢幕時的畫素值:
但實際場景中,無法為各種螢幕做切圖適配,我麼遵循大圖可壓縮小,小圖不可變大的原則:
- 【Mobile】選擇3x圖輸出,適配於ios和andirod
- 【Web】選擇2x的圖輸出,適配普通螢幕和retina螢幕
畫布設定規則
- 【Mobile】選擇375*667作為繪圖尺寸
- 【Andriod】 選擇360*640作為繪圖尺寸
- 【Web】使用1440寬作為繪圖尺寸
3、具體實現
主要思路
- 設計稿中統一使用dp作為畫素單位,具體規則參考上面的
切圖規則
和畫布設計規則
- rem和vw多多少少存在各種問題,所以統一使用px作為實現單位
- web和wap可以使用同一套工程方案
- 實現設計稿的dp到真實應用中px的對映關係,並且px會隨著裝置視窗大小的改變而改變
當然,如果稿子是px的也可以手動將px轉換為dp。
想要實現這個整體方案,核心就在於第4條(實現設計稿的dp到真實應用中px的對映關係),並且這個過程只靠工程化的編譯階段是無法完美解決的,必須和瀏覽器執行時一起配合工作才能夠達成我們的目標。
前提
- 業務模組的css不可以抽離為獨立的css檔案,必須輸出在js檔案中(style-loader的能力),這樣才有改變css內容的基本能力。
- 定義一個尺寸單位
dp
,標識這是在設計稿上的尺寸(類似於小程式中的rpx
)。 - 並不是所有的px都需要做彈性轉換的,對於需要做彈性轉換的容器的px統一改為
dp
,否則繼續使用px。
假設我們根據Mobile設計稿定義一個移動端H5的容器元素:
<div class="box">
<div class="tip">this is tip</div>
</div>
複製程式碼
.box {
/* 這裡使用的單位為dp,表示需要根據裝置大小進行彈縮 */
width: 100dp;
height: 150dp;
background: red;
}
.box .tip {
/* 使用的單位為px,不需要根據裝置大小進行彈縮。無論裝置怎麼變化,該元素的寬高都是10畫素。 */
width: 10px;
height: 10px;
background: blue;
}
複製程式碼
最終,元素.box
會根據裝置的寬高的改變而改變自身的大小,下方就是.box
元素在不同裝置下的寬和高:
裝置 | 寬度 | 高度 |
---|---|---|
設計稿 | 100dp | 150dp |
iPhone 5/SE | 85.33px | 128px |
iPhone 6/7/8 | 100px | 150px |
iPhone 6/7/8 Plus | 110.4px | 165.60px |
iPhone X | 100px | 150px |
Galaxy S5 | 96px | 144px |
在實現這個功能必須先提供一個轉換dp為px的幫助函式:unitParser。因為接下來的兩種方式中都需要這個函式來幫助我們實現最終目的。
const allowMiniPixel = () => {
let allow = false;
if (window.devicePixelRatio && devicePixelRatio >= 2) {
let ele = document.createElement("div");
ele.style.border = ".5px solid transparent";
document.body.appendChild(ele);
allow = 1 === ele.offsetHeight;
document.body.removeChild(ele);
}
return allow;
}();
function unitParser(unit) {
let type = void 0 === unit ? "undefined" : getType(unit);
if ("number" === type) {
unit += "dp"
}
if ("string" !== type) {
return unit;
}
let regExp = /^([\d\.]+)(np|dp)?$/g;
return unit.replace(regExp, (chars, count, suffix) => {
count = Number(count)
switch (suffix) {
case "np":
// np不做轉換。1np就是1px 100np就是100px
break;
case "dp":
default:
// 注意這裡375。說明的上文說了,設計稿是按照iphone 6的375進行設計的。
// deviceWidth為螢幕的寬度。iphone 5/SE為320、iphone 6/7/8為375
count = count / 375 * deviceWidth
};
if (!allowMiniPixel && count < 1) {
count = 1
}
return count + "px";
})
}
複製程式碼
3.2.1、方式一:styled-components + css-in-js + JSX
Vue:
import styled from 'vue-emotion'
import unitParser from './unitParser.js';
const box = styled('div')`
width: ${unitParser("100dp")};
height: ${unitParser("150dp")};
background: red;
`
const tip = styled(div)`
width: 10px;
height: 10px;
background: blue;
`
new Vue({
render() {
return (
<box>
<tip>this is a tip</tip>
</box>
)
}
})
複製程式碼
react:
import styled from 'styled-components';
import unitParser from './unitParser.js';
const Box = styled.div`
width: ${unitParser("100dp")};
height: ${unitParser("150dp")};
background: red;
`
const Tip = styled.div`
width: 10px;
height: 10px;
background: red;
`
render(
<Box>
<Tip>this is a tip</Tip>
</Box>
)
複製程式碼
注意: 使用此方案需要注意和sass、post-css等工具的結合使用問題,會增加一定的工程複雜度。另外這種方案會產生大量的元素style屬性,導致dom複雜度增加。
3.2.2、方式二:在瀏覽器執行時動態計算
自定義一個webpack的css-loader,進行unitParser處理。
function styleInject(css, ref) {
if ( ref === void 0 ) ref = {};
var insertAt = ref.insertAt;
if (!css || typeof document === 'undefined') { return; }
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css = ".box {width: "
+ unitParser("100dp")
+ "; height: "
+ unitParser("150dp")
+ "; background: red;} .box .tip {width: 10px; height:10px; background: blue}";
styleInject(css);
複製程式碼
3.2.3、優缺點
優點:
- 文章開頭所提到的rem和vw都存在眾多問題,該方案都可以完美解決。
- 還可以和其他任何單位混合使用,這意味這使用這種方案的同時還可以使用之前的rem和vw方式。
缺點:
- 不管使用方式一還是方式二,都會對專案的工程化複雜度增加,不過和目前處理rem以及vm的px2rem以及px2viewport等工具相比,這點複雜度根本不值一提。
- 增加了js bundle檔案的體積,減少了css檔案的體積。不過沒有整體的體積沒有增加,僅僅是將部分業務的css內容搬到了js檔案裡。
最後
- 借用了大部分工程化的能力,主要還是用來css in js的能力
- 但是這樣有個缺點。就是視窗大小改變的時候會出現佈局錯亂。不過影響不大,因為真機裡是不會出現視窗大小改版的。而且這一套方案完全抹平了web和wap的差異性,在開發層面完全一直。需要縮放就用dp,不需要就用np和px作為單位即可
- 這套方案還是蠻不錯的,比rem和vw好很多。確實屬於第三代移動端佈局方案了