前端開發中,比較重要的一個環節就是要適配各個屏幕的尺寸。
PC端比較簡單的是響應式和自適應。響應式比較簡單,通過Media查詢頁面寬度,再加載相應的樣式即可。自適應就是用百分比,rem,vw這樣的單位去做。
但是今天主要講的是移動端的適配。從iphone4到iphone7P,3.5寸小屏到如今的5.5寸大屏,如何提供一套簡單的適配方案呢?這里推薦一下手淘團隊的方案--Flexable。
不同分辨率,不同尺寸的屏幕。首先普及一些基本概念:
物理像素:物理像素又被稱為設備像素,他是顯示設備中一個最微小的物理部件。每個像素可以根據操作系統設置自己的顏色和亮度。正是這些設備像素的微小距離欺騙了我們肉眼看到的圖像效果。
設備獨立像素:設備獨立像素也稱為密度無關像素,可以認為是計算機坐標系統中的一個點,這個點代表一個可以由程序使用的虛擬像素(比如說CSS像素),然后由相關系統轉換為物理像素。
css像素:CSS像素是一個抽像的單位,主要使用在瀏覽器上,用來精確度量Web頁面上的內容。一般情況之下,CSS像素稱為與設備無關的像素(device-independent pixel),簡稱DIPs。
屏幕密度:屏幕密度是指一個設備表面上存在的像素數量,它通常以每英寸有多少像素來計算(PPI)。
設備像素比:設備像素比簡稱為dpr,其定義了物理像素和設備獨立像素的對應關系。它的值可以按下面的公式計算得到:
設備像素比=物理像素/設備獨立像素。
眾所周知,iPhone6的設備寬度和高度為375pt * 667pt,根據上面公式,我們可以很輕松得知其物理像素為750pt * 1334pt。在不同的屏幕上,CSS像素所呈現的物理尺寸是一致的,而不同的是CSS像素所對應的物理像素具數是不一致的。在普通屏幕下1個CSS像素對應1個物理像素,而在Retina屏幕下,1個CSS像素對應的卻是4個物理像素。
了解了前面一些相關概念之后,接下來我們來看實際解決方案。在整個手淘團隊,我們有一個名叫lib-flexable的庫。可以百度搜索flexable下載這個庫。之后就可以把庫引用到你的項目中去了。
讀到這里,大家應該都知道,我們接下來要做的事情,就是如何把視覺稿中的px轉換成rem。
目前Flexible會將視覺稿分成100份,(主要為了以后能更好的兼容vh和vw),而每一份被稱為一個單位。同時1rem單位被認定為10a。針對我們這份視覺稿可以計算出:1a=7.5px;1rem=75px。
這樣一來,對于視覺稿上的元素尺寸換算,只需要原始的px值除以rem基準值即可。例如此例視覺稿中的圖片,其尺寸是176px * 176px,轉換成為2.346667rem * 2.346667rem。在實際生產當中,如果每一次計算px轉換rem,或許會覺得非常麻煩,或許直接影響大家平時的開發效率。為了能讓大家更快進行轉換,我們團隊內的同學各施所長,為px轉換rem寫了各式各樣的小工具。CSSREM是一個CSS的px轉rem值的Sublime Text3自動完成插件。
文為Varlet組件庫源碼主題閱讀系列第八篇,讀完本篇,可以了解到移動端頁面如何適配各種尺寸的屏幕,包括pc端,另外如何將觸摸事件轉換成鼠標事件。
開發移動端頁面,我們通常都會按照一個固定寬度的設計稿來做,但是實際上的手機屏幕尺寸五花八門,如果不進行適配的話會比較影響使用體驗。
Varlet組件庫的設計就是基于375px寬度的設計稿,然后使用postcss-px-to-viewport進行移動端適配,這個PostCSS插件會將px單位轉換成vw單位,1vw等于1/100的視口寬度,所以使用vw作為單位就會隨著視口的寬度進行變化達到適配不同機型的效果。
px轉vw也很簡單,假設某個元素的寬高為100px,設計稿寬度為375px,那么視口也就相當于是375px,那么1vw=375 / 100=3.75px,那么100px / 3.75px=26.66vw,公式如下:
vw=px / (viewportSize / 100)
接下來我們從零創建一個Vite項目來看一下postcss-px-to-viewport插件的使用。
創建項目:
npm init vite@latest
根據選項創建一個Vue的項目,然后寫一個非常簡單的按鈕:
接下來安裝依賴和啟動服務,效果如下:
假設我們的設計稿就是375px,那么我們切換到尺寸更大一點的機型看看:
直接上iPad,可以看到按鈕尺寸沒有變,但是因為屏幕變大了而顯得按鈕太小了,這顯然是不夠友好的,接下來我們就配置一下postcss-px-to-viewport插件。
這個插件本身是一個PostCSS的插件,所以首先要支持PostCss,在Vite項目中使用PostCSS很簡單,只要項目中包含有效的PostCSS 配置,Vite就會自動使其應用于所有導入的CSS,所以我們要做的就是增加一個PostCSS 配置,參考postcss-px-to-viewport插件文檔,先安裝:
npm install postcss-px-to-viewport
然后創建postcss.config.js文件,寫入如下內容:
module.exports={
plugins: {
"postcss-px-to-viewport": {
// 需要轉換的單位
unitToConvert: "px",
// 設計稿的視口寬度
viewportWidth: 375,
// 單位轉換后保留的精度
unitPrecision: 4,
},
},
};
再次啟動服務看看效果:
報錯了,雖然不知道為什么會把這個配置文件也當成ES Module解析,但是解決方法很簡單,把后綴名改成.cjs即可,再次重啟:
可以看到按鈕變大了,單位也由我們書寫的px變成了vw。
這個適配指的不是尺寸,因為前面已經使用vw解決了尺寸的適配問題,這里主要是指事件,具體來說是我們在移動端使用的交互事件一般是touch事件,但是桌面端肯定不支持,所以為了讓我們的移動端組件庫不至于在桌面端完全無法使用,需要將touch事件轉成mouse事件。
Varlet使用的是@varlet/touch-emulator這個包來實現的,使用也很簡單,安裝:
npm i @varlet/touch-emulator
導入:
import '@varlet/touch-emulator'
接下來修改一下我們上面的示例,給按鈕增加一個touchstart事件:
然后分別在模擬器和非模擬器環境下單擊一下按鈕:
顯然,非模擬器環境下單擊是沒有效果的,接下來配置一下@varlet/touch-emulator,再次查看非模擬器環境下的點擊效果:
可以看到成功觸發了。
接下來就來窺探一下@varlet/touch-emulator都做了些什么。
// 判斷是否是瀏覽器環境
const inBrowser=typeof window !=='undefined'
// 判斷該環境是否支持touch事件
const supportTouch=inBrowser && 'ontouchstart' in window
// ...
首先進行了一下環境判斷,如果不滿足這兩個條件就不需要做任何處理。
// ...
if (inBrowser && !supportTouch) {
createTouchEmulator()
}
// ...
滿足條件則調用createTouchEmulator方法:
// ...
function createTouchEmulator() {
window.addEventListener('mousedown', (event)=> onMouse(event, 'touchstart'), true)
window.addEventListener('mousemove', (event)=> onMouse(event, 'touchmove'), true)
window.addEventListener('mouseup', (event)=> onMouse(event, 'touchend'), true)
}
// ...
監聽了三個鼠標事件,分別對應三個touch事件,注意addEventListener方法第三個參數都傳了true,這個參數默認是false,表示在事件冒泡的階段調用事件處理函數,傳true就表示在事件捕獲的階段調用事件處理函數,舉個栗子,比如我們給頁面上的一個div也綁定了mousedown事件,然后當我們鼠標在這個div上按下,如果是冒泡階段,那么div的事件函數會先被調用,如果是捕獲階段,那么window的事件函數會先被調用,所以這里傳true筆者猜測是因為如果是冒泡階段觸發的話,某個元素的可能會阻止冒泡,那么就不會觸發window上綁定的這幾個事件了。
這幾個處理方法內都調用了onMouse方法:
// ...
let initiated=false
let eventTarget
function onMouse(mouseEvent, touchType) {
// 事件類型、事件目標
const { type, target }=mouseEvent
// mousedown=true(mousedown事件)
// false(mouseup事件)
// 保持(mousemove事件)
initiated=isMousedown(type) ? true : isMouseup(type) ? false : initiated
// 如果是鼠標移動事件且鼠標沒有按下則返回
if (isMousemove(type) && !initiated) return
// 判斷是否要更新事件目標
if (isUpdateTarget(type)) eventTarget=target
// 手動構造對應的touch事件并觸發
triggerTouch(touchType, mouseEvent)
// 如果鼠標松開了則清除保存的事件目標
if (isMouseup(type)) eventTarget=null
}
const isMousedown=(eventType)=> eventType==='mousedown'
const isMousemove=(eventType)=> eventType==='mousemove'
const isMouseup=(eventType)=> eventType==='mouseup'
// ...
這個方法首先根據鼠標事件的類型設置了initiated變量,記錄鼠標的按下狀態,如果是鼠標移動事件且鼠標沒有按下,那么個方法會直接返回,因為touch事件都需要先按下才會觸發,然后調用了isUpdateTarget方法判斷是否要更新事件目標:
const isUpdateTarget=(eventType)=>
isMousedown(eventType) || !eventTarget || (eventTarget && !eventTarget.dispatchEvent)
鼠標按下顯然對應的是touchstart,觸發的第一個touch事件,事件目標肯定也是新的,所以需要更新,理論上不同手指的事件目標是可能不一樣的,但是由于桌面端鼠標事件只能有一個,所以直接用一個變量保存即可。
eventTarget不存在當然也需要更新,但是筆者覺得這種情況應該不會出現,因為touchstart或者說是mousedown事件肯定是最先被觸發的,eventTarget應該已經有值了。
第三個條件筆者也沒有理解,按理說只要是DOM元素應該都會有dispatchEvent方法。
接下來調用了triggerTouch方法:
// ...
function triggerTouch(touchType, mouseEvent) {
const { altKey, ctrlKey, metaKey, shiftKey }=mouseEvent;
// bubbles:該事件是否冒泡
// cancelable:該事件能否被取消
const touchEvent=new Event(touchType, { bubbles: true, cancelable: true });
// 設置幾個鍵的按下標志
touchEvent.altKey=altKey;
touchEvent.ctrlKey=ctrlKey;
touchEvent.metaKey=metaKey;
touchEvent.shiftKey=shiftKey;
// 設置三種類型的觸摸點對象數據
touchEvent.touches=getActiveTouches(mouseEvent);
touchEvent.targetTouches=getActiveTouches(mouseEvent);
touchEvent.changedTouches=createTouchList(mouseEvent);
// 派發事件
eventTarget.dispatchEvent(touchEvent);
}
// ...
先手動創建一個對應類型的touchEvent對象,設置該事件支持冒泡,然后設置了相關按鍵的按下狀態,筆者也是才知道TouchEvent事件是需要這幾個屬性的:
然后設置觸摸點數據,一共有三種類型:
移動端觸摸點是可能存在多個的,比如我同時好幾個手指一起觸摸,可以通過這三個列表進行區分,同樣舉個栗子,比如我給一個div綁定了三個touch事件,第一次我一個手指觸摸到div上,此時這三個列表的值是一樣的,就是第一個手指的觸摸點,然后我第二個手指也開始觸摸,但是不是觸摸到div上,而是其他元素上,那么此時touches列表會包含兩個手指的觸摸點,targetTouches列表只會包含第一個手指的觸摸點,changedTouches列表則為第二個手指的觸摸點。手指全部松開后,這三個列表都將為空。
但是在桌面端,鼠標觸摸點顯然只有一個,所以這三個列表其實都是相同的。
touches和targetTouches都調用了getActiveTouches方法獲取:
// ...
function getActiveTouches(mouseEvent) {
const { type }=mouseEvent;
if (isMouseup(type)) return createTouchList();
return updateTouchList(mouseEvent);
}
// ...
松開事件touchList是空的,所以返回一個空列表即可,調用的是createTouchList方法:
// ...
function createTouchList() {
const touchList=[];
touchList.item=function (index) {
return this[index] || null;
};
return touchList;
}
// ...
原生的TouchList對象存在一個item方法,返回列表中以指定值作為索引的 Touch 對象,所以使用數組來代表TouchList需要自行提供一個同名方法。
其他事件類型則會調用updateTouchList方法:
// ...
function updateTouchList(mouseEvent) {
const touchList=createTouchList();
touchList.push(new Touch(eventTarget, 1, mouseEvent));
return touchList;
}
// ...
同樣先創建了一個touchList,然后創建了一個Touch實例添加進去,這個Touch類定義如下,模擬的是原生的Touch對象:
// ...
function Touch(target, identifier, mouseEvent) {
const { clientX, clientY, screenX, screenY, pageX, pageY }=mouseEvent;
this.identifier=identifier;
this.target=target;
this.clientX=clientX;
this.clientY=clientY;
this.screenX=screenX;
this.screenY=screenY;
this.pageX=pageX;
this.pageY=pageY;
}
// ...
changedTouches直接調用的是createTouchList方法,顯然無論何時返回的都是空的列表,這個似乎是有點問題的,因為前面說了,只有一個觸摸點的話這三個列表的值應該都是一樣的。
最后在事件目標上進行了事件的派發。
總結一下,整體所做的事情就是監聽鼠標的三個事件,然后手動創建對應的touch事件對象,最后在事件目標元素上進行派發即可。
前端開發中,移動端不同設備的屏幕適配一直是個繞不開的技術話題。目前比較流行的方案是類似淘寶的flexible。其原理是使用js動態計算html的font-size,利用rem來實現不同寬度的適配。使用js方案雖然比較成熟,但也有它的一些缺點,比如性能損耗,由于js的阻塞加載和動態計算,頁面不免會出現卡頓和閃屏的現象,影響用戶體驗。今天我們不使用js,完全使用css來實現適配,來看看是怎么實現的吧!
移動端屏幕適配
在html的head中插入下面的meta標簽:
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=375, user-scalable=no">
沒錯,是兩個viewport標簽。width=device-width寫在上面,width=375寫在下面,375就是以哪個設備寬度為基準,現在大部分設計稿都是以iphone6的375寬度為基準做2倍圖。加了上面兩個mata標簽,后面的css就可以完全使用px為單位直接使用,整個頁面會自動按設備寬度進行等比例縮放。看下面的演示效果:
<script src="https://lf6-cdn-tos.bytescm.com/obj/cdn-static-resource/tt_player/tt.player.js?v=20160723"></script>
在css中定義html的font-size為:calc(100vw/3.75),calc、vw能兼容ios8+和android4.4+,可放心使用,如下:
html {
font-size: calc(100vw/3.75);
-webkit-text-size-adjust: 100%;
}
然后在css中,就可以將所有的px單位除以100,得到rem單位了。比如:
.row>div {
float: left;
width: .82rem;
height: .82rem;
text-align: center;
line-height: .82rem;
margin-left: .05rem;
background-color: #f0f0f0;
}
上面的rem單位轉換,建議大家可以使用px2rem這個插件完成,webpack、vscode都能支持。設置時將rootFontSize 設為100即可。
設置px2rem參數
在vscode中,可以使用ctrl+shift+p,選擇px2rem就可以將當前頁面的px全部轉換為rem。
px2rem在vscode中的使用方法
當然,rem和px可以相互共存,比如我標題欄就想要44px高,這樣就不會縮放了。看下面的演示效果:
純css實現移動端適配
方案一,直接使用html的mata實現整個頁面的縮放,比較適合那些宣傳單頁或全屏游戲交互類,無法實現px與rem共存的情況。
方案二,利用了rem來縮放,可實現與px共存,在借助px2rem的情況下,能高效方便的實現適配。
綜合考慮,小編建議使用方案二。你,學會了嗎?
*請認真填寫需求信息,我們會在24小時內與您取得聯系。