第三方插件封装组合式函数
前言
在项目中引用了拖拽第三方插件 sortable,拖拽时数据并没有发生变化,还是旧的数据。
<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 方法来更新数据。
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 回调函数,这样太麻烦,可封装一个组合式函数来解决。
<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>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 回调中调用使用者的回调,再实现剩余功能。
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()
})
})
}<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 方法销毁实例。
<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 就行了。代码如下:
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
}理想很丰满,但是一运行代码,点击后报错:
Uncaught TypeError: Cannot read properties of null (reading 'destroy')
at handleDestroy这是为什么呢?因为JavaScript代码执行顺序问题。onMounted 是一个回调函数,还没执行就 return 了,因此外部拿到的还是 null。那怎么办呢?最简单的方法是用 ref 来保存 instance,这样就可以在 onMounted 之前使用 instance 了。
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 组件中引入组合式函数,通过 defineProps 和 defineModel 接收参数。在模板中不写 div 这种渲染节点,而是直接一个 <slot></slot> 插槽展示内容。
父组件引入这个组件,用默认插槽展示需要展示的内容即可。
以 useSortable 为例,代码如下:
<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><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 数据。再 defineExpose 将 instance 暴露给父组件使用。
在模板中,通过插槽也能把参数传递给父组件,父组件通过 v-slot 接收参数。
上述示例代码中,严格来说不算是无渲染组件,因为在模板中有一个
ul标签。但是这个ul标签是Sortable的容器,因此可以认为这个ul标签是必须的。其他不需要挂载容器的组合式函数,可以不需要渲染节点,就能真正意义上实现无渲染组件。