Практическое обучение: выпадающее обновление Vue, подключаемый модуль загрузки компонента (очень подробно)

Vue.js

предисловие

Давно затянувшийся выпадающий список для обновления компонента Vue — наконец-то он здесь, на самом деле, он давно написан, и я давно не писал статьи…

Над изображениями

технический пункт

На самом деле технический момент не сложный, в основном с использованием сенсорного события H5:

  • touchstart: событие, вызванное касанием экрана пальцем, основная задача — получить координату Y щелчка мыши при его срабатывании, event.touches[0].pageY.
  • touchmove: событие, вызванное скольжением пальца, основная задача состоит в том, чтобы получить координату перемещения Y за вычетом начальной координаты Y, когда оно запускается, чтобы получить расстояние перемещения, а затем использовать преобразование для изменения положения контейнера.
  • touchend: событие, вызванное отпусканием пальца, основная задача — отпустить мышь, чтобы восстановить div в исходное положение.

Идеальное использование

Поскольку это компонент, разработчикам должно быть легко им пользоваться, просто и легко. Предположим, что наши компоненты обновления по запросу и загрузки по запросуupLoadDownRefresh:

    <upLoadDownRefresh>
      <div v-for="(item, index) in list" :key="index">
        {{ item.name }}
      </div>
    </upLoadDownRefresh>

Просто поместите компонент в легкое, чтобы получить эффект.

Реализация: Версия 1. Реализовать контейнер, который можно перетаскивать и возвращать в исходное положение после отпускания пальца.

<template>
  <div>
    <div id="scroll-container" 
      @touchstart.stop="handlerTouchStart"
      @touchmove.stop="handlerTouchMove"
      @touchend.stop="handlerTouchEnd"
      ref="scrollContainer"
    >
      <slot></slot>
    </div>
  </div>
</template>
  
<script>
export default {
  data () {
    return {
      startLocation: '', // 记录鼠标点击的位置
      moveDistance: 0,  // 记录移动的位置
      distance: '' // 记录移动的距离
    }
  },
  methods: {
    // 获取手指触屏时的屏幕Y轴位置
    handlerTouchStart (e) {
      this.startLocation = e.touches[0].pageY
    },
    // 获取手指移动的距离
    handlerTouchMove (e) {
      this.moveDistance = Math.floor(e.touches[0].pageY - this.startLocation)
      this.$refs.scrollContainer.style.transform = `translateY(${this.moveDistance}px)`
    },
    // 获取手指松开的Y轴位置
    handlerTouchEnd (e) {
      // 清除已移动的距离
      this.moveDistance = 0
      // 恢复原位
      this.$refs.scrollContainer.style.transform = 'translateY(0px)'
    }
  }
}
</script>
<style scoped>
  #scroll-container {
    background-color: yellow;
  }
</style>

В основном с помощью события touchstart, чтобы получить расстояние по оси Y движения контейнера, затем вычислить расстояние по оси Y движения пальца в событии touchmove и переместить, установив transform: translateY(), и, наконец, восстановить исходное положение в преобразование события касания: translateY( 0px).

Задержка восстановления рендеринга

Из приведенных выше рендеров видно, что процесс отпускания пальца для восстановления положения восстанавливается мгновенно, и эффект не является дружественным для пользователя, далее мы его улучшим. добавлениемtransitionРеализуйте отложенное восстановление.

Давай, по коду:

<template>
  <div>
    <div id="scroll-container" 
	  ...
      :class="{'transition': isTransition}"
    >
      <slot></slot>
    </div>
  </div>
</template>

<script>
export default {
  data () {
    return {
	  ...
      isTransition: false // 是否启动transition
      ...
    }
  },
  methods: {
    // 获取手指触屏时的屏幕Y轴位置
    handlerTouchStart (e) {
      ...
      this.isTransition = false
      ...
    },
    // 获取手指松开的Y轴位置
    handlerTouchEnd (e) {
      ...
      this.isTransition = true // 开启transition
      ...
    }
  }
}
</script>
<style scoped>
  ...
  .transition {
    transition: all .7s; 
  }
  ...
</style>

Задержка восстановления может быть достигнута путем добавления вышеуказанного кода, в основном с помощьюtransitionЭтот стиль css реализован и переданthis.isTransitionчтобы определить нужно ли активировать стиль, ведь стиль только при отпускании пальца, т.е.touchend事件добавляется, когдаtouchstart事件закрытие.

Рендеринг версии 2:

