最近在研究electron-vite 编写自己的东西 安装了vuedraggable 作者5年前就停止更新了 只兼容vue1 2版本 如果直接用sortablejs编写 不美观 且要处理 数组的数据更新 映射到界面上并且对重渲染重新绑定sortable实例的时机也要处理 今天讲下我踩坑的地方
根据原来vuedraggable的使用形式进行编写代码
vue3 option api 写法
<template>
<div v-if="refresh">
<!-- 使用动态组件 -->
<component
:is="tag"
id="custom-drag"
v-bind="attrs"
v-on="listeners"
>
<div
v-if="$slots.header"
class="disabled-filter"
>
<slot name="header" />
</div>
<slot></slot>
<div
v-if="$slots.footer"
class="disabled-filter"
>
<slot name="footer" />
</div>
</component>
</div>
</template>
<script>
import Sortable from 'sortablejs'
export default {
props: {
// v-model绑定
modelValue: {
type: Object,
default: () => {
return {}
}
},
// 自定义的tag组件
tag: {
type: String,
default: 'div'
},
// 绑定到动态组件上的数据
componentData: {
type: Object,
default: () => {
return {
on: {},
attrs: {},
props: {}
}
}
},
// 处理move事件 为了能拿到返回值
move: {
type: Function,
default: () => true
}
},
// 监听事件 映射回sortable配置上响应
emits: [
'update:modelValue',
'endEvent',
'chooseEvent',
'unchooseEvent',
'startEvent',
'onSetEvent',
'addEvent',
'updateEvent',
'sortEvent',
'removeEvent',
'filterEvent',
'cloneEvent',
'changeEvent'
],
data() {
return {
// 用于重渲染的标志位
refresh: true,
// sortable实例
sort: null
}
},
computed: {
// 映射model数据的操作
list: {
get() {
return this.modelValue
},
set(value) {
this.$emit('update:modelValue', value)
}
},
// :is 中 props和attrs 是直接写在标签上的 所以直接合并
attrs() {
let { attrs = {}, props = {} } = this.componentData
return { ...attrs, ...props }
},
// v-on 形式上的事件映射 需要+on
listeners() {
return this.componentData && this.componentData.on
},
// 如果有header 则索引+1
slotIndex() {
return this.$slots.header ? 1 : 0
}
},
watch: {
// 监听数据变化 重新渲染数组
list: {
handler(nv, ov) {
// 判断原实例是否存在并销毁
this.sort && this.sort.destroy()
// 重新渲染
this.createSort()
},
deep: true
}
},
mounted() {
this.createSort()
},
methods: {
// 处理索引
handleIndex(evt) {
if (evt.oldIndex !== null) evt.oldIndex -= this.slotIndex
if (evt.newIndex !== null) evt.newIndex -= this.slotIndex
if (evt.newDraggableIndex !== null) evt.newDraggableIndex -= this.slotIndex
if (evt.oldDraggableIndex !== null) evt.oldDraggableIndex -= this.slotIndex
},
// 创建拖拽
createSort() {
this.refresh = false
// 等到下一帧重渲染
this.$nextTick(() => {
this.refresh = true
// 这次也要等待 不然dom还没生成出来
requestAnimationFrame(() => {
let dom = document.querySelector('#custom-drag')
if (!dom) return new Error('未找到dom')
// 为什么不用ref 因为只有正常标签下拿的的dom 其他拿的是组件数据 $el才能拿到dom 不如直接用id
// 对外部事件进行emit调用
this.sort = new Sortable(dom, {
...this.$attrs,
filter: '.disabled-filter,' + this.$attrs.filter,
setData: (/** DataTransfer */ dataTransfer, /** HTMLElement*/ dragEl) => {
this.$emit('onSetEvent', dataTransfer, dragEl)
},
// 元素被选中
onChoose: (/**Event*/ evt) => {
this.handleIndex(evt)
this.$emit('chooseEvent', evt)
},
// 元素未被选中的时候(从选中到未选中)
onUnchoose: (/**Event*/ evt) => {
this.handleIndex(evt)
this.$emit('unchooseEvent', evt)
},
// 开始拖拽的时候
onStart: (/**Event*/ evt) => {
this.handleIndex(evt)
this.$emit('startEvent', evt)
},
// 结束拖拽 处理数据源
onEnd: (/**Event*/ evt) => {
this.handleIndex(evt)
let { oldIndex, newIndex } = evt
const currentRow = this.list.splice(oldIndex, 1)[0]
this.list.splice(newIndex, 0, currentRow)
// evt.clone && evt.clone.remove()
this.$emit('endEvent', evt)
},
// 元素从一个列表拖拽到另一个列表
onAdd: (/**Event*/ evt) => {
this.handleIndex(evt)
this.$emit('addEvent', evt)
},
// 列表内元素顺序更新的时候触发
onUpdate: (/**Event*/ evt) => {
this.handleIndex(evt)
this.$emit('updateEvent', evt)
},
// 列表的任何更改都会触发
onSort: (/**Event*/ evt) => {
this.handleIndex(evt)
this.$emit('sortEvent', evt)
},
// 元素从列表中移除进入另一个列表
onRemove: (/**Event*/ evt) => {
this.handleIndex(evt)
this.$emit('removeEvent', evt)
},
// 试图拖拽一个filtered的元素
onFilter: (/**Event*/ evt) => {
this.handleIndex(evt)
this.$emit('filterEvent', evt)
},
// 拖拽移动的时候 这个特别处理从props传入 因为要获取判定
onMove: (/**Event*/ evt, /**Event*/ originalEvent) => {
this.handleIndex(evt)
let result = this.move(evt, originalEvent)
// 去除经过头尾插槽的经过效果
if (evt.related.classList.contains('disabled-filter')) {
return false
}
return result
},
// clone一个元素的时候触发
onClone: (/**Event*/ evt) => {
this.handleIndex(evt)
this.$emit('cloneEvent', evt)
},
// 拖拽元素改变位置的时候
onChange: (/**Event*/ evt) => {
this.handleIndex(evt)
this.$emit('changeEvent', evt)
}
})
})
})
}
}
}
</script>
在页面中使用
<template>
<draggable
v-model="list"
handle=".mdi-drag"
tag="v-list"
:component-data="getComponentData()"
:move="move"
>
<template #header>
<v-list-subheader>header</v-list-subheader>
</template>
<v-list-item v-for="item in list">
<v-icon class="cursor-pointer ml-2">mdi-drag</v-icon>
{{item}}
</v-list-item>
<template #footer>
<v-list-subheader>footer</v-list-subheader>
</template>
</draggable>
</template>
<script>
export default{
methods:{
getComponentData() {
return {
attrs: {
class: 'pa-2 mt-2'
},
props: {
'bg-color': '#eee'
},
on: {
click: (e) => {
console.log(1)
}
}
}
}
}
}
</script>
vue3 composition api render式写法
<script>
import {
h,
defineComponent,
ref,
nextTick,
useAttrs,
watch,
computed,
useSlots,
onMounted,
toRefs,
getCurrentInstance
} from 'vue'
import Sortable from 'sortablejs'
export default defineComponent({
props: {
// v-model绑定
modelValue: {
type: Object,
default: () => {
return {}
}
},
// 自定义的tag组件
tag: {
type: String,
default: 'div'
},
// 绑定到动态组件上的数据
componentData: {
type: Object,
default: () => {
return {
on: {},
attrs: {},
props: {}
}
}
},
// 处理move事件 为了能拿到返回值
move: {
type: Function,
default: () => true
}
},
// 监听事件 映射回sortable配置上响应
emits: [
'update:modelValue',
'endEvent',
'chooseEvent',
'unchooseEvent',
'startEvent',
'onSetEvent',
'addEvent',
'updateEvent',
'sortEvent',
'removeEvent',
'filterEvent',
'cloneEvent',
'changeEvent'
],
setup(props, { emit }) {
// 处理props和model
let { tag, componentData, move } = props
let { modelValue } = toRefs(props)
let sort = ref(null)
let refresh = ref(true)
// 计算列表数据
const list = computed({
get() {
return modelValue.value
},
set(value) {
emit('update:modelValue', value)
}
})
// 获取$attrs的数据
const attrs = useAttrs()
const slots = useSlots()
// 计算增加的索引
const slotIndex = computed(() => {
return slots.header ? 1 : 0
})
const handleIndex = (evt) => {
if (evt.oldIndex !== null) evt.oldIndex -= slotIndex.value
if (evt.newIndex !== null) evt.newIndex -= slotIndex.value
if (evt.newDraggableIndex !== null) evt.newDraggableIndex -= slotIndex.value
if (evt.oldDraggableIndex !== null) evt.oldDraggableIndex -= slotIndex.value
}
// 渲染拖拽事件
const createSort = () => {
sort.value && sort.value.destroy()
refresh.value = false
nextTick(() => {
refresh.value = true
requestAnimationFrame(() => {
sort.value = new Sortable(document.querySelector('#custom-draggable'), {
...attrs.value,
filter: '.disabled-filter,' + attrs.filter,
setData: (/** DataTransfer */ dataTransfer, /** HTMLElement*/ dragEl) => {
emit('onSetEvent', dataTransfer, dragEl)
},
// 元素被选中
onChoose: (/**Event*/ evt) => {
handleIndex(evt)
emit('chooseEvent', evt)
},
// 元素未被选中的时候(从选中到未选中)
onUnchoose: (/**Event*/ evt) => {
handleIndex(evt)
emit('unchooseEvent', evt)
},
// 开始拖拽的时候
onStart: (/**Event*/ evt) => {
handleIndex(evt)
emit('startEvent', evt)
},
// 结束拖拽
onEnd: (/**Event*/ evt) => {
handleIndex(evt)
let { oldIndex, newIndex } = evt
const currentRow = list.value.splice(oldIndex, 1)[0]
list.value.splice(newIndex, 0, currentRow)
emit('endEvent', evt)
},
// 元素从一个列表拖拽到另一个列表
onAdd: (/**Event*/ evt) => {
handleIndex(evt)
emit('addEvent', evt)
},
// 列表内元素顺序更新的时候触发
onUpdate: (/**Event*/ evt) => {
handleIndex(evt)
emit('updateEvent', evt)
},
// 列表的任何更改都会触发
onSort: (/**Event*/ evt) => {
handleIndex(evt)
emit('sortEvent', evt)
},
// 元素从列表中移除进入另一个列表
onRemove: (/**Event*/ evt) => {
handleIndex(evt)
emit('removeEvent', evt)
},
// 试图拖拽一个filtered的元素
onFilter: (/**Event*/ evt) => {
handleIndex(evt)
emit('filterEvent', evt)
},
// 拖拽移动的时候
onMove: (/**Event*/ evt, /**Event*/ originalEvent) => {
handleIndex(evt)
let result = move(evt, originalEvent)
// 去除经过头尾插槽的经过效果
if (evt.related.classList.contains('disabled-filter')) {
return false
}
return result
},
// clone一个元素的时候触发
onClone: (/**Event*/ evt) => {
handleIndex(evt)
emit('cloneEvent', evt)
},
// 拖拽元素改变位置的时候
onChange: (/**Event*/ evt) => {
handleIndex(evt)
emit('changeEvent', evt)
}
})
})
})
}
// 监听变动
watch(
list,
() => {
createSort()
},
{
deep: true
}
)
onMounted(() => {
createSort()
})
// 获取外部tag是不是注册的组件 这里有坑
const transformComponent = () => {
// 获取用户输入的带-的标签转换为驼峰
let components = getCurrentInstance().appContext.components
// 首字母大写
let tagName = tag.replace(/-(\w)/g, (_, letter) => letter.toUpperCase())
// 可能是多个斜杠
let getName = tagName.charAt(0).toUpperCase() + tagName.slice(1)
if (!components[getName]) {
console.error(`组件${getName}不存在`)
return tag
}
return components[getName]
}
return () => {
// 处理渲染数据
return refresh.value
? h(
transformComponent(),
{
id: 'custom-draggable',
...(componentData.attrs || {}),
...(componentData.props || {}),
...(componentData.on || {})
},
[
// 具名插槽header,
slots.header ? h('div', { class: 'disabled-filter' }, slots.header()) : null,
// 插槽渲染
slots.default(),
slots.footer ? h('div', { class: 'disabled-filter' }, slots.footer()) : null
]
)
: h('div')
}
}
})
</script>
使用方式和上面相同 但是注意常用事件是要加on的
<template>
<renderer
v-model="list"
handle=".mdi-drag"
tag="v-list"
:component-data="getComponentData()"
>
<template #header>
<v-list-subheader>header</v-list-subheader>
</template>
<v-list-item v-for="item in list">
<v-icon class="cursor-pointer ml-2">mdi-drag</v-icon>
{{item}}
</v-list-item>
<template #footer>
<v-list-subheader>footer</v-list-subheader>
</template>
</renderer>
</template>
<script>
export default{
methods: {
getComponentData() {
return {
attrs: {
class: 'pa-2 mt-2'
},
props: {
'bg-color': '#eee'
},
on: {
onClick: (e) => {
console.log(1)
}
}
}
}
}
}
</script>
踩坑点
为什么写了两版api的代码 因为一开始其实用option api写出来超级快 就是卡在了tag自定义渲染的部分 :is一直无法渲染正确的自定义组件 我以为是render的问题 用option去写render函数一直无效就直接开了一个新的文件写 发现setup下语法糖的return h渲染出来的也无效 只能用defineComponent踩渲染成功
问题点1:vite加载vuetify的机制 autoImport导致 :is失效
绞尽脑汁发现class的属性应用到了自定义组件上 但是其他props合并的属性不生效 打开了F12一看 好家伙 直接给我渲染出<v-list></v-list>在界面上 灵光一闪 去检查全局注册的组件 好家伙app.appContext.components 下只有两个组件 RouterLink 和 RouterView 去检查了vite 是autoImport 那是只有我在使用的某个组件时候才会加载
解决方案
引入所有组件
import { createVuetify } from 'vuetify'
import 'vuetify/styles'
import '@mdi/font/css/materialdesignicons.css'
import * as components from "vuetify/components"
export default createVuetify({
icons: {
defaultSet: 'mdi'
},
components
})
问题点2:机制问题 v-on 和 h函数里放事件绑定的方式不同
在is写法中 :on是可以直接 写 click……之类的 在render中的 click事件要写成onClick
问题点3:增加了自定义插槽 sortablejs认为也是拖拽元素
在增加了首尾插槽后 我发现拖拽的时候会拖拽出null的东西出来 一下 oh~ 是sortablejs认为是拖拽元素 导致索引变动了 数组的索引都+1了
解决方案 onEnd时候处理 有header的插槽 索引就-1 并传递正确的索引回事件里
onEnd: (/**Event*/ evt) => {
evt.oldIndex -= this.slotIndex
evt.newIndex -= this.slotIndex
evt.newDraggableIndex -= this.slotIndex
evt.oldDraggableIndex -= this.slotIndex
let { oldIndex, newIndex } = evt
const currentRow = this.list.splice(oldIndex, 1)[0]
this.list.splice(newIndex, 0, currentRow)
// evt.clone && evt.clone.remove()
this.$emit('endEvent', evt)
}
问题点4:阻止经过事件在头尾插槽响应动画
被认为是插槽元素还会伴随着 可以被经过动画影响的情况 所以我们要对move事件进行修改 以及头尾插槽 添加标识的class
解决方案
// 拖拽移动的时候 这个特别处理从props传入 因为要获取判定
onMove: (/**Event*/ evt, /**Event*/ originalEvent) => {
let result = this.move(evt, originalEvent)
// 去除经过头尾插槽的经过效果
if (evt.related.classList.contains('disabled-filter')) {
return false
}
return result
},
这就是这次编写一个封装sortablejs组件的心得