1. 程式人生 > >移動 Web 適配:rem

移動 Web 適配:rem

提到rem,大家首先會想到的是em,px,pt這類的詞語,大多數人眼中這些單位是用於設定字型的大小的,沒錯這的確是用來設定字型大小的,但是對於rem來說它可以用來做移動端的響應式適配哦。

01相容性

先看看相容性,關於移動端

  • ios:6.1系統以上都支援

  • android:2.1系統以上都支援

大部分主流瀏覽器都支援,可以安心的往下看了。

02rem設定字型大小

rem是(font size of the root element),官方解釋

意思就是根據網頁的根元素來設定字型大小,和em(font size of the element)的區別是,em是根據其父元素的字型大小來設定,而rem是根據網頁的跟元素(html)來設定字型大小的,舉一個簡單的例子,

現在大部分瀏覽器IE9+,Firefox、Chrome、Safari、Opera ,如果我們不修改相關的字型配置,都是預設顯示font-size是16px即

html { font-size:16px;}

那麼如果我們想給一個P標籤設定12px的字型大小那麼用rem來寫就是

p { font-size: 0.75rem; //12÷16=0.75(rem)}

基本上使用rem這個單位來設定字型大小基本上是這個套路,好處是假如使用者自己修改了瀏覽器的預設字型大小,那麼使用rem時就可以根據使用者的調整的大小來顯示了。 但是rem不僅可以適用於字型,同樣可以用於width height margin這些樣式的單位。下面來具體說一下

03rem進行螢幕適配

在講rem螢幕適配之前,先說一下一般做移動端適配的方法,一般可以分為:

  1. 簡單一點的頁面,一般高度直接設定成固定值,寬度一般撐滿整個螢幕。

  2. 稍複雜一些的是利用百分比設定元素的大小來進行適配,或者利用flex等css去設定一些需要定製的寬度。

  3. 再複雜一些的響應式頁面,需要利用css3的media query屬性來進行適配,大致思路是根據螢幕不同大小,來設定對應的css樣式。

上面的一些方法,其實也可以解決螢幕適配等問題,但是既然出來的rem這個新東西,也一定能兼顧到這些方面,下面具體使用rem:

rem適配

先看一個簡單的例子:

.con { width: 10rem; height: 10rem; background-color: red; }<div class="con"></div>

這是一個div,寬度和高度都用rem來設定了,在瀏覽器裡面是這樣顯示的, 可以看到,在瀏覽器裡面width和height分別是160px,正好是16px * 10,那麼如果將html根元素的預設font-size修改一下呢?

html { font-size: 17px;}.con { width: 10rem; height: 10rem; background-color: red; }<div class="con"></div>

再來看看結果:

這時width和height都是170px,這就說明了將rem應用與width和height時,同樣適用rem的特性,根據根元素的font-size值來改變自身的值,由此我們應該可以聯想到我們可以給html設定不同的值,從而達到我們css樣式中的適配效果。

rem數值計算

如果利用rem來設定css的值,一般要通過一層計算才行,比如如果要設定一個長寬為100px的div,那麼就需要計算出100px對應的rem值是 100 / 16 =6.25rem,這在我們寫css中,其實算比較繁瑣的一步操作了。

對於沒有使用sass的工程:

為了方便起見,可以將html的font-size設定成100px,這樣在寫單位時,直接將數值除以100在加上rem的單位就可以了。

對於使用sass的工程:

前端構建中,完全可以利用scss來解決這個問題,例如我們可以寫一個scss的function px2rem即:

@function px2rem($px){ $rem : 37.5px; @return ($px/$rem) + rem;}

這樣,當我們寫具體數值的時候就可以寫成:

height: px2rem(90px);width: px2rem(90px);;

看到這裡,你可能會發現一些不理解的地方,就是上面那個rem:37.5px是怎麼來的,正常情況下不是預設的16px麼,這個其實就是頁面的基準值,和html的font-size有關。

rem基準值計算

關於rem的基準值,也就是上面那個37.5px其實是根據我們所拿到的視覺稿來決定的,主要有以下幾點原因:

  1. 由於我們所寫出的頁面是要在不同的螢幕大小裝置上執行的

  2. 所以我們在寫樣式的時候必須要先以一個確定的螢幕來作為參考,這個就由我們拿到的視覺稿來定

  3. 假如我們拿到的視覺稿是以iphone6的螢幕為基準設計的

  4. iPhone6的螢幕大小是375px,

rem = window.innerWidth / 10

這樣計算出來的rem基準值就是37.5(iphone6的視覺稿),這裡為什麼要除以10呢,其實這個值是隨便定義的,因為不想讓html的font-size太大,當然也可以選擇不除,只要在後面動態js計算時保證一樣的值就可以,在這裡列舉一下其他手機的

  • iphone3gs: 320px / 10 = 32px

  • iphone4/5: 320px / 10 = 32px

  • iphone6: 375px / 10 =37.5px

動態設定html的font-size

現在關鍵問題來了,我們該如何通過不同的螢幕去動態設定html的font-size呢,這裡一般分為兩種辦法

1利用css的media query來設定即

@media (min-device-width : 375px) and (max-device-width : 667px) and (-webkit-min-device-pixel-ratio : 2){ html{font-size: 37.5px;}}

2利用java來動態設定 根據我們之前算出的基準值,我們可以利用js動態算出當前螢幕所適配的font-size即:

document.getElementsByTagName('html')[0].style.fontSize = window.innerWidth / 10+ 'px';

然後我們看一下之前那個demo展示的效果

.con { width: px2rem(200px); height: px2rem(200px); background-color: red;}<div class="con"></div>document.addEventListener('DOMContentLoaded', function(e) { document.getElementsByTagName('html')[0].style.fontSize = window.innerWidth / 10 + 'px';}, false);

iPhone6下,正常顯示200px

在iphone4下,顯示169px

由此可見我們可以通過設定不同的html基礎值來達到在不同頁面適配的目的,當然在使用js來設定時,需要繫結頁面的resize事件來達到變化時更新html的font-size。

04rem適配進階

我們知道,一般我們獲取到的視覺稿大部分是iphone6的,所以我們看到的尺寸一般是雙倍大小的,在使用rem之前,我們一般會自覺的將標註/2,其實這也並無道理,但是當我們配合rem使用時,完全可以按照視覺稿上的尺寸來設定。

1 設計給的稿子雙倍的原因是iphone6這種螢幕屬於高清屏,也即是裝置畫素比(device pixel ratio)dpr比較大,所以顯示的畫素較為清晰。

2 一般手機的dpr是1,iphone4,iphone5這種高清屏是2,iphone6s plus這種高清屏是3,可以通過js的window.devicePixelRatio獲取到當前裝置的dpr,所以iphone6給的視覺稿大小是(*2)750×1334了。

3 拿到了dpr之後,我們就可以在viewport meta頭裡,取消讓瀏覽器自動縮放頁面,而自己去設定viewport的content例如(這裡之所以要設定viewport是因為我們要實現border1px的效果,加入我給border設定了1px,在scale的影響下,高清屏中就會顯示成0.5px的效果)

meta.setAttribute('content', 'initial-scale='+ 1/dpr + ', maximum-scale='+ 1/dpr + ', minimum-scale='+ 1/dpr + ', user-scalable=no');

4 設定完之後配合rem,修改

@function px2rem($px){ $rem : 75px; @return ($px/$rem) + rem;}

雙倍75,這樣就可以完全按照視覺稿上的尺寸來了。不用在/2了,這樣做的好處是:

  1. 解決了圖片高清問題。

  2. 解決了border 1px問題(我們設定的1px,在iphone上,由於viewport的scale是0.5,所以就自然縮放成0.5px)

在iphone6下的例子:

我們使用動態設定viewport,在iphone6下,scale會被設定成1/2即0.5,其他手機是1/1即1.

meta.setAttribute('content', 'initial-scale='+ 1/dpr + ', maximum-scale='+ 1/dpr + ', minimum-scale='+ 1/dpr + ', user-scalable=no');

我們的css程式碼,注意這裡設定了1px的邊框

.con { margin-top: 200px; width: 5.3rem; height: 5.3rem; border-top:1px solid #000;}

在iphone6下的顯示:

在android下的顯示:

05rem進行螢幕適配總結

下面這個網址是針對rem來寫的一個簡單的demo頁面,大家可以在不同的手機上看一下效果

但是rem也並不是萬能的,下面也有一些場景是不適於使用rem的

1 當用作圖片或者一些不能縮放的展示時,必須要使用固定的px值,因為縮放可能會導致圖片壓縮變形等。

  • 此方案僅適用於移動端web
  • 文章底部常見問題說明第四條,筆者已給出一個相當便捷的解決方案。

該方案使用相當簡單,把下面這段已壓縮過的 原生JS(僅1kb,原始碼已在文章底部更新) 放到 HTML 的 head 標籤中即可(注:不要手動設定viewport,該方案自動幫你設定)

<script>!function(e){function t(a){if(i[a])return i[a].exports;var n=i[a]={exports:{},id:a,loaded:!1};return e[a].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var i={};return t.m=e,t.c=i,t.p="",t(0)}([function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=window;t["default"]=i.flex=function(normal,e,t){var a=e||100,n=t||1,r=i.document,o=navigator.userAgent,d=o.match(/Android[\S\s]+AppleWebkit\/(\d{3})/i),l=o.match(/U3\/((\d+|\.){5,})/i),c=l&&parseInt(l[1].split(".").join(""),10)>=80,p=navigator.appVersion.match(/(iphone|ipad|ipod)/gi),s=i.devicePixelRatio||1;p||d&&d[1]>534||c||(s=1);var u=normal?1:1/s,m=r.querySelector('meta[name="viewport"]');m||(m=r.createElement("meta"),m.setAttribute("name","viewport"),r.head.appendChild(m)),m.setAttribute("content","width=device-width,user-scalable=no,initial-scale="+u+",maximum-scale="+u+",minimum-scale="+u),r.documentElement.style.fontSize=normal?"50px": a/2*s*n+"px"},e.exports=t["default"]}]);  flex(false,100, 1);</script>

程式碼原理

這是阿里團隊的高清方案佈局程式碼,所謂高清方案就是利用rem的特性(我們知道預設情況下html的1rem = 16px),根據裝置螢幕的DPR(裝置畫素比,又稱DPPX,比如dpr=2時,表示1個CSS畫素由4個物理畫素點組成)根據裝置DPR調整頁面的壓縮比率(即:1/dpr),同時動態設定 html 的font-size為(50 * dpr),進而達到高清效果

有何優勢

  • 引用簡單,佈局簡便
  • 根據裝置螢幕的DPR,自動設定最合適的高清縮放。
  • 保證了不同裝置下視覺體驗的一致性。(老方案是,螢幕越大元素越大;此方案是,螢幕越大,看的越多)
  • 有效解決移動端真實1px問題(這裡的1px 是裝置螢幕上的物理畫素)

如何使用

重要的事情說三遍!
絕不是每個地方都要用rem,rem只適用於固定尺寸!
絕不是每個地方都要用rem,rem只適用於固定尺寸!
絕不是每個地方都要用rem,rem只適用於固定尺寸!
在相當數量的佈局情境中(比如底部導航元素平分螢幕寬,大尺寸元素),你必須使用百分比或者flex才能完美佈局!
看過 《手機端頁面自適應解決方案—rem佈局》的朋友,應該對rem有所瞭解,這裡不再贅述,
此方案也是預設 1rem = 100px,所以你佈局的時候,完全可以按照設計師給你的效果圖寫各種尺寸啦。
比如你在效果圖上量取的某個按鈕元素長 55px, 寬37px ,那你直接可以這樣寫樣式:

.myBtn {
   width: 0.55rem;
   height: 0.37rem;
}

rem佈局(進階版)實踐應用

iPhone5 下頁面效果.png

iPhone 6 Plus 下頁面效果.png

許多同學對該方案存在不少誤解導致使用出現各種問題,這裡統一回復下。

1.問:為啥手機網頁效果圖寬度是要640或者750的,我非得弄個666的不行咩?

答:老實說當然可以,不過為了規範,640或者750是相對合適的。
拿Iphone 5s 舉例,它的css畫素寬度是320px,由於它的dpr=2,所以它的物理畫素寬度為320 × 2 = 640px,這也就是為什麼,你在5s上截了一張圖,在電腦上開啟,它的原始寬度是640px的原因。
那 iphone 6 的截圖寬度呢? 375 × 2 = 750
那 iphone 6 sp 的截圖寬度呢? 414 × 3 = 1242
以此類推,你現在能明白效果圖為什麼一般是 640 ,750 甚至是 1242 的原因了麼?(真沒有歧視安卓機的意思。。。)

2.問:寬度用rem寫的情況下, 在 iphone6 上沒問題, 在 iphone5上會有橫向滾動條,何解?

答:假設你的效果圖寬度是750,在這個效果圖上可能有一個寬度為7rem(高清方案預設 1rem = 100px)的元素。我們知道,高清方案的特點就是幾乎完美還原效果圖,也就是說,你寫了一個寬度為 7rem 的元素,那麼在目前主流移動裝置上都是7rem。然而,iphone 5 的寬度為640,也就是6.4rem。於是橫向滾動條不可避免的出現了。
怎麼辦呢? 這是我目前推薦的比較安全的方式:如果元素的寬度超過效果圖寬度的一半(效果圖寬為640或750),果斷使用百分比寬度,或者flex佈局。就像把等屏寬的圖片寬度設為100%一樣。

3.問:不是 1rem = 100px嗎,為什麼我的程式碼寫了一個寬度為3rem的元素,在電腦端的谷歌瀏覽器上寬度只有150px?

答:先說高清方案程式碼,再次強調咱們的高清方案程式碼是根據裝置的dpr動態設定html 的 font-size,
如果dpr=1(如電腦端),則html的font-size為50px,此時 1rem = 50px
如果dpr=2(如iphone 5 和 6),則html的font-size為100px,此時 1rem = 100px
如果dpr=3(如iphone 6 sp),則html的font-size為150px,此時 1rem = 150px
如果dpr為其他值,即便不是整數,如3.4 , 也是一樣直接將dpr 乘以 50 。

再來說說效果圖,一般來講,我們的效果圖寬度要麼是640,要麼是750,無論哪一個,它們對應裝置的dpr=2,此時,1 rem = 50 × 2 = 100px。這也就是為什麼高清方案預設1rem = 100px。而將1rem預設100px也是好處多多,可以幫你快速換算單位,比如在750寬度下的效果圖,某元素寬度為53px,那麼css寬度直接設為53/100=0.53rem了。

然而極少情況下,有設計師將效果圖寬定為1242px,因為他手裡只有一個iphone 6 sp (dpr = 3),設計完效果圖剛好可以在他的iphone 6 sp裡檢視調整。一切完畢之後,他將這個效果圖交給你來切圖。由於這個效果圖對應裝置的dpr=3,也就是1rem = 50 × 3 = 150px。所以如果你量取了一個寬度為90px的元素,它的css寬度應該為 90/150=0.6rem。由於咱們的高清方案預設1rem=100px,為了還原效果圖,你需要這樣換算。當然,一個技巧就是你可以直接修改咱們的高清方案的預設設定。在程式碼的最後 你會看到 flex(false, 100, 1) ,將其修改成flex(false, 66.66667, 1)(感謝簡友:V旅行指出此處錯誤! 2017/3/24)就不用那麼麻煩的換算了,此時那個90px的直接寫成0.9rem就可以了。

4.問:在此方案下,我如果引用了別的UI庫,那些UI庫的元素會顯得特別小,如何解決?

答:可以這樣去理解問題的原因,如果不用高清方案,別的UI庫的元素在移動裝置上(假設這個裝置是iphone 5好了)顯示是正常的,這沒有問題,然後我們在這個裝置上將該頁面截圖放到電腦上看,發現寬度是640(問答1解釋過了),根據你的畫素眼大致測量,你發現這個裝置上的某個字型大小應該是12px,而你在電腦上測量應該是24px。

現在我們使用高清方案去還原這個頁面,那麼字型大小應該寫為 0.24rem 才對!

所以,如果你引用了其他的UI庫,為了相容高清方案,你需要對該UI庫裡凡是應用px的地方做相應處理,即: a px => a*0.02 rem
(具體處理方式因人而異,有模組化開發經驗的同學可使用類似的 px2rem 的外掛去轉化,也可以完全手動處理)

然而真實情況往往更為複雜,比如,你引入了百度地圖(N個樣式需要處理轉換);或者你引入了一個
framework;又或者你使用了 video 標籤,上面預設的尺寸樣式很難處理。等等這些棘手問題

面對這些情況,此時我們的高清方案如果不再壓縮頁面,那麼以上問題將迎刃而解。
基於這樣的思路,筆者對高清方案的原始碼做了如下修改,即新增一個叫做 normal 的引數,由它來控制頁面是否壓縮。
在文章頂部程式碼的最後,你會看到 flex(false, 100, 1),預設情況下頁面是開啟壓縮的。

如果你需要禁止壓縮,由於我們的原始碼執行後,直接將flex函式掛載到全域性變數window上了,此時你直接在需要禁止壓縮的頁面執行 window.flex(true) 就可以了,而rem的用法保持不變。

有一點美中不足的是,如果禁止了頁面壓縮,高清屏的1畫素就不能實現了,如果你必須要實現1畫素,那麼自行谷歌:css 0.5畫素,有N多的解決方案,這裡不再贅述。

5.問:有時候字型會不受控制的變大,怎麼辦?

答:在X5新核心Blink中,在排版頁面的時候,會主動對字型進行放大,會檢測頁面中的主字型,當某一塊字型在我們的判定規則中,認為字號較小,並且是頁面中的主要字型,就會採取主動放大的操作。然而這不是我們想要的,可以採取給最大高度解決

解決方案:

*, *:before, *:after { max-height: 100000px }

補充:有同學反映,在一些情況下 textarea 標籤內的字型大小即便加上上面的方案,字型也會變大,無法控制。此時你需要給 textareadisplay 設為 table 或者 inline-table 即可恢復正常。(感謝 程式媛喵喵 對此的補充!)

6.問:我在底部導航用的flex感覺更合適一些,請問這樣子混著用可以嗎?

答:咱們的rem適合寫固定尺寸。其餘的根據需要換成flex或者百分比。原始碼示例中就有這三種的綜合運用。

7.問:在高清方案下,一個標準的,較為理想的寬度為640的頁面效果圖應該是怎樣的?

點選瀏覽:一個標準的640手機頁面設計稿參考(沒錯,在此方案中,你可以完全按照這張設計稿的尺寸寫佈局了。就是這麼簡單!)

8.問:用了這個方案如何使用媒體查詢呢?

一般來講,使用了這個方案是沒必要用媒體查詢了,如果你必須要用,假設你要對 iphone5 (css畫素寬度320px,
這裡需要取其物理畫素,也就是640)寬度下的類名做處理,你可以這樣

@media screen and (max-width: 640px) {
    .yourLayout {
        width:100%;
    }
}

9.問:可以提供下這個高清方案的原始碼嗎?

'use strict';

/**
 * @param {Boolean} [normal = false] - 預設開啟頁面壓縮以使頁面高清;  
 * @param {Number} [baseFontSize = 100] - 基礎fontSize, 預設100px;
 * @param {Number} [fontscale = 1] - 有的業務希望能放大一定比例的字型;
 */
const win = window;
export default win.flex = (normal, baseFontSize, fontscale) => {
  const _baseFontSize = baseFontSize || 100;
  const _fontscale = fontscale || 1;

  const doc = win.document;
  const ua = navigator.userAgent;
  const matches = ua.match(/Android[\S\s]+AppleWebkit\/(\d{3})/i);
  const UCversion = ua.match(/U3\/((\d+|\.){5,})/i);
  const isUCHd = UCversion && parseInt(UCversion[1].split('.').join(''), 10) >= 80;
  const isIos = navigator.appVersion.match(/(iphone|ipad|ipod)/gi);
  let dpr = win.devicePixelRatio || 1;
  if (!isIos && !(matches && matches[1] > 534) && !isUCHd) {
    // 如果非iOS, 非Android4.3以上, 非UC核心, 就不執行高清, dpr設為1;
    dpr = 1;
  }
  const scale = normal ? 1 : 1 / dpr;

  let metaEl = doc.querySelector('meta[name="viewport"]');
  if (!metaEl) {
    metaEl = doc.createElement('meta');
    metaEl.setAttribute('name', 'viewport');
    doc.head.appendChild(metaEl);
  }
  metaEl.setAttribute('content', `width=device-width,user-scalable=no,initial-scale=${scale},maximum-scale=${scale},minimum-scale=${scale}`);
  doc.documentElement.style.fontSize = normal ? '50px' : `${_baseFontSize / 2 * dpr * _fontscale}px`;
};