• 移动端页面如何优雅的适配各种屏幕,包括PC端


    本文为Varlet组件库源码主题阅读系列第八篇,读完本篇,可以了解到移动端页面如何适配各种尺寸的屏幕,包括pc端,另外如何将触摸事件转换成鼠标事件。

    移动端适配

    开发移动端页面,我们通常都会按照一个固定宽度的设计稿来做,但是实际上的手机屏幕尺寸五花八门,如果不进行适配的话会比较影响使用体验。

    Varlet组件库的设计就是基于375px宽度的设计稿,然后使用postcss-px-to-viewport进行移动端适配,这个PostCSS插件会将px单位转换成vw单位,1vw等于1/100的视口宽度,所以使用vw作为单位就会随着视口的宽度进行变化达到适配不同机型的效果。

    pxvw也很简单,假设某个元素的宽高为100px,设计稿宽度为375px,那么视口也就相当于是375px,那么1vw = 375 / 100 = 3.75px,那么100px / 3.75px = 26.66vw ,公式如下:

    vw = px / (viewportSize / 100)
    
    • 1

    接下来我们从零创建一个Vite项目来看一下postcss-px-to-viewport插件的使用。

    创建项目:

    npm init vite@latest
    
    • 1

    根据选项创建一个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
    
    • 1

    然后创建postcss.config.js文件,写入如下内容:

    module.exports = {
      plugins: {
        "postcss-px-to-viewport": {
          // 需要转换的单位
          unitToConvert: "px",
          // 设计稿的视口宽度
          viewportWidth: 375,
          // 单位转换后保留的精度
          unitPrecision: 4,
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    再次启动服务看看效果:

    报错了,虽然不知道为什么会把这个配置文件也当成ES Module解析,但是解决方法很简单,把后缀名改成.cjs即可,再次重启:

    可以看到按钮变大了,单位也由我们书写的px变成了vw

    桌面端适配

    这个适配指的不是尺寸,因为前面已经使用vw解决了尺寸的适配问题,这里主要是指事件,具体来说是我们在移动端使用的交互事件一般是touch事件,但是桌面端肯定不支持,所以为了让我们的移动端组件库不至于在桌面端完全无法使用,需要将touch事件转成mouse事件。

    Varlet使用的是@varlet/touch-emulator这个包来实现的,使用也很简单,安装:

    npm i @varlet/touch-emulator
    
    • 1

    导入:

    import '@varlet/touch-emulator'
    
    • 1

    接下来修改一下我们上面的示例,给按钮增加一个touchstart事件:

    然后分别在模拟器和非模拟器环境下单击一下按钮:

    显然,非模拟器环境下单击是没有效果的,接下来配置一下 @varlet/touch-emulator,再次查看非模拟器环境下的点击效果:

    可以看到成功触发了。

    接下来就来窥探一下 @varlet/touch-emulator都做了些什么。

    // 判断是否是浏览器环境
    const inBrowser = typeof window !== 'undefined'
    // 判断该环境是否支持touch事件
    const supportTouch = inBrowser && 'ontouchstart' in window
    // ...
    
    • 1
    • 2
    • 3
    • 4
    • 5

    首先进行了一下环境判断,如果不满足这两个条件就不需要做任何处理。

    // ...
    if (inBrowser && !supportTouch) {
      createTouchEmulator()
    }
    // ...
    
    • 1
    • 2
    • 3
    • 4
    • 5

    满足条件则调用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)
    }
    // ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    监听了三个鼠标事件,分别对应三个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'
    // ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    这个方法首先根据鼠标事件的类型设置了initiated变量,记录鼠标的按下状态,如果是鼠标移动事件且鼠标没有按下,那么个方法会直接返回,因为touch事件都需要先按下才会触发,然后调用了isUpdateTarget方法判断是否要更新事件目标:

    const isUpdateTarget = (eventType) =>
      isMousedown(eventType) || !eventTarget || (eventTarget && !eventTarget.dispatchEvent)
    
    • 1
    • 2

    鼠标按下显然对应的是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);
    }
    // ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    先手动创建一个对应类型的touchEvent对象,设置该事件支持冒泡,然后设置了相关按键的按下状态,笔者也是才知道TouchEvent事件是需要这几个属性的:

    然后设置触摸点数据,一共有三种类型:

    • touches:当前屏幕上所有触摸点的列表
    • targetTouches:当前对象上所有触摸点的列表
    • changedTouches:涉及当前(引发)事件的触摸点的列表

    移动端触摸点是可能存在多个的,比如我同时好几个手指一起触摸,可以通过这三个列表进行区分,同样举个栗子,比如我给一个div绑定了三个touch事件,第一次我一个手指触摸到div上,此时这三个列表的值是一样的,就是第一个手指的触摸点,然后我第二个手指也开始触摸,但是不是触摸到div上,而是其他元素上,那么此时touches列表会包含两个手指的触摸点,targetTouches列表只会包含第一个手指的触摸点,changedTouches列表则为第二个手指的触摸点。手指全部松开后,这三个列表都将为空。

    但是在桌面端,鼠标触摸点显然只有一个,所以这三个列表其实都是相同的。

    touchestargetTouches都调用了getActiveTouches方法获取:

    // ...
    function getActiveTouches(mouseEvent) {
      const { type } = mouseEvent;
      if (isMouseup(type)) return createTouchList();
      return updateTouchList(mouseEvent);
    }
    // ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    松开事件touchList是空的,所以返回一个空列表即可,调用的是createTouchList方法:

    // ...
    function createTouchList() {
      const touchList = [];
    
      touchList.item = function (index) {
        return this[index] || null;
      };
    
      return touchList;
    }
    // ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    原生的TouchList对象存在一个item方法,返回列表中以指定值作为索引的 Touch 对象,所以使用数组来代表TouchList需要自行提供一个同名方法。

    其他事件类型则会调用updateTouchList方法:

    // ...
    function updateTouchList(mouseEvent) {
      const touchList = createTouchList();
    
      touchList.push(new Touch(eventTarget, 1, mouseEvent));
      return touchList;
    }
    // ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    同样先创建了一个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;
    }
    // ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    changedTouches直接调用的是createTouchList方法,显然无论何时返回的都是空的列表,这个似乎是有点问题的,因为前面说了,只有一个触摸点的话这三个列表的值应该都是一样的。

    最后在事件目标上进行了事件的派发。

    总结一下,整体所做的事情就是监听鼠标的三个事件,然后手动创建对应的touch事件对象,最后在事件目标元素上进行派发即可。

  • 相关阅读:
    LeetCode 0522.最长特殊序列 II:两句话讲明思路(子序列判断)
    Shell 脚本编程——变量和运算符
    金色传说:SAP-ME-IDOC:使用基本类型LOIPRO01向ME发送生产订单,IDOC状态30问题解决
    ArcGIS中ArcMap导入mxd地图文档文件出现红色感叹号、地图空白的解决
    【C++】格式与实例化操作——[模板]详解(7)
    【ELM预测】基于matlab引力搜索算法优化极限学习机预测(含前后对比)【含Matlab源码 2205期】
    最全iOS 应用上架流程(提交到AppStore)
    在域控批量导出用户及其所在路径的信息
    【(难)828. 统计子串中的唯一字符】(HashMap)
    计算机组成原理_虚拟存储器
  • 原文地址:https://blog.csdn.net/sinat_33488770/article/details/127416447