【山竹记账前端-vue】7.滑动手势封装 useSwipe Hook


大纲链接 §

[toc]


1. 重构拆分组件 ⇧

拆分组件 @/views/WelcomeView.tsx

  • 注意原本纯函数式写法不支持vue组件生命周期钩子函数
    • 在纯函数组建中使用生命周期钩子的话会报警告
    • [Vue warn]:onMounted is called when there is no active component instance to be associated with.
    • Lifecycle injection APIs can only be used during execution of setup().
  • 在拆分的组件 @/modules/welcome/MainSwiper.tsx 中改为使用 defineComponent 用法

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    import { defineComponent } from 'vue'
    import { WelcomeHeader } from '@/modules/welcome/WelcomeHeader.tsx'
    import { WelcomeSkipper } from '@/modules/welcome/WelcomeSkipper.tsx'
    import { MainSwiper } from '@/modules/welcome/MainSwiper.tsx'
    import s from '@/views/WelcomeView.module.scss'
    
    export const WelcomeView = defineComponent(
    () => {
    return () => (
      <div class={s.wrapper}>
        <WelcomeHeader />
    
        <MainSwiper />
    
        <WelcomeSkipper />
      </div>
    )
    },
    {
    props: {},
    name: 'WelcomeView',
    },
    )
    

@/modules/welcome/WelcomeHeader.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { defineComponent } from 'vue'
import s from '@/views/WelcomeView.module.scss'
import { SvgIcon } from '@/components/icons/SvgIcon.tsx'
import { cn } from 'cn'

export const WelcomeHeader = defineComponent({
  name: 'WelcomeHeader',
  setup(/*props, ctx*/) {
    return () => (
      <header
        class={cn(
          s.title,
          'flex flex-shrink-1 flex-grow-0 flex-col items-center justify-center',
          'm-y-2em m-x-0 pt-10%',
          'text-2em color-[var(--title-text)]',
        )}>
        <SvgIcon class="w-64px h-69px" name="mangosteen" />
        <h1>山竹记账</h1>
      </header>
    )
  },
})

@/modules/welcome/WelcomeSkipper.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { defineComponent } from 'vue'
import { cn } from 'cn'

export const WelcomeSkipper = defineComponent({
  name: 'WelcomeSkipper',
  setup(/*props, ctx*/) {
    return () => (
      <section
        class={cn(
          'color-[var(--title-text)] text-[1.5em]',
          'top-0.5em right-0.5em fixed',
        )}>
        跳过
      </section>
    )
  },
})

@/modules/welcome/MainSwiper.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import { defineComponent, onMounted, Transition, useTemplateRef, type VNode } from 'vue'
import { type RouteLocationNormalizedLoaded, RouterView } from 'vue-router'
import { cn } from 'cn'
import '@/modules/welcome/MainSwiper.scoped.scss'

export const MainSwiper = defineComponent({
  name: 'MainSwiper',
  props: {},
  components: {},
  setup(/*props, ctx*/) {
    const mainRef = useTemplateRef<HTMLElement>('mainRef')

    onMounted(() => {
      console.log('WelcomeView_______________________')
      console.log("%c 4 --> mainRef: ","color:#f00;", mainRef);
      console.log("%c 10 --> mainRef: ","color:#42b883;", mainRef.value);
      console.log('_______________________WelcomeView')
    })

    return () => (
      <main
        ref="mainRef"
        class={cn([
          'max-h-[calc(100%-360px)]',
          'flex flex-col flex-1',
          'mb-62px mx-16px',
          'position-relative'
        ])}>
        <RouterView name="main">
          {({ Component, route }: { Component: VNode; route: RouteLocationNormalizedLoaded }) => (
            <Transition
              name="slide-fade"
              enterActiveClass="slide-fade-enter-active"
              leaveActiveClass="slide-fade-leave-active"
              enterFromClass="slide-fade-enter-from"
              leaveToClass="slide-fade-leave-to">
              <div
                key={route.path}
                class={cn([
                  'bg-welcomeCardBg rounded-lg',
                  'flex flex-col flex-grow items-center justify-around',
                  'position-absolute top-0 left-0 h-100% w-100%',
                ])}>
                {Component}
              </div>
            </Transition>
          )}
        </RouterView>
      </main>
    )
  },
})

