跳转到内容

第三方插件封装组合式函数

刀刀
0字
0分钟
2026/8/16

前言

在项目中引用了拖拽第三方插件 sortable,拖拽时数据并没有发生变化,还是旧的数据。

vue
<script setup lang="ts">
import { ref, useTemplateRef, onMounted } from 'vue'
import Sortable from 'sortablejs'

const list = ref([
  { id: 1, name: '张三' },
  { id: 2, name: '李四' },
  { id: 3, name: '王五' },
  { id: 4, name: '赵六' },
  { id: 5, name: '钱七' },
])

const containerRef = useTemplateRef('containerRef')

onMounted(() => {
  Sortable.create(containerRef.value, {
    onStart: (evt) => {
      // 给拖拽的元素添加样式类
      evt.item.classList.add('bg-amber')
    },
    onEnd: (evt) => {
      // 移除拖拽样式类
      evt.item.classList.remove('bg-amber')
    },
    onUpdate: (item) => {
      console.log('item', item)
    },
  })
})
</script>

<template>
  <div class="flex gap-20">
    <ul ref="containerRef" class="flex flex-col gap-10 list-none">
      <li
        v-for="item in list"
        :key="item.id"
        class="flex justify-center items-center w-100 h-40 bg-#7553db text-#fff rounded-5 cursor-pointer"
      >
        {{ item.name }}
      </li>
    </ul>

    <div class="w-120">{{ JSON.stringify(list, null, 2) }}</div>
  </div>
</template>

<style scoped></style>

效果图

可以看到,list 的数据并没有发生变化,还是旧的数据。

数据更新

那么,如何让数据更新?

Sortable 插件提供了 onUpdate 回调函数,当拖拽结束后,会触发该回调函数,先看一下打印出来的内容。

打印出来的内容

打印出来是一个对象,其中 oldIndex 是拖拽前的索引,newIndex 是拖拽后的索引。那么可以通过 splice 方法来更新数据。

js
onMounted(() => {
  Sortable.create(containerRef.value, {
    onStart: (evt) => {
      // 给拖拽的元素添加样式类
      evt.item.classList.add('bg-amber')
    },
    onEnd: (evt) => {
      // 移除拖拽样式类
      evt.item.classList.remove('bg-amber')
    },
    onUpdate: (item) => {
      const { oldIndex, newIndex } = item 
      const oldValue = list.value[oldIndex] 
      list.value.splice(oldIndex, 1) 
      list.value.splice(newIndex, 0, oldValue) 
    },
  })
})

现在页面的数据已经更新了。

组合式函数封装

但是,每次使用第三方插件时都需写一遍 onUpdate 回调函数,这样太麻烦,可封装一个组合式函数来解决。

vue
<script setup lang="ts">
import { ref, useTemplateRef, onMounted } from 'vue'
import { useSortable } from './useSortable'

const list = ref([
  { id: 1, name: '张三' },
  { id: 2, name: '李四' },
  { id: 3, name: '王五' },
  { id: 4, name: '赵六' },
  { id: 5, name: '钱七' },
])

const containerRef = useTemplateRef('containerRef')

useSortable(containerRef, list)
</script>

<template>
  <div class="flex gap-20">
    <ul ref="containerRef" class="flex flex-col gap-10 list-none">
      <li
        v-for="item in list"
        :key="item.id"
        class="flex justify-center items-center w-100 h-40 bg-#7553db text-#fff rounded-5 cursor-pointer"
      >
        {{ item.name }}
      </li>
    </ul>

    <div class="w-120">{{ JSON.stringify(list, null, 2) }}</div>
  </div>
</template>

<style scoped></style>
ts
import Sortable from 'sortablejs'
import { onMounted, onUnmounted } from 'vue'

export const useSortable = (containerRef: any, list: any[], options?: any) => {
  onMounted(() => {
    const instance = Sortable.create(containerRef.value, {
      onStart: (evt) => {
        // 给拖拽的元素添加样式类
        evt.item.classList.add('bg-amber')
      },
      onEnd: (evt) => {
        // 移除拖拽样式类
        evt.item.classList.remove('bg-amber')
      },
      onUpdate: (item) => {
        const { oldIndex, newIndex } = item
        const oldValue = list.value[oldIndex]
        list.value.splice(oldIndex, 1)
        list.value.splice(newIndex, 0, oldValue)
      },
    })

    // 在组件卸载时销毁 sortable 实例
    onUnmounted(() => {
      instance.destroy()
    })
  })
}