Drag and drop - эффект восстановления отсутствует, выглядит хорошо, но еще много чего не хватает.Например, отсутствует анимация загрузки анимации при потягивании вниз для обновления и подтягивании для загрузки, что крайне плохо для пользователя опыт. , теперь добавим эти две анимации.

Версия 2: добавление анимации загрузки и обновления

Изменить часть кода:

<template>
  <div>
    <div id="scroll-container" 
      @touchstart.stop="handlerTouchStart"
      @touchmove.stop="handlerTouchMove"
      @touchend.stop="handlerTouchEnd"
      ref="scrollContainer"
      :class="{'transition': isTransition}"
    >
      <!-- 添加刷新图片 -->
      <div class="refresh">
        <img 
          src="https://www.easyicon.net/api/resizeApi.php?id=1190769&size=48" 
        >
      </div>
      <slot></slot>
      <!-- 添加加载图片 -->
      <div class="load">
        <img src="https://img.lanrentuku.com/img/allimg/1212/5-121204193R5-50.gif">
      </div>
    </div>
  </div>
</template>
  
<script>
export default {
  data () {
    return {
      startLocation: '', // 记录鼠标点击的位置
      moveDistance: 0,  // 记录移动的位置
      distance: '', // 记录移动的距离
      isTransition: false // 是否启动transition
    }
  },
  methods: {
    // 获取手指触屏时的屏幕Y轴位置
    handlerTouchStart (e) {
      this.isTransition = false
      this.startLocation = e.touches[0].pageY
    },
    // 获取手指移动的距离
    handlerTouchMove (e) {
      this.moveDistance = Math.floor(e.touches[0].pageY - this.startLocation)
      this.$refs.scrollContainer.style.transform = `translateY(${this.moveDistance}px)`
    },
    // 获取手指松开的Y轴位置
    handlerTouchEnd (e) {
      this.moveDistance = 0 // 清除已移动的距离
      this.isTransition = true // 开启transition
      this.$refs.scrollContainer.style.transform = 'translateY(0px)'
    }
  }
}
</script>
<style scoped>
  #scroll-container {
    background-color: yellow;
  }
  .transition {
    transition: all .7s; 
  }
  /* -----添加新样式------ */
  .load, .refresh {
    text-align: center;
  }
  .load img, .refresh img {
    width: 20px;
  }
  /* -----添加新样式------ */
</style>

Изображение эффекта:

В настоящее время箭头,加载статичны, а сейчас есть такой спрос, пусть выше箭头Стрелка указывает вниз, когда я тяну вниз и когда я отпускаю палец箭头направлен вверх, при возвращении в исходное положение,箭头стали加载. а также下拉刷新а также上拉加载Срабатывает при перетаскивании на определенное расстояние.

Модификация HTML-кода:

<template>
  <div>
    <div id="scroll-container" 
      @touchstart.stop="handlerTouchStart"
      @touchmove.stop="handlerTouchMove"
      @touchend.stop="handlerTouchEnd"
      ref="scrollContainer"
      :class="{'transition': isTransition}"
    >
      <!-- 根据isDisplay.refresh 动态隐藏显示 -->
      <div :class="['refresh', {'display': isDisplay.refresh}]">
        <!-- 添加isShrnked 加载 和 箭头相互转换 -->
        <!-- 添加rotate类 反转箭头 下箭头和上箭头互相转换 -->
        <img
          :src="isShrinked? loadImg : refreshImg" 
          :class="{'rotate': isRotate}"
        >
      </div>
      <slot></slot>
      <!-- 根据isDisplay.load 动态隐藏显示 -->
      <div :class="['load', {display: isDisplay.load}]">
        <img :src="loadImg">
      </div>
    </div>
  </div>
</template>

Стрелка ↑ и стрелка ↓ преобразуются с помощью transform: rotate(180deg), здесь адреса загрузки картинок и обновления картинок хранятся в переменных для облегчения динамического переключения.

Модификация кода JS:

<script>
// 拖拽状态 true:下拉  false:上拉
let SCROLLSTATUS
export default {
  props: {
    // 能够拖拽的最大距离
    maxDistance: {
      style: Number,
      default: 300
    },
    // 定义触发加载刷新事件的拉伸长度
    triggerDistance: {
      style: Number,
      default: 100
    }
  },
  data () {
    return {
      startLocation: '', // 记录鼠标点击的位置
      moveDistance: 0,  // 记录移动的位置
      distance: '', // 记录移动的距离
      isTransition: false, // 是否启动transition
      isDisplay: {
        refresh: true,
        load: true
      },
      // 把图片地址抽离出来 方便动态切换
      loadImg: 'https://img.lanrentuku.com/img/allimg/1212/5-121204193R5-50.gif',
      refreshImg: 'https://www.easyicon.net/api/resizeApi.php?id=1190769&size=48',
      isRotate: false, // 是否选择箭头
      isShrinked: false // 是否收缩完成
    }
  },
  methods: {
    // 获取手指触屏时的屏幕Y轴位置
    handlerTouchStart (e) {
      this.isTransition = false
      this.startLocation = e.touches[0].pageY
      // 重置箭头反转
      this.isRotate = false
      // 重置箭头
      this.isShrinked = false
    },
    // 获取手指移动的距离
    handlerTouchMove (e) {
      if (this.moveDistance > this.maxDistance + 1) {
        this.isRotate = true
        return
      }
      this.moveDistance = Math.floor(e.touches[0].pageY - this.startLocation)
      this.$refs.scrollContainer.style.transform = `translateY(${this.moveDistance}px)`
      // 显示加载 刷新图片
      if (this.moveDistance > this.triggerDistance && this.isDisplay.refresh) {
        this.isDisplay.refresh = false
      } else if (this.moveDistance < -this.triggerDistance && this.isDisplay.load) {
        this.isDisplay.load = false
      }
    },
    // 获取手指松开的Y轴位置
    handlerTouchEnd (e) {
      // 记录拖拽状态是为上拉还是下拉
      SCROLLSTATUS = this.moveDistance > 0
      this.isTransition = true // 开启transition
      this.$refs.scrollContainer.style.transform = 'translateY(0px)'
      if (Math.abs(this.moveDistance) < this.triggerDistance) return (this.moveDistance = 0)
      this.moveDistance = 0 // 清除已移动的距离
      // 拖拽距离是否大于指定的触发长度
      // 容器位置恢复后触发
      setTimeout(() => {
        this.shrinked()
      }, 700)
    },
    // 容器恢复后的操作
    shrinked () {
      if (SCROLLSTATUS) {
        // 下拉刷新业务逻辑
        // 已经恢复完,箭头转为加载
        this.isShrinked = true
        // 隐藏刷新、加载
        this.isDisplay.refresh = true
        this.isDisplay.load = true
        alert('这是下拉操作')
      } else {
        // 上拉加载业务逻辑
        alert('这是上拉操作')
      }
    }
  }
}
</script>

Объем информации модифицированный этим кодом большой, читать нужно внимательно... Добавим два свойства PROPS:maxDistanceа такжеtriggerDistance.

  • maxDistance: эта переменная представляет собой максимальное расстояние, которое можно перетащить.
  • triggerDistance: эта переменная представляет собой расстояние, которое запускает загрузку и обновление.

визуализация

На данный момент остается только последний шаг, который заключается в передаче событий обновления и загрузки во внешние компоненты.Когда нам нужно создавать события?

я определяюprops.triggerDistanceАтрибут, обновление и загрузка будут запускаться только тогда, когда расстояние перетаскивания больше этого значения, поэтому мы находимся в@touchmoveСобытие выдается в событии для использования внешними компонентами, но оно будет срабатывать до тех пор, пока перемещается мышь.@touchmoveСобытия не всегда могут запускаться непрерывноVue.$emit()Да, это слишком сильно влияет на производительность.

Решение состоит в том, чтобы использовать массив для хранения обновления и загрузкиemit()метод и, наконец,@touchendВыньте его и выполните, так что он будет выполнен только один разНаконец, метод emit() выполняется в `@touchend`, так что он будет выполнен только один раз`метод emit()`.

Версия 3 Добавление событий обновления и загрузки

Изменить JS:

<script>
// 拖拽状态 true:下拉  false:上拉
let SCROLLSTATUS
export default {
  props: {
	...
  },
  data () {
    return {
      ...
      // 添加emit缓存数组,并以undefined填充
      emitEvents: new Array(2).fill(undefined)
      ...
    }
  },
  methods: {
    // 获取手指移动的距离
    handlerTouchMove (e) {
      if (this.moveDistance > this.maxDistance + 1) {
        this.isRotate = true
        return
      }
      this.moveDistance = Math.floor(e.touches[0].pageY - this.startLocation)
      this.$refs.scrollContainer.style.transform = `translateY(${this.moveDistance}px)`
      // 显示加载 刷新图片
      if (this.moveDistance > this.triggerDistance && this.isDisplay.refresh) {
        this.isDisplay.refresh = false
      } else if (this.moveDistance < -this.triggerDistance && this.isDisplay.load) {
        this.isDisplay.load = false
      }
      // 缓存刷新的emit
      if (this.moveDistance > this.triggerDistance && !this.emitEvents[0]) {
      	this.emitEvents[0] = function () { this.$emit('refresh', this.displayDiv) }
      }
      // 缓存加载的emit
      if (this.moveDistance < -this.triggerDistance && !this.emitEvents[1]) {
      	this.emitEvents[1] = function () { this.$emit('load', this.displayDiv) }
      }
    },
    // 获取手指松开的Y轴位置
    handlerTouchEnd (e) {
      // 记录拖拽状态是为上拉还是下拉
      SCROLLSTATUS = this.moveDistance > 0
      this.isTransition = true // 开启transition
      this.$refs.scrollContainer.style.transform = 'translateY(0px)'
      if (Math.abs(this.moveDistance) < this.triggerDistance) return (this.moveDistance = 0)
      this.moveDistance = 0 // 清除已移动的距离
      // 拖拽距离是否大于指定的触发长度
      // 容器位置恢复后触发
      setTimeout(() => {
        this.shrinked()
      }, 700)
      // 遍历emit并执行
      this.emitEvents.forEach((fn, index) => {
        if (!fn) return
        this.emitEvents[index] = undefined
        fn.apply(this)
      })
    },
    // 容器恢复后的操作
    shrinked () {
      if (SCROLLSTATUS) {
        // 下拉恢复完,箭头转为加载
        this.isShrinked = true
      } else {
        // 上拉回复完
      }
    },
    // 该方法通过$emit()传给外部组件调用 然后隐藏刷新、加载的gif图片
    displayDiv () {
      this.isDisplay.refresh = true
      this.isDisplay.load = true
    }
  }
}
</script>

Код здесь в основном проверяет длину перетаскивания, чтобы длина была больше, чемthis.triggerDistanceЗатем выполняются операции обновления и загрузки, и каждый раз, когда он проверяется, он будет оцениваться.this.emitEventsМассив уже существуетemit()метод, пропустить, если он существует, сохранить, если он не существует, и, наконец, в@touchendобход центраthis.emitEvents. с вызовом$emitПри вызове метода метод скрытия gif передается внешнему компоненту для вызова.

Компоненты используют:

<template>
  <div>
    <refreshLoad 
      @refresh="refresh" 
      @load="load" 
      :maxDistance="300"
      :triggerDistance="100"
     >
      <div v-for="(item, index) in list" :key="index">
        {{ item.name }}
      </div>
    </refreshLoad>
  </div>
</template>
  
<script>
import refreshLoad from './refreshLoading'
export default {
  data () {
    return {
      timer: '',
      list: [
        {
          name: '张三'
        },
        {
          name: '李四'
        },
        {
          name: '王五'
        },
        {
          name: '赵六'
        }
      ],
      date: '',
      show: false
    }
  },
  components: {
    refreshLoad
  },
  methods: {
    refresh (done) {
      console.log(done)
      if (this.timer) clearTimeout(this.timer)
      this.timer = setTimeout(() => {
        console.log('refresh')
        done()
      }, 2000)
    },
    load (done) {
      if (this.timer) clearTimeout(this.timer)
      this.timer = setTimeout(() => {
        done()
        this.list = this.list.concat([{
          name: '新增的' + Math.ceil(Math.random() * 10)
        }, {
          name: '新增的' + Math.ceil(Math.random() * 10)
        }, {
          name: '新增的' + Math.ceil(Math.random() * 10)
        }])
      }, 1000)
    }
  }
}
</script>

Здесь важно отметить, что@refreshа также@loadМетод принимает параметрыdone,здесьdoneв компонентеdisplayDivметод, пользователь скрывает загруженное и обновленное изображение gif.

окончательные визуализации

быть улучшенным

Если нет адаптации мобильного терминала, вам придется перейти к размеру пикселя в компоненте в соответствии с вашим личным проектом разработки.

В нем нет разделяющих функций вытягивания и подтягивания, они смешаны, и заинтересованные друзья могут изменить их самостоятельно.

Загружено в нпм

Я загрузил модифицированный компонент в npm, который можно установить и использовать напрямую.

  • Установить:

npm i -s refresh-load-plugin

  • использовать:
import upLoadDownRefresh from 'refresh-load-plugin'
import 'refresh-load-plugin/lib/refresh-load-plugin.css'
Vue.use(upLoadDownRefresh)
<template>
  <upLoadDownRefresh 
    @refresh="refresh" 
    @load="load" 
    :maxDistance="300"
    :triggerDistance="100"
   >
    <div v-for="(item, index) in list" :key="index">
      {{ item.name }}
    </div>
  </upLoadDownRefresh>
</template>