本文为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 [email protected]
依据选项创立一个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 = falselet eventTargetfunction 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);}// ...
先手动创立一个对应类型的touch
Event对象,设置该事件反对冒泡,而后设置了相干按键的按下状态,笔者也是才晓得TouchEvent
事件是须要这几个属性的:
而后设置触摸点数据,一共有三种类型:
touches
:以后屏幕上所有触摸点的列表targetTouches
:以后对象上所有触摸点的列表changedTouches
:波及以后(引发)事件的触摸点的列表
挪动端触摸点是可能存在多个的,比方我同时好几个手指一起触摸,能够通过这三个列表进行辨别,同样举个栗子,比方我给一个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
事件对象,最初在事件指标元素上进行派发即可。