移動端樣式全面適配(基於webpack+vue為例)
該適配方案基於webpack和vue來闡述,但是原理基本大同小異,並不侷限於webpack+vue
為方便闡述,直接以vue-cli搭建的腳手架專案開始
該專案引入一個外掛postcss-px2rem配合使用,當然該外掛也可以自己寫
1.快速搭建好一個專案
vue init webpack my-project
安裝好依賴之後,就算建立專案成功,此時把專案跑起來npm run dev
2.引入初始化樣式
// reset.css /* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 License: none (public domain) */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block; } body { line-height: 1; } ol, ul { list-style: none; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } table { border-collapse: collapse; border-spacing: 0; }
上面的初始化樣式是來自網上,我們可以根據自己的業務需求將樣式寫得更為完善:
// reset.css /* * guagnzhul * 20180930 */ *{ box-sizing: border-box; } html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, menu, nav, output, ruby, section, summary, time, mark, audio, video, input { margin: 0; padding: 0; border: 0; font-size: 100%; vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, footer, header, menu, nav, section{ display: block; } body { line-height: 1; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: none; } table { border-collapse: collapse; border-spacing: 0; } /* custom */ a { color: #7e8c8d; -webkit-backface-visibility: hidden; text-decoration: none; } li { list-style: none; } body { -webkit-text-size-adjust: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } /* 消除邊框 */ input,img{ border: none; } /* 消除高亮 */ input{ outline: none; border-radius: 0; } /* 統一對齊方式,避免間隙, 限定最大寬度,避免溢位 */ img{ vertical-align: bottom; max-width: 100%; } /* 方便具體頁面設定不同的背景色 */ html, body, #app{ min-height: 100%; /* 解決邊界的margin不生效或溢位問題 */ padding: 1px 0; /* no */ margin: -1px 0; /* no */ } body{ /* 解決 Ios 300毫秒延遲 */ touch-action: manipulation; }
初始化的樣式放置位置和引入方式可以參考下圖:
3.安裝外掛postcss-plugin-px2rem
npm install --save-dev postcss-px2rem
4.vue結尾檔案引用vue-loader.config.js中的配置
'use strict' const utils = require('./utils') const config = require('../config') const isProduction = process.env.NODE_ENV === 'production' const sourceMapEnabled = isProduction ? config.build.productionSourceMap : config.dev.cssSourceMap /*引入postcss-px2rem 通過require的形式*/ var px2rem = require('postcss-px2rem'); module.exports = { loaders: utils.cssLoaders({ sourceMap: sourceMapEnabled, extract: isProduction, /*允許使用usePostCSS*/ usePostCSS:true, }), cssSourceMap: sourceMapEnabled, cacheBusting: config.dev.cacheBusting, transformToRequire: { video: ['src', 'poster'], source: 'src', img: 'src', image: 'xlink:href' }, /*配置remUnit*/ postcss: function() { return [px2rem({remUnit: 75})]; } }
上面的程式碼中postcss: function() { return [px2rem({remUnit: 75})]; }
是什麼意思?這是我們自己去設定的一個值,為什麼用75?它是什麼意思?通常設計稿都是以iphone6的尺寸為標準,也就是375個物理畫素,我們開發的時候可以把設計稿看做成750px。這樣75就是750的十分之一,我們把一個螢幕的十分之一看做1rem,這樣的話,我們就可以用750px的設計稿用px的方式去寫程式碼,能夠自動相容所有的機型了。例如,你寫一個width: 150px
的寬度,那麼它會自動計算150/75=2rem,那麼你的樣式最終生成出來的就是width: 2rem
。因為我們的rem的基準是基於html的font-size的大小的,所以接下來我們要去設定這個大小。
5.在index.html引入font-size的設定,設定的比例與remUnit一致
// index.html,其將螢幕寬度分成了10份
<script>document.getElementsByTagName('html')[0].style.fontSize = (document.documentElement.clientWidth || document.body.clientWidth) /10 + 'px';</script>
6.適配各機型的原理是什麼?
舉個例子,iphone螢幕寬度375px(物理畫素),分成10份,每份37.5px,即<html style="font-size: 37.5px;">
,此時寫下width: 150px
,它會自動計算150/75=2rem,那麼外掛轉換為width: 2rem
,也就是會在該手機顯示出width: 70px
的效果。
這個時候換了iPad,其螢幕寬度是768px(物理畫素),分成10份,每份37.5px,即<html style="font-size: 76.8px;">
,此時寫下width: 150px
,它會自動計算150/75=2rem,那麼外掛轉換為width: 2rem
,也就是會在該平板顯示出width: 153.59px
的效果。
7.關於1px的轉換
並非所有機型和瀏覽器都會支援1px以下的畫素,例如我們寫width:1px
,可能會被轉換成width: 0.013333rem;
,那麼如果該瀏覽器並不支援1px的顯示怎麼辦呢?這時候我們針對該比較特殊的單位,進行一個不編譯的做法,也就是直接讓它顯示1px,而不會進行單位的轉換。
// 做法很簡單,只需要在該屬性後面加上這個註釋即可/* no */
h1{
width: 1px; /* no */
}
8.關於1px會偏粗的問題
比如有的機型,dpr為2,那麼我們所寫的1px邏輯畫素,會在螢幕上顯示2個物理畫素,那麼我們看到線條會比我們想要的1px的粗,這時候我們應該分兩類情況處理:
(1)手機支援1px以下畫素的顯示;
(2)手機不支援1px以下畫素的顯示。
當手機支援1px以下畫素的顯示之後,再分三種情況(只要相容主流的機型的dpr):
(1)dpr為2時,1px除以2,即寫0.5px,且不編譯,其他尺寸同理;
(2)dpr為3時,1px除以3,即寫0.333px,且不編譯,其他尺寸同理;
(3)dpr為其他情況的時候,直接取原本的尺寸,即1px,且不編譯。
所以我們的關鍵是,何以判斷瀏覽器是否支援1px以下畫素的顯示,同時檢測到手機螢幕dpr的值(以下為rem相容程式碼adaptation.html):
<!-- rem相容方案 -->
<meta id="__j_viewport_meta_tag__" name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<script>
;(function flexible (window, document) {
var docEl = document.documentElement
var dpr = window.devicePixelRatio || 1
// adjust body font size
function setBodyFontSize () {
if (document.body) {
document.body.style.fontSize = (12 * dpr) + 'px'
}
else {
document.addEventListener('DOMContentLoaded', setBodyFontSize)
}
}
setBodyFontSize();
// set 1rem = viewWidth / 10
function setRemUnit () {
var rem = docEl.clientWidth / 10
docEl.style.fontSize = rem + 'px'
}
setRemUnit()
// reset rem unit on page resize
window.addEventListener('resize', setRemUnit)
window.addEventListener('pageshow', function (e) {
if (e.persisted) {
setRemUnit()
}
})
// detect 0.5px supports
if (dpr >= 2) {
var fakeBody = document.createElement('body')
var testElement = document.createElement('div')
testElement.style.border = '.5px solid transparent'
fakeBody.appendChild(testElement)
docEl.appendChild(fakeBody)
if (testElement.offsetHeight === 1) {
docEl.setAttribute('data-dpr', Math.floor(dpr));
}
docEl.removeChild(fakeBody)
}
docEl.setAttribute('data-origin-dpr', window.devicePixelRatio);
var oMeta = document.getElementById('__j_viewport_meta_tag__')
var iphoneXFixed = (osv = window.navigator.userAgent.match(/(iphone|ipad|ipod)\s+os\s+(\d{2})/i)) && osv.length > 0 && +osv[osv.length - 1] > 10 && 812 == screen.height && 375 == screen.width ? ", viewport-fit=cover" : "";
oMeta.setAttribute("content", oMeta.getAttribute('content') + iphoneXFixed);
}(window, document));
</script>
當我們引入這個檔案的時候,就可以忽略第5步提到的index.html引入的那段程式碼了,反而直接在index.html引入該檔案即可,因為該檔案程式碼已經包含了第5步的程式碼了,我們直接引入到index.html:
// index.html
<!DOCTYPE>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>my-project</title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
<script>
(function flexible(window, document) {
var docEl = document.documentElement
var dpr = window.devicePixelRatio || 1
// adjust body font size
function setBodyFontSize() {
if (document.body) {
document.body.style.fontSize = (12 * dpr) + 'px'
} else {
document.addEventListener('DOMContentLoaded', setBodyFontSize)
}
}
setBodyFontSize();
// set 1rem = viewWidth / 10
function setRemUnit() {
var rem = docEl.clientWidth / 10
docEl.style.fontSize = rem + 'px'
}
setRemUnit()
// reset rem unit on page resize
window.addEventListener('resize', setRemUnit)
window.addEventListener('pageshow', function (e) {
if (e.persisted) {
setRemUnit()
}
})
// detect 0.5px supports
if (dpr >= 2) {
var fakeBody = document.createElement('body')
var testElement = document.createElement('div')
testElement.style.border = '.5px solid transparent'
fakeBody.appendChild(testElement)
docEl.appendChild(fakeBody)
if (testElement.offsetHeight === 1) {
docEl.setAttribute('data-dpr', Math.floor(dpr));
}
docEl.removeChild(fakeBody)
}
docEl.setAttribute('data-origin-dpr', window.devicePixelRatio);
var oMeta = document.getElementById('__j_viewport_meta_tag__')
var iphoneXFixed = (osv = window.navigator.userAgent.match(/(iphone|ipad|ipod)\s+os\s+(\d{2})/i)) && osv.length >
0 && +osv[osv.length - 1] > 10 && 812 == screen.height && 375 == screen.width ? ", viewport-fit=cover" : "";
oMeta.setAttribute("content", oMeta.getAttribute('content') + iphoneXFixed);
}(window, document));
</script>
</html>
核心程式碼解釋:
(1)檢測螢幕的dpr並不是什麼難事:
// 賦值預設值為1
var dpr = window.devicePixelRatio || 1
(2)設定html的font-size:
var docEl = document.documentElement;
function setRemUnit() {
var rem = docEl.clientWidth / 10
docEl.style.fontSize = rem + 'px'
}
(3)檢測瀏覽器是否支援1px以下畫素顯示:
// detect 0.5px supports
// 建立一個0.5px去檢測,看是否得到瀏覽器的支援,若支援則認為瀏覽器支援1px以下畫素,否則則認為不支援
if (dpr >= 2) {
// 建立一個body是dom節點
var fakeBody = document.createElement('body')
// 建立一個div
var testElement = document.createElement('div')
// 給該div一個0.5畫素的透明border
testElement.style.border = '.5px solid transparent'
// div加入body
fakeBody.appendChild(testElement)
// document.documentElement加入fakeBody
docEl.appendChild(fakeBody)
// 當testElement.offsetHeight即可視高度能夠為1的時候,證明該瀏覽器是可以讓0.5px顯示出1的效果
// 從而證明該瀏覽器可以支援到1px以下畫素的顯示
// 於是在docEl賦值自定義屬性data-dpr為其dpr
if (testElement.offsetHeight === 1) {
alert(testElement.offsetHeight)
docEl.setAttribute('data-dpr', Math.floor(dpr));
}
// 移除該dom
docEl.removeChild(fakeBody)
}
原理是藉助一個testElement.style.border = '.5px solid transparent'
的透明0.5px的dom節點去檢測其可視高度testElement.offsetHeight
從而得出該瀏覽器是否支援0.5畫素的結論。
9.如何在CSS中運用
對於命中dpr為2和3的分別作處理(樣式優先順序最高,以覆蓋後者預設樣式),另外有一個預設的處理使用者處理不命中的情況。兩處都不能進行編譯,因為要用原生來顯示。
<style>
[data-dpr="2"]{
div {
/* 不可以編譯且優先順序最高 */
border: 0.5px solid #979797 !important; /* no */
}
}
[data-dpr="3"]{
div {
border: 0.333px solid #979797 !important; /* no */
}
}
div {
/* 該處始終被執行,但是如果上面的執行了,該處會因為優先順序不夠高而被覆蓋 */
border: 1px solid #979797; /* no */
}
</style>
技巧:當都沒有命中dpr為2和3的情況的話,就會觸發預設的樣式設定,但是此時可能會存在顯示的1px比實際上的1px要大,這個時候可以考慮給予一定的透明度讓其線條看起來更為纖細,達到視覺的效果。
例如:
div {
/* 給予透明度顯得線條更為纖細 */
border: 1px solid rgba(151, 151, 151, 0.8); /* no */
}