现在,只需调用 useSortable 函数即可实现拖拽功能,且数据也会更新。

如果使用者还有其他的方法和属性,可在 options 中传入,实现更灵活的配置。例如传入 animation 属性设置拖拽时的动画效果。

如果使用者还传入自己的 onUpdate 回调函数,想要实现额外需求,可在 onUpdate 回调中调用使用者的回调,再实现剩余功能。

ts
import Sortable from 'sortablejs'
import { onMounted, onUnmounted } from 'vue'

export const useSortable = (
  containerRef: any,
  list: any[],
  options?: any = {},
) => {
  onMounted(() => {
    const instance = Sortable.create(containerRef.value, {
      ...options, 
      onStart: (evt) => {
        // 给拖拽的元素添加样式类
        evt.item.classList.add('bg-amber')
      },
      onEnd: (evt) => {
        // 移除拖拽样式类
        evt.item.classList.remove('bg-amber')
      },
      onUpdate: (item) => {
        options?.onUpdate?.(item) 
        const { oldIndex, newIndex } = item
        const oldValue = list.value[oldIndex]
        list.value.splice(oldIndex, 1)
        list.value.splice(newIndex, 0, oldValue)
      },
    })

    // 在组件卸载时销毁 sortable 实例
    onUnmounted(() => {
      instance.destroy()
    })
  })
}
vue
<script setup lang="ts">
import { ref, useTemplateRef, onMounted } from 'vue'
import { useSortable } from './useSortable'

const list = ref([
  { id: 1, name: '张三' },
  { id: 2, name: '李四' },
  { id: 3, name: '王五' },
  { id: 4, name: '赵六' },
  { id: 5, name: '钱七' },
])

const containerRef = useTemplateRef('containerRef')

useSortable(containerRef, list, {
  animation: 150,
  onUpdate: (e) => {
    console.log('我自己的需求', e)
  },
})
</script>

<template>
  <div class="flex gap-20">
    <ul ref="containerRef" class="flex flex-col gap-10 list-none">
      <li
        v-for="item in list"
        :key="item.id"
        class="flex justify-center items-center w-100 h-40 bg-#7553db text-#fff rounded-5 cursor-pointer"
      >
        {{ item.name }}
      </li>
    </ul>

    <div class="w-120">{{ JSON.stringify(list, null, 2) }}</div>
  </div>
</template>

<style scoped></style>

实例获取

有时候,使用者想要获取 instance 实例,以便做自己的操作。例如手动调用 destroy 方法销毁实例。

index.vue
vue
<script setup lang="ts">
import { ref, useTemplateRef, onMounted } from 'vue'
import { useSortable } from './useSortable'

const list = ref([
  { id: 1, name: '张三' },
  { id: 2, name: '李四' },
  { id: 3, name: '王五' },
  { id: 4, name: '赵六' },
  { id: 5, name: '钱七' },
])

const containerRef = useTemplateRef('containerRef')

const instance = useSortable(containerRef, list, {
  animation: 150,
  onUpdate: (e) => {
    console.log('我自己的需求', e)
  },
})

const handleDestroy = () => {
  instance.destroy()
}
</script>

<template>
  <div>
    <div class="flex gap-20">
      <ul ref="containerRef" class="flex flex-col gap-10 list-none">
        <li
          v-for="item in list"
          :key="item.id"
          class="flex justify-center items-center w-100 h-40 bg-#7553db text-#fff rounded-5 cursor-pointer"
        >
          {{ item.name }}
        </li>
      </ul>

      <div class="w-120">{{ JSON.stringify(list, null, 2) }}</div>
    </div>
    <el-button @click="handleDestroy">手动销毁实例</el-button>
  </div>
</template>

<style scoped></style>

有人说那简单,直接在 useSortable 函数中返回 instance 就行了。代码如下:

useSortable.ts
ts
import Sortable from 'sortablejs'
import { onMounted, onUnmounted } from 'vue'

export const useSortable = (
  containerRef: any,
  list: any[],
  options?: any = {},
) => {
  let instance: any = null
  onMounted(() => {
    instance = Sortable.create(containerRef.value, {
      ...options,
      onStart: (evt) => {
        // 给拖拽的元素添加样式类
        evt.item.classList.add('bg-amber')
      },
      onEnd: (evt) => {
        // 移除拖拽样式类
        evt.item.classList.remove('bg-amber')
      },
      onUpdate: (item) => {
        options?.onUpdate?.(item)
        const { oldIndex, newIndex } = item
        const oldValue = list.value[oldIndex]
        list.value.splice(oldIndex, 1)
        list.value.splice(newIndex, 0, oldValue)
      },
    })

    // 在组件卸载时销毁 sortable 实例
    onUnmounted(() => {
      instance.destroy()
    })
  })

  return instance 
}