@/modules/welcome/MainSwiper.scoped.scss

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.slide-fade-enter-active,
.slide-fade-leave-active {
  transition: all 0.8s ease-out;
}

.slide-fade-enter-from {
  transform: translateX(100vw);
  opacity: 0;
}

.slide-fade-leave-to {
  transform: translateX(-100vw);
  opacity: 0;
}

@/views/WelcomeView.module.scss

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.wrapper {
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  background-image: linear-gradient(
    to bottom,
    var(--welcome-card-start) 0%,
    var(--welcome-card-end) 100%
  );
}

2. 实现思路与为 useSwipe 搭框架 ⇧

支持滑动事件

  • 使用 Hooks,即使用 Composition Api 实现
  • 只支持在手机端触控滑动,不管 PC 鼠标事件

@/hooks/useSwipe.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import type { Ref } from 'vue'

// type Point = {x: number; y: number}
interface SwipeState {
  isSwiping: boolean
  direction: string
  distance: number
}

export const useSwipe = (
  elementRef: Ref<HTMLElement | null>
): SwipeState => {

  console.log('useSwipe_______________________')
  console.log("%c 2 --> elementRef: ","color:#58c4dc;", elementRef);
  console.log('_______________________useSwipe')

  // noinspection JSAnnotator
  return {
    isSwiping: false,
    direction: '',
    distance : 0
  }
}

引入到 @src/modules/welcome/MainSwiper.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// ...
export const MainSwiper = defineComponent({
  name: 'MainSwiper',
  props: {},
  components: {},
  setup(/*props, ctx*/) {
    const mainRef = useTemplateRef<HTMLElement>('mainRef')

    const {isSwiping}  =  useSwipe(mainRef)

    onMounted(() => {
      console.log('WelcomeView_______________________')
      console.log("%c 5 --> isSwiping: ","color:#ff6a00;", isSwiping);
      console.log("%c 4 --> mainRef: ","color:#f00;", mainRef);
      console.log("%c 10 --> mainRef: ","color:#42b883;", mainRef.value);
      console.log('_______________________WelcomeView')
    })

    return () => (
      <main
        ref="mainRef"
        class={cn([/* ... */])}>
        <RouterView name="main">
          {({ Component, route }: { Component: VNode; route: RouteLocationNormalizedLoaded }) => (
            <Transition
              name="slide-fade"
              enterActiveClass="slide-fade-enter-active"
              leaveActiveClass="slide-fade-leave-active"
              enterFromClass="slide-fade-enter-from"
              leaveToClass="slide-fade-leave-to">
              <div
                key={route.path}
                class={cn([/* ... */])}>
                {Component}
              </div>
            </Transition>
          )}
        </RouterView>
      </main>
    )
  },
})
  • 声明 const mainRef = useTemplateRef<HTMLElement>('mainRef')
  • 关联 <main ref="mainRef">

swipe_demo_1

3. 监听 Touch / Pointer 事件 ⇧

思路

  • onMounted 组件挂载后再监听事件
    • 手指先按下 对应 ontouchstart
    • 手指滑动 对应 ontouchmove
    • 手指松开 对应 ontouchend
  • 移动方向判定:手指按下的瞬间,记录一个坐标;在移动时,记录一个坐标,并对比大小
    • y 坐标轴向下,x 坐标轴向右
    • 松开时y坐标比按下时y坐标大,则向下移动
    • 松开时x坐标比按下时x坐标大,则向右移动

@/hooks/useSwipe.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import { computed, type ComputedRef, onMounted, onUnmounted, ref, type Ref } from 'vue'

type Point = { x: number; y: number }

interface SwipeState {
  isSwiping: Ref<boolean>
  direction: ComputedRef<'' | 'right' | 'left' | 'up' | 'down'>
  distance: ComputedRef<Point | null>
}

