前端开发中,比较重要的一个环节就是要适配各个屏幕的尺寸。
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小时内与您取得联系。