mobile web適配總結

    開門見山,本篇將總結一下 MobileWeb 的適配方法,即咱們常說的H5頁面、手機頁面、WAP頁、webview頁面等等。

本篇討論的頁面指專門針對手機設備設計的頁面,並不是兼容全設備的響應式佈局。 文中提到的 device-width 指 viewport meta 標籤中 width 的值,即由瀏覽器指定的值,經常使用機型對應值可參照 Screen Sizesjavascript

適配達到的效果是什麼?

在不一樣尺寸的手機設備上,頁面「相對性的達到合理的展現(自適應)」或者「保持統一效果的等比縮放(看起來差很少)」。css

適配應關注哪些要素?

通常來講,咱們須要關注的是:字體、高寬間距、圖像(圖標、圖片)。html

其中,圖像相對要複雜一些,針對流量、清晰度等問題網上也有比較成熟的解決方案,好比:矢量化、字體化、image-set 等等,在此不作深刻。在知足快速開發的需求下,咱們使用較爲偷懶的方式:利用 css 將圖像限定在元素內( img 圖片使用 [max-]width: 100% ,背景圖像使用 background-size ),佈局只針對元素進行前端

另外要考慮到,設計師設計視覺稿時使用什麼樣的寬度,才能既知足設計自身的需求又能讓前端開發方便的切圖適配。java

舉個例子

圍繞這三要素,咱們用一個小例子來講明接下來要介紹的三種方案的實現方式,按 640px 標準需實現的效果如圖:android

技術分享


固定高度,寬度自適應

這是目前最通用的一種作法,屬於自適應佈局,viewport width 設置爲 device-width,以較小寬度(如 320px)的視覺稿做爲參照進行佈局。垂直方向的高度和間距使用定值,水平方向混合使用定值和百分比或者利用彈性佈局,最終達到「當手機屏幕變化時,橫向拉伸或者填充空白的效果」。圖像元素根據容器狀況,使用定值或者 background-size 縮放。git

粗略瀏覽了下一些大廠的首頁,像百度、騰訊、Facebook、Twitter 都是採用的這種方案。github

要點:

  • 以小寬度做爲參照是由於若是佈局知足了小寬度的擺放,當屏幕變寬時,簡單的填充空白就能夠了;而若是反過來就可能形成「擠壞了」,考慮 header 區域,左測 logo 右測橫向 nav 的狀況。
  • 須要小寬度的佈局,又須要大寬度的圖像,這是一個矛盾點。
  • 320px 過於窄小,不利於頁面的設計;只能設計橫向拉伸的元素佈局,存在不少侷限性。
  • 兼容性較好。

實現比較簡單,樣式中的尺寸都按照視覺稿二分之一大小設置web


 

固定寬度,viewport 縮放

視覺稿、頁面寬度、viewport width 使用統一寬度,利用瀏覽器自身縮放完成適配。頁面樣式(包括圖像元素)徹底按照視覺稿的尺寸,使用定值單位 (px、em)便可完成。瀏覽器

優勢:

  • 開發簡單    縮放交給瀏覽器,徹底按視覺稿切圖。
  • 還原精準    絕對等比例縮放,能夠精準還原視覺稿(不考慮清晰度的狀況下)。
  • 測試方便    在PC端便可完成大部分測試,手機端只需酌情調整一些細節(好比圖標、字體混合排列時,由於字體不一樣形成的對齊問題)。

存在的問題:

  • 像素丟失     對於一些分辨率較低的手機,可能設備像素還未達到指定的 viewport 寬度,此時屏幕的渲染可能就不許確了。比較常見的是邊框「消失」了,不過隨着手機硬件的更新,這個問題會愈來愈少的。
  • 縮放失效     某些安卓機不能正常的根據 meta 標籤中 width 的值來縮放 viewport,須要配合 initial-scale 。
  • 文本折行    存在於縮放失效的機型中,某些手機爲了便於文本的閱讀,在文本到達 viewport 邊緣(非元素容器的邊緣)時即進行折行,而當 viewport 寬度被修正後,瀏覽器並無正確的重繪,因此就發現文本沒有佔滿整行。一些經常使用的段落性文本標籤會存在該問題。

縮放失效問題需經過 js 動態設定 initial-scale

var fixScreen = function() { var metaEl = doc.querySelector(meta[name="viewport"]‘), metaCtt = metaEl ? metaEl.content : ‘‘, matchScale = metaCtt.match(/initial\-scale=([\d\.]+)/), matchWidth = metaCtt.match(/width=([^,\s]+)/); if ( metaEl && !matchScale && ( matchWidth && matchWidth[1] != device-width) ) { var width = parseInt(matchWidth[1]), iw = win.innerWidth || width, ow = win.outerWidth || iw, sw = win.screen.width || iw, saw = win.screen.availWidth || iw, ih = win.innerHeight || width, oh = win.outerHeight || ih, ish = win.screen.height || ih, sah = win.screen.availHeight || ih, w = Math.min(iw,ow,sw,saw,ih,oh,ish,sah), scale = w / width; if ( ratio < 1) { metaEl.content += ‘,initial-scale=‘ + ratio + ‘,maximum-scale=‘ + ratio + ‘, minimum-scale=‘ + scale; } } }