const calcDistance = (start: Point, end: Point) => {
  return {
    x: end.x - start.x,
    y: end.y - start.y,
  }
}

const calcDirection = (distance: Point) => {
  if(!distance) { return '' }
  const { x, y } = distance
  if(Math.abs(x) > Math.abs(y)) {
    return x > 0 ? 'right' : 'left'
  }
  return y > 0 ? 'down' : 'up'
}

export const useSwipe = (elementRef: Ref<HTMLElement | null>): SwipeState => {
  const start = ref<Point | null>(null)
  const end = ref<Point | null>(null)
  const isSwiping = ref(false)
  const distance = computed(() => {
    if(!start.value || !end.value) { return null }
    return calcDistance(start.value, end.value)
  })

  const direction = computed(() => {
    if(!distance.value || !isSwiping.value) { return '' }
    return calcDirection(distance.value)
  })

  const aborter = new AbortController()
  const opt = { signal: aborter.signal }

  const onStart = (e: TouchEvent) => {
    start.value = {
      x: e.touches[0]?.screenX ?? 0,
      y: e.touches[0]?.screenY ?? 0,
    }
    end.value = null
    isSwiping.value = true
  }

  const onMove = (e: TouchEvent) => {
    end.value = {
      x: e.touches[0]?.screenX ?? 0,
      y: e.touches[0]?.screenY ?? 0,
    }
  }

  const onEnd = () => {
    isSwiping.value = false
    start.value = null
    end.value = null
  }

  onMounted(() => {
    if(!elementRef.value) {return}
    elementRef.value.addEventListener('touchstart', onStart, opt)
    elementRef.value.addEventListener('touchmove', onMove, opt)
    elementRef.value.addEventListener('touchend', onEnd, opt)
  })

  onUnmounted(() => {aborter.abort()})

  // noinspection JSAnnotator
  return {
    isSwiping,
    direction,
    distance,
  }
}
  • 坐标数据类型 type Point = {x: number; y: number}
  • 记录开始按下的坐标 const start = ref<Point>()
  • 记录松开的坐标 const end = ref<Point>()
  • 只处理单指点击的情况 e.touches[0]
  • 记录是否在移动 const isSwiping = ref(false)
  • 计算移动距离:<开始坐标 - 结束坐标>
    • x: end.x - start.x
    • y: end.y - start.y
    • 使用计算属性做缓存
  • 根据 start 和 end 计算方向:
    • 先判断 x、y 绝对值距离更大的一方作为主要方向
    • 再根据 x、y 和 0 比较结果返回方向字符串
    • 使用计算属性做缓存
  • 回调函数 onStart
    • 记录开始位移坐标,清空上一次结束坐标位移
    • 将是否滑动状态设置为 true
  • 回调函数 onMove
    • 记录当前位移坐标为结束坐标
  • 回调函数 onEnd
    • 清空开始坐标和结束坐标
    • 将是否滑动状态设置为 false
  • 声明终止控制器 const aborter = new AbortController()
    • 声明监听事件选项 const opt = { signal: aborter.signal }
  • onMounted 组件挂载后再监听事件,添加三个回调,并提供参数选项 opt 终止信号
  • onUnmounted 组件卸载,终止控制器取消监听事件
    • aborter.abort()

参考


4. 滑动切换路由 + 重构 ⇧

统一编码规范之 ref 模板引用-泛型传参和默认值 ⇧

  • 由于纯函数式写法不支持vue组件生命周期钩子函数,而已注册的 ref 必须在组件挂载后才可以获取
  • 所以不支持 FunctionalComponent

推荐使用 ✅

1
2
3
const mainRef = useTemplateRef<HTMLElement>('mainRef')
// 或者
const divRef = ref<HTMLElement>()

不推荐使用 ❌

1
2
const mainRef = useTemplateRef<HTMLElement | null>(null)
const divRef = ref<HTMLElement | null>(null)

在返回的节点标签上注册

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import type { FunctionalComponent } from 'vue'
export const Welcome1stPage = defineComnent({
  setup() {
    return () => (
      <main ref="mainRef">
        <div ref={divRef}>
          ✅
        </div>

        <div ref="divRef">
          ✅
        </div>

        <main ref={mainRef}>❌ 无法获取</main>
      </main>
    )
  },
})