理想很丰满,但是一运行代码,点击后报错:

js
Uncaught TypeError: Cannot read properties of null (reading 'destroy')
    at handleDestroy

这是为什么呢?因为JavaScript代码执行顺序问题。onMounted 是一个回调函数,还没执行就 return 了,因此外部拿到的还是 null。那怎么办呢?最简单的方法是用 ref 来保存 instance,这样就可以在 onMounted 之前使用 instance 了。

useSortable.ts
ts
import Sortable from 'sortablejs'
import { ref, onMounted, onUnmounted } from 'vue'

export const useSortable = (
  containerRef: any,
  list: any[],
  options?: any = {},
) => {
  let instance: any = ref(null) 
  onMounted(() => {
    instance.value = Sortable.create(containerRef.value, {
      ...options,
      onStart: (evt) => {
        // 给拖拽的元素添加样式类
        evt.item.classList.add('bg-amber')
      },
      onEnd: (evt) => {
        // 移除拖拽样式类
        evt.item.classList.remove('bg-amber')
      },
      onUpdate: (item) => {
        options?.onUpdate?.(item)
        const { oldIndex, newIndex } = item
        const oldValue = list.value[oldIndex]
        list.value.splice(oldIndex, 1)
        list.value.splice(newIndex, 0, oldValue)
      },
    })

    // 在组件卸载时销毁 sortable 实例
    onUnmounted(() => {
      instance.value.destroy() 
    })
  })

  return instance
}

无渲染组件方式

大部分的组合式函数都可以做成无渲染组件。

无渲染组件咋一听感觉很高端,实际上就是在一个新的 .vue 组件中引入组合式函数,通过 definePropsdefineModel 接收参数。在模板中不写 div 这种渲染节点,而是直接一个 <slot></slot> 插槽展示内容。

父组件引入这个组件,用默认插槽展示需要展示的内容即可。

useSortable 为例,代码如下:

vue
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue'
import VueSortable from './useSortable.vue'

const list = ref([
  { id: 1, name: '张三' },
  { id: 2, name: '李四' },
  { id: 3, name: '王五' },
  { id: 4, name: '赵六' },
  { id: 5, name: '钱七' },
])

const vueSortable = useTemplateRef('vueSortable')

const handleDestroy = () => {
  vueSortable.value.instance.destroy()
}
</script>

<template>
  <div>
    <div class="flex gap-40">
      <VueSortable ref="vueSortable" :animation="150" v-model="list">
        <template v-slot="{ instance }">
          <li
            v-for="item in list"
            :key="item.id"
            class="flex justify-center items-center w-120 h-40 bg-#7553db text-#fff rounded-5 cursor-pointer"
          >
            {{ item.name }}
          </li>
        </template>
      </VueSortable>
    </div>
    <el-button class="mt-20" @click="handleDestroy">手动销毁实例</el-button>
  </div>
</template>

<style scoped></style>
vue
<script setup lang="ts">
import { useTemplateRef, useAttrs } from 'vue'
import { useSortable } from './useSortable'

const modelValue = defineModel()

const containerRef = useTemplateRef('containerRef')

const instance = useSortable(containerRef, modelValue, useAttrs())
defineExpose({ instance })
</script>

<template>
  <ul class="flex flex-col gap-10 list-none" ref="containerRef">
    <slot :instance="instance"></slot>
  </ul>
</template>

<style scoped></style>

子组件主要使用 defineModel 接收父组件传递的 v-model,使用 useAttrs 接收父组件全部的 v-bind 参数作为 options 数据。再 defineExposeinstance 暴露给父组件使用。

在模板中,通过插槽也能把参数传递给父组件,父组件通过 v-slot 接收参数。

上述示例代码中,严格来说不算是无渲染组件,因为在模板中有一个 ul 标签。但是这个 ul 标签是 Sortable 的容器,因此可以认为这个 ul 标签是必须的。其他不需要挂载容器的组合式函数,可以不需要渲染节点,就能真正意义上实现无渲染组件。

动手实操

跳转预览:点击跳转

贡献者

The avatar of contributor named as duyidao duyidao
The avatar of contributor named as 刀刀 刀刀

页面历史

刀刀博客累计访客 人;文档累计访问量共