文本折行問題能夠經過 css 樣式解決。

section, p, div, h1, h2, h3, h4, h5, h6, .fix-break { background: tranparent url(about:blank); word-break: break-all; }

既然該方案使用固定寬度值,那麼這個值是多少合適呢?首要考慮的是主流分辨率,可參考 Screen Sizes 和 友盟指數 的數據;其次要考慮設計部門經常使用的設計尺寸,綜合協調,最終肯定一個合適的值。

該處使用 640px 來實現例子,


 

利用 rem 佈局

依照某特定寬度設定 rem 值(即 html 的 font-size),頁面任何須要彈性適配的元素,尺寸均換算爲 rem 進行佈局;當頁面渲染時,根據頁面有效寬度進行計算,調整 rem 的大小,動態縮放以達到適配的效果。利用該方案,還能夠根據 devicePixelRatio 設定 initial-scale 來放大 viewport,使頁面按照物理像素渲染,提高清晰度。

優勢:

  • 清晰度高,能達到物理像素的清晰度。
  • 能解決 DPR 引發的「1像素」問題。
  • 向後兼容較好,即使屏幕寬度增長、PPI 增長該方案依舊適用。

缺點:

  • 適配 js 需儘量早進入,減小(避免)viewport 變化引發的重繪。
  • 某些Android機會丟掉 rem 小數部分。
  • 須要預編譯庫進行單位轉換。

開發時,css 及 js 都以 16px 做爲基數換算 rem,藉助預編譯庫(以 scss 爲例),咱們設定一個動態尺寸單位 $ppr: 750px/16px/1rem ,即 pixel per rem,任何使用彈性尺寸的地方寫做:width: 100px/$ppr

動態調整 rem 的方法以下:

var fixScreen = function() { var metaEl = doc.querySelector(meta[name="viewport"]‘), metaCtt = metaEl ? metaEl.content : ‘‘, matchScale = metaCtt.match(/initial\-scale=([\d\.]+)/), matchWidth = metaCtt.match(/width=([^,\s]+)/); if ( !metaEl ) { // REM var docEl = doc.documentElement, maxwidth = docEl.dataset.mw || 750, // 每 dpr 最大頁面寬度 dpr = isIos ? Math.min(win.devicePixelRatio, 3) : 1, scale = 1 / dpr, tid; docEl.removeAttribute(data-mw); docEl.dataset.dpr = dpr; metaEl = doc.createElement(meta); metaEl.name = viewport; metaEl.content = initial-scale=‘ + ratio + ‘,maximum-scale=‘ + ratio + ‘, minimum-scale=‘ + scale; docEl.firstElementChild.appendChild(metaEl); var refreshRem = function() { var width = docEl.getBoundingClientRect().width; if (width / dpr > maxwidth) { width = maxwidth * dpr; } var rem = width / 16; docEl.style.fontSize = rem + px; }; //... refreshRem(); } }

代碼實現主要參考淘寶網觸屏版的適配方法

注意,較小的背景圖(好比一些 icon)的 background-size 不要使用具體 rem 數值,裁剪後會出現邊緣丟失。應使用與元素等尺寸切圖,設定 background-size: contain|cover 來縮放。


總結

總的來看,目前尚未完美的解決方案,這些也只是儘量的知足快速開發、簡單適配需求的通用方案。其中對於一些較爲細節的問題(好比字體的點陣尺寸、非彈性的定值需求)未作討論。實際開發過程當中,更應該綜合考慮項目類型、資源成本、人員配合等多方面的因素,選擇合適的方案。

代碼實現中使用到的 mobile-util.js 對定寬和 rem 適配進行了整合,源碼在此

/**
  * MobileWeb 通用功能助手,包含經常使用的 UA 判斷、頁面適配、search 參數轉 鍵值對。
  * 該 JS 應在 head 中儘量早的引入,減小重繪。
  *
  * fixScreen 方法根據兩種狀況適配,該方法自動執行。
  * 1. 定寬: 對應 meta 標籤寫法 -- <meta name="viewport" content="target-densitydpi=device-dpi,width=750">
  * 該方法會提取 width 值,主動添加 scale 相關屬性值。
  * 注意: 若是 meta 標籤中指定了 initial-scale, 該方法將不作處理(即不執行)。
  * 2. REM: 不用寫 meta 標籤,該方法根據 dpr 自動生成,並在 html 標籤中加上 data-dpr 和 font-size 兩個屬性值。
  * 該方法約束:IOS 系統最大 dpr = 3,其它系統 dpr = 1,頁面每 dpr 最大寬度(即頁面寬度/dpr) = 750,REM 換算比值爲 16。
  * 對應 css 開發,任何彈性尺寸均使用 rem 單位,rem 默認寬度爲 視覺稿寬度 / 16;
  * scss 中 $ppr(pixel per rem) 變量寫法 -- $ppr: 750px/16/1rem;
  * 元素尺寸寫法 -- html { font-size: $ppr*1rem; } body { width: 750px/$ppr; }。
   
  */
  window.mobileUtil = (function(win, doc) {
  var UA = navigator.userAgent,
  isAndroid = /android|adr/gi.test(UA),
  isIos = /iphone|ipod|ipad/gi.test(UA) && !isAndroid, // 聽說某些國產機的UA會同時包含 android iphone 字符
  isMobile = isAndroid || isIos; // 粗略的判斷
   
  return {
  isAndroid: isAndroid,
  isIos: isIos,
  isMobile: isMobile,
   
  isNewsApp: /NewsApp\/[\d\.]+/gi.test(UA),
  isWeixin: /MicroMessenger/gi.test(UA),
  isQQ: /QQ\/\d/gi.test(UA),
  isYixin: /YiXin/gi.test(UA),
  isWeibo: /Weibo/gi.test(UA),
  isTXWeibo: /T(?:X|encent)MicroBlog/gi.test(UA),
   
  tapEvent: isMobile ? ‘tap: ‘click‘,
   
  /**
  * 縮放頁面
  */
  fixScreen: function() {
  var metaEl = doc.querySelector(‘meta[name="viewport"]‘),
  metaCtt = metaEl ? metaEl.content : ‘,
  matchScale = metaCtt.match(/initial\-scale=([\d\.]+)/),
  matchWidth = metaCtt.match(/width=([^,\s]+)/);
   
  if ( !metaEl ) { // REM
  var docEl = doc.documentElement,
  maxwidth = docEl.dataset.mw || 750, // 每 dpr 最大頁面寬度
  dpr = isIos ? Math.min(win.devicePixelRatio, 3) : 1,
  scale = 1 / dpr,
  tid;
   
  docEl.removeAttribute(‘data-mw‘);
  docEl.dataset.dpr = dpr;
  metaEl = doc.createElement(‘meta‘);
  metaEl.name = ‘viewport‘;
  metaEl.content = fillScale(scale);
  docEl.firstElementChild.appendChild(metaEl);
   
  var refreshRem = function() {
  var width = docEl.getBoundingClientRect().width;
  if (width / dpr > maxwidth) {
  width = maxwidth * dpr;
  }
  var rem = width / 16;
  docEl.style.fontSize = rem + ‘px‘;
  };
   
  win.addEventListener(‘resize‘, function() {
  clearTimeout(tid);
  tid = setTimeout(refreshRem, 300);
  }, false);
  win.addEventListener(‘pageshow‘, function(e) {
  if (e.persisted) {
  clearTimeout(tid);
  tid = setTimeout(refreshRem, 300);
  }
  }, false);
   
  refreshRem();
  } else if ( isMobile && !matchScale && ( matchWidth && matchWidth[1] != ‘device-width‘ ) ) { // 定寬
  var width = parseInt(matchWidth[1]),
  iw = win.innerWidth || width,
  ow = win.outerWidth || iw,
  sw = win.screen.width || iw,
  saw = win.screen.availWidth || iw,
  ih = win.innerHeight || width,
  oh = win.outerHeight || ih,
  ish = win.screen.height || ih,
  sah = win.screen.availHeight || ih,
  w = Math.min(iw,ow,sw,saw,ih,oh,ish,sah),
  scale = w / width;
   
  if ( scale < 1 ) {
  metaEl.content = metaCtt + ‘,+ fillScale(scale);
  }
  }
   
  function fillScale(scale) {
  return ‘initial-scale=+ scale + ‘,maximum-scale=+ scale + ‘,minimum-scale=+ scale;
  }
  },
   
  /**
  * 轉href參數成鍵值對
  * @param href {string} 指定的href,默認爲當前頁href
  * @returns {object} 鍵值對
  */
  getSearch: function(href) {
  href = href || win.location.search;
  var data = {},reg = new RegExp( "([^?=&]+)(=([^&]*))?", "g" );
  href && href.replace(reg,function( $0, $1, $2, $3 ){
  data[ $1 ] = $3;
  });
  return data;
  }
  };
  })(window, document);
   
  // 默認直接適配頁面
  mobileUtil.fixScreen();
相關文章
相關標籤/搜索