参考


获取当前路由实例跳转 ⇧

  • 获取 const router = useRouter()
  • 跳转 router.push('/welcome/2')
  • 判断仅当正在滑动,并且方向向左时,执行跳转

@/modules/welcome/MainSwiper.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// import ...
export const MainSwiper = defineComponent({
  name: 'MainSwiper',
  setup(/*props, ctx*/) {
    const mainRef = useTemplateRef<HTMLElement>('mainRef')
    const { isSwiping, direction }  =  useSwipe(mainRef)
    const router = useRouter()
    watchEffect(async () => {
      if(isSwiping.value && direction.value === 'left') {
        await router.push('/welcome/2')
      }
    })

    return () => (
      <main
        ref="mainRef"
        class={cn([
          'max-h-[calc(100%-360px)]',
          'flex flex-col flex-1',
          'mb-62px mx-16px',
          'position-relative'
        ])}>
        <RouterView name="main">
          {/* ... */}
        </RouterView>
      </main>
    )
  },
})

先完成两个欢迎页面跳转哈希表映射跳转逻辑 @/modules/welcome/MainSwiper.tsx

  • 使用 useRoute() 获取当前路由信息中的 name 属性

需要在路由表中添加各个路由的 name 属性,修改 @/router/routes.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import type { RouteRecordRaw } from 'vue-router'
import { WelcomeView } from '@/views/WelcomeView.tsx'
import { Welcome1stPage } from '@/modules/welcome/Welcome1stPage.tsx'
import { Welcome2ndPage } from '@/modules/welcome/Welcome2ndPage.tsx'
import { Welcome3rdPage } from '@/modules/welcome/Welcome3rdPage.tsx'
import { Welcome4thPage } from '@/modules/welcome/Welcome4thPage.tsx'

export const routes: RouteRecordRaw[] = [
  {
    path: '/',
    redirect: '/welcome/1',
  },
  {
    path: '/welcome',
    name: 'welcome',
    component: WelcomeView,
    children: [
      // { path: '', name: 'default', component: WelCome1stPage },
      { path: '', name: 'welcomeDefault', redirect: '/welcome/1' },
      { path: '1', name: 'welcomePig', components: { main: Welcome1stPage } },
      { path: '2', name: 'welcomeClock', components: { main: Welcome2ndPage } },
      { path: '3', name: 'welcomeChart', components: { main: Welcome3rdPage } },
      { path: '4', name: 'welcomeCloud', components: { main: Welcome4thPage } },
    ],
  },
]

@/modules/welcome/MainSwiper.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// import ...
export const MainSwiper = defineComponent({
  name: 'MainSwiper',
  setup(/*props, ctx*/) {
    const mainRef = useTemplateRef<HTMLElement>('mainRef')
    const { isSwiping, direction } = useSwipe(mainRef)
    const router = useRouter()

    const route = useRoute()
    watchEffect(async () => {
      console.log('MainSwiper_______________________')
      console.log('MainSwiper route.name:', route.name)
      console.log('_______________________MainSwiper')
      if (isSwiping.value && direction.value === 'left') {
        if('welcomePig' === route.name) {
          await router.push('/welcome/2')
        }else if('welcomeClock' === route.name) {
          await router.push('/welcome/3')
        }
      }
    })

    return () => (
      <main
        ref="mainRef"
        class={cn([
          'max-h-[calc(100%-360px)]',
          'flex flex-col flex-1',
          'mb-62px mx-16px',
          'position-relative',
        ])}>
        <RouterView name="main">
          {/* ... */}
        </RouterView>
      </main>
    )
  },
})

滑动事件连续触发,滑动一下就跳转到最后一页了

  • 需要添加Touch 事件节流控制

滑动触发 Touch 事件,使用节流控制 ⇧

@/shared/throttle.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
export const throttle = (fn: () => void, delay: number = 500) => {
    let timer: NodeJS.Timeout | null = null
    return () => {
      if (timer) {
        return
      }
      fn()
      timer = setTimeout(() => {
        timer = null
      }, delay)
    }
  }

使用节流控制滑动事件 @/modules/welcome/MainSwiper.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// import ...
import { useSwipe } from '@/hooks/useSwipe.ts'
import { throttle } from '@/shared/throttle.ts'

export const MainSwiper = defineComponent({
  name: 'MainSwiper',
  setup(/*props, ctx*/) {
    const mainRef = useTemplateRef<HTMLElement>('mainRef')
    const { isSwiping, direction } = useSwipe(mainRef)
    const router = useRouter()

    const route = useRoute()

    const push = throttle(async () => {
      if('welcomePig' === route.name) {
        await router.push('/welcome/2')
      }else if('welcomeClock' === route.name) {
        await router.push('/welcome/3')
      }
    }, 500)

    watchEffect(() => {
      if (isSwiping.value && direction.value === 'left') {
        push()
      }
      console.log('MainSwiper_______________________')
      console.log('MainSwiper route.name:', route.name)
      console.log('_______________________MainSwiper')
    })

    return () => (
      <main
        ref="mainRef"
        class={cn([
          'max-h-[calc(100%-360px)]',
          'flex flex-col flex-1',
          'mb-62px mx-16px',
          'position-relative',
        ])}>
        <RouterView name="main">
          {/* ... */}
        </RouterView>
      </main>
    )
  },
})

根据路由名称对应跳转哈希表进行跳转 ⇧

@/modules/welcome/MainSwiper.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// import ...

const routerToHash = {
  'welcomePig': '/welcome/2',
  'welcomeClock': '/welcome/3',
  'welcomeChart': '/welcome/4',
  'welcomeCloud': '/start',
}

export const MainSwiper = defineComponent({
  name: 'MainSwiper',
  setup(/*props, ctx*/) {
    const mainRef = useTemplateRef<HTMLElement>('mainRef')
    const { isSwiping, direction } = useSwipe(mainRef)
    const router = useRouter()
    const route = useRoute()
    const push = throttle(async () => {
      await router.push(routerToHash[route.name as keyof typeof routerToHash])
    }, 500)

    watchEffect(() => {
      if (isSwiping.value && direction.value === 'left') {
        push()
      }
    })

    return () => (
      <main
        ref="mainRef"
        class={cn([
          'max-h-[calc(100%-360px)]',
          'flex flex-col flex-1',
          'mb-62px mx-16px',
          'position-relative',
        ])}>
        <RouterView name="main">
          {/* ... */}
        </RouterView>
      </main>
    )
  },
})

如果不想用户回退,路由跳转方法使用 replace 代替 push 即可


添加配置选项,包含6个钩子函数,自定义3个滑动动作前后逻辑,添加阻止默认行为 ⇧

手势向左左滑,浏览器默认有一个左滑空白的弹性动画占据部分页面

  • 在 useSwipe 中的 onStart 的逻辑中调用 e.preventDefault() 来阻止
  • 但阻止逻辑并不是每一个事件逻辑都会需要的,可以通过参数传入来控制是否需要
  • 提供一个选项 useSwipe(elementRef: ref<HTMLElement>, options?: EventOptions)
  • 但 preventDefault 会影响所有子节点的事件默认行为,暂时不配置,仅实现 useSwipe 选项配置

@/hooks/useSwipe.ts

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import { computed, type ComputedRef, onMounted, onUnmounted, ref, type Ref } from 'vue'

type Point = { x: number; y: number }
interface SwipeState {
  isSwiping: Ref<boolean>
  direction: ComputedRef<'' | 'right' | 'left' | 'up' | 'down'>
  distance: ComputedRef<Point | null>
}
interface EventOptions {
  beforeStart?: (e: TouchEvent) => void,
  afterStart?: (e: TouchEvent) => void,
  beforeMove?: (e: TouchEvent) => void,
  afterMove?: (e: TouchEvent) => void,
  beforeEnd?: (e: TouchEvent) => void,
  afterEnd?: (e: TouchEvent) => void,
}

const calcDistance = (start: Point, end: Point) => {
  return {
    x: end.x - start.x,
    y: end.y - start.y,
  }
}

const calcDirection = (distance: Point) => {
  if (!distance) {
    return ''
  }
  const { x, y } = distance
  if (Math.abs(x) > Math.abs(y)) {
    return x > 0 ? 'right' : 'left'
  }
  return y > 0 ? 'down' : 'up'
}

export const useSwipe = (
  elementRef: Ref<HTMLElement | null>,
  eventOptions?: EventOptions,
): SwipeState => {
  const {
    beforeStart,
    afterStart,
    beforeMove,
    afterMove,
    beforeEnd,
    afterEnd,
  } = eventOptions ?? {}

  const start = ref<Point>()
  const end = ref<Point>()
  const isSwiping = ref(false)
  const distance = computed(() => {
    if (!start.value || !end.value) {
      return null
    }
    return calcDistance(start.value, end.value)
  })

  const direction = computed(() => {
    if (!distance.value || !isSwiping.value) {
      return ''
    }
    return calcDirection(distance.value)
  })

  const aborter = new AbortController()
  const opt = { signal: aborter.signal }

  const onStart = (e: TouchEvent) => {
    beforeStart?.(e)
    start.value = {
      x: e.touches[0]?.screenX ?? 0,
      y: e.touches[0]?.screenY ?? 0,
    }
    end.value = void 0
    isSwiping.value = true
    afterStart?.(e)
  }

  const onMove = (e: TouchEvent) => {
    beforeMove?.(e)
    end.value = {
      x: e.touches[0]?.screenX ?? 0,
      y: e.touches[0]?.screenY ?? 0,
    }
    afterMove?.(e)
  }

  const onEnd = (e: TouchEvent) => {
    beforeEnd?.(e)
    isSwiping.value = false
    start.value = void 0
    end.value = void 0
    afterEnd?.(e)
  }

  onMounted(() => {
    if (!elementRef.value) {
      return
    }
    elementRef.value.addEventListener('touchstart', onStart, opt)
    elementRef.value.addEventListener('touchmove', onMove, opt)
    elementRef.value.addEventListener('touchend', onEnd, opt)
  })

  onUnmounted(() => {
    aborter.abort()
  })

  // noinspection JSAnnotator
  return {
    isSwiping,
    direction,
    distance,
  }
}

5. 选看:TS 类型体操 ⇧

@/shared/throttle.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
export const throttle = <P>(fn: (...args: P[]) => void, delay: number = 500) => {
    let timer: NodeJS.Timeout | null = null
    return (...args: P[]) => {
      if (timer) {
        return
      }
      fn(...args)
      timer = setTimeout(() => {
        timer = null
      }, delay)
    }
  }
  • 或者声明一个函数泛型 F,使用 Parameters<F>

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    export const throttle = <F extends (...args: unknown[]) => any>(fn: F, delay: number = 500) => {
    let timer: NodeJS.Timeout | null = null
    let result: ReturnType<F>
    return (...args: Parameters<F>) => {
    if (timer) {
      return result
    }
    result = fn(...args)
    timer = setTimeout(() => {
      timer = null
    }, delay)
    return result
    }
    }
    

6. 选看:对比同行的代码,VueUse 社区的实现 usePointerSwipe 和 useSwipe ⇧

安装 @vueuse/core

1
pnpm i @vueuse/core@15.0.0

使用 usePointerSwipe

  • 引入 import { usePointerSwipe } from '@vueuse/core'
  • 替换 const { isSwiping, direction } = usePointerSwipe(mainRef) 即可

使用 useSwipe

  • 引入 import { useSwipe } from '@vueuse/core'
  • 替换 const { isSwiping, direction } = useSwipe(mainRef) 即可

总结 ⇧

  • 纯函数式写法不支持vue组件生命周期钩子函数,包括使用了生命周期钩子的自定义封装的钩子函数
  • 使用 AbortController 可卸载事件监听
  • 使用 e.preventDefault() 可阻止手机默认的整个页面弹性滑动效果

·未完待续·

参考文章

1~3

4~5

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名