大家好,我是你们今天的拖拽专家,今天我们要聊聊如何用 Vue 的 provide
/inject
机制,搞定一个跨组件的拖拽功能,而且还得支持复杂的数据传递。别怕,这玩意儿听起来高大上,其实就是个纸老虎,咱们一步一步把它拆解了,保证你听完之后,也能撸起袖子就上。
开场白:为啥要用 provide
/inject
?
首先,咱们得明白,为啥要选择 provide
/inject
这对好基友。难道 Vuex
或者事件总线不香吗?当然香,但它们有各自的适用场景。
Vuex
:适合管理全局状态,对于一些组件内部的临时状态,有点杀鸡用牛刀了。- 事件总线:简单粗暴,但组件多了容易乱,而且类型定义啥的也比较麻烦。
provide
/inject
的优势在于:
- 轻量级:只在需要的地方注入,不会污染全局。
- 解耦:父组件不用关心子组件如何使用
provide
的数据,子组件也不用关心provide
的数据来自哪里。 - 灵活:可以传递任何类型的数据,包括对象、函数等等。
所以,对于一些组件内部的,跨组件传递的状态(比如拖拽状态),provide
/inject
是个不错的选择。
第一部分:搭个骨架,先让它跑起来
咱们先来搭个最简单的骨架,让拖拽功能跑起来。
- Provider 组件 (拖拽容器)
这个组件负责提供拖拽相关的数据和方法。
<template>
<div class="drag-container">
<h2>Drag Container</h2>
<slot></slot>
</div>
</template>
<script>
import { ref, provide } from 'vue';
export default {
setup() {
const draggingItem = ref(null); // 存储当前正在拖拽的 item
const startDrag = (item) => {
draggingItem.value = item;
};
const endDrag = () => {
draggingItem.value = null;
};
provide('dragContext', {
draggingItem,
startDrag,
endDrag,
});
return {};
},
};
</script>
<style scoped>
.drag-container {
border: 2px dashed #ccc;
padding: 20px;
margin: 20px;
}
</style>
这个组件做了啥?
- 定义了一个
draggingItem
的ref
,用来存储当前正在拖拽的 item。 - 定义了
startDrag
和endDrag
两个方法,分别用于开始和结束拖拽。 - 使用
provide
将这些数据和方法提供给子组件,key 是'dragContext'
。
- Draggable Item 组件 (可拖拽的 item)
这个组件就是可以被拖拽的 item。
<template>
<div class="draggable-item" draggable="true" @dragstart="onDragStart" @dragend="onDragEnd">
{{ item.name }}
</div>
</template>
<script>
import { inject } from 'vue';
export default {
props: {
item: {
type: Object,
required: true,
},
},
setup(props) {
const dragContext = inject('dragContext');
const onDragStart = (event) => {
console.log('开始拖拽', props.item);
dragContext.startDrag(props.item);
event.dataTransfer.setData("text/plain", JSON.stringify(props.item)); // 设置拖拽数据
};
const onDragEnd = () => {
console.log('结束拖拽');
dragContext.endDrag();
};
return {
onDragStart,
onDragEnd,
};
},
};
</script>
<style scoped>
.draggable-item {
border: 1px solid #999;
padding: 10px;
margin: 5px;
cursor: move;
}
</style>
这个组件做了啥?
- 使用
inject
注入了dragContext
。 - 监听了
dragstart
和dragend
事件,分别调用dragContext
的startDrag
和endDrag
方法。 - 在
dragstart
事件中,将item
的数据设置到dataTransfer
中,方便在drop
事件中使用。
- Dropzone 组件 (可放置的区域)
这个组件就是可以放置拖拽 item 的区域。
<template>
<div class="dropzone" @dragover="onDragOver" @drop="onDrop">
<h2>Dropzone</h2>
<p>Drag items here</p>
<ul>
<li v-for="item in droppedItems" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const droppedItems = ref([]);
const onDragOver = (event) => {
event.preventDefault(); // 必须阻止默认行为,才能触发 drop 事件
};
const onDrop = (event) => {
event.preventDefault();
const itemData = event.dataTransfer.getData("text/plain");
const item = JSON.parse(itemData);
console.log('放置', item);
droppedItems.value.push(item);
};
return {
droppedItems,
onDragOver,
onDrop,
};
},
};
</script>
<style scoped>
.dropzone {
border: 2px dashed #999;
padding: 20px;
margin: 20px;
}
</style>
这个组件做了啥?
- 监听了
dragover
和drop
事件。 - 在
dragover
事件中,必须调用event.preventDefault()
,才能触发drop
事件。 - 在
drop
事件中,从dataTransfer
中获取item
的数据,并将其添加到droppedItems
数组中。
- App.vue (整合所有组件)
<template>
<DragContainer>
<DraggableItem v-for="item in items" :key="item.id" :item="item" />
</DragContainer>
<Dropzone />
</template>
<script>
import DragContainer from './components/DragContainer.vue';
import DraggableItem from './components/DraggableItem.vue';
import Dropzone from './components/Dropzone.vue';
export default {
components: {
DragContainer,
DraggableItem,
Dropzone,
},
data() {
return {
items: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
],
};
},
};
</script>
这个组件只是简单地将所有组件整合在一起,并提供了一些初始数据。
第二部分:数据传递,复杂场景也不怕
上面的例子只是传递了简单的数据,如果我们需要传递更复杂的数据,比如包含嵌套对象、函数等等,该怎么办呢?
其实,provide
/inject
可以传递任何类型的数据,关键在于如何正确地处理这些数据。
假设我们的 item
对象包含一个函数:
{
id: 1,
name: 'Item 1',
action: () => {
alert('Hello from Item 1!');
},
}
- Provider 组件 (拖拽容器)
<template>
<div class="drag-container">
<h2>Drag Container</h2>
<slot></slot>
</div>
</template>
<script>
import { ref, provide } from 'vue';
export default {
setup() {
const draggingItem = ref(null); // 存储当前正在拖拽的 item
const startDrag = (item) => {
draggingItem.value = item;
};
const endDrag = () => {
draggingItem.value = null;
};
const executeAction = (item) => {
if (item && item.action) {
item.action();
}
}
provide('dragContext', {
draggingItem,
startDrag,
endDrag,
executeAction
});
return {};
},
};
</script>
<style scoped>
.drag-container {
border: 2px dashed #ccc;
padding: 20px;
margin: 20px;
}
</style>
- Draggable Item 组件 (可拖拽的 item)
<template>
<div class="draggable-item" draggable="true" @dragstart="onDragStart" @dragend="onDragEnd">
{{ item.name }}
<button @click="executeItemAction">Execute Action</button>
</div>
</template>
<script>
import { inject } from 'vue';
export default {
props: {
item: {
type: Object,
required: true,
},
},
setup(props) {
const dragContext = inject('dragContext');
const onDragStart = (event) => {
console.log('开始拖拽', props.item);
dragContext.startDrag(props.item);
event.dataTransfer.setData("text/plain", JSON.stringify(props.item)); // 设置拖拽数据,注意函数无法序列化
};
const onDragEnd = () => {
console.log('结束拖拽');
dragContext.endDrag();
};
const executeItemAction = () => {
dragContext.executeAction(props.item);
}
return {
onDragStart,
onDragEnd,
executeItemAction
};
},
};
</script>
<style scoped>
.draggable-item {
border: 1px solid #999;
padding: 10px;
margin: 5px;
cursor: move;
}
</style>
- Dropzone 组件 (可放置的区域)
<template>
<div class="dropzone" @dragover="onDragOver" @drop="onDrop">
<h2>Dropzone</h2>
<p>Drag items here</p>
<ul>
<li v-for="item in droppedItems" :key="item.id">{{ item.name }}</li>
</ul>
<button v-if="lastDroppedItem" @click="executeDroppedItemAction">Execute Last Dropped Item Action</button>
</div>
</template>
<script>
import { ref, inject } from 'vue';
export default {
setup() {
const droppedItems = ref([]);
const dragContext = inject('dragContext');
const lastDroppedItem = ref(null);
const onDragOver = (event) => {
event.preventDefault(); // 必须阻止默认行为,才能触发 drop 事件
};
const onDrop = (event) => {
event.preventDefault();
const itemData = event.dataTransfer.getData("text/plain");
const item = JSON.parse(itemData);
console.log('放置', item);
droppedItems.value.push(item);
lastDroppedItem.value = item;
};
const executeDroppedItemAction = () => {
if (lastDroppedItem.value) {
//由于函数不能通过 dataTransfer 传递,所以这里无法直接调用 item.action()
//console.log(lastDroppedItem.value);
//alert("函数不能被序列化,所以不能被传递");
dragContext.executeAction(lastDroppedItem.value);
}
}
return {
droppedItems,
onDragOver,
onDrop,
lastDroppedItem,
executeDroppedItemAction
};
},
};
</script>
<style scoped>
.dropzone {
border: 2px dashed #999;
padding: 20px;
margin: 20px;
}
</style>
注意点:
- 函数序列化问题:
dataTransfer
只能传递字符串类型的数据,这意味着我们无法直接将函数传递过去。 所以我们需要在 Provider 中提供一个executeAction
方法,然后在 DraggableItem 中,点击的时候去调用这个方法。 - 对象引用问题:如果传递的是对象,需要注意对象引用问题。如果多个组件共享同一个对象引用,修改其中一个组件的数据,会影响其他组件。可以使用
JSON.parse(JSON.stringify(item))
来深拷贝对象,避免引用问题。 或者使用cloneDeep
方法。
第三部分:优化和扩展
- 类型定义:使用 TypeScript 可以为
provide
/inject
提供类型定义,提高代码的可维护性和可读性。
// DragContext.ts
import { InjectionKey, Ref } from 'vue';
export interface DragContext {
draggingItem: Ref<any | null>;
startDrag: (item: any) => void;
endDrag: () => void;
executeAction: (item: any) => void;
}
export const dragContextKey: InjectionKey<DragContext> = Symbol('dragContext');
// DragContainer.vue
<script lang="ts">
import { ref, provide } from 'vue';
import { DragContext, dragContextKey } from './DragContext';
export default {
setup() {
const draggingItem = ref<any | null>(null);
const startDrag = (item: any) => {
draggingItem.value = item;
};
const endDrag = () => {
draggingItem.value = null;
};
const executeAction = (item: any) => {
if (item && item.action) {
item.action();
}
}
const dragContext: DragContext = {
draggingItem,
startDrag,
endDrag,
executeAction
};
provide(dragContextKey, dragContext);
return {};
},
};
</script>
// DraggableItem.vue
<script lang="ts">
import { inject } from 'vue';
import { dragContextKey } from './DragContext';
export default {
props: {
item: {
type: Object,
required: true,
},
},
setup(props) {
const dragContext = inject(dragContextKey);
const onDragStart = (event: DragEvent) => {
console.log('开始拖拽', props.item);
dragContext.startDrag(props.item);
event.dataTransfer!.setData("text/plain", JSON.stringify(props.item)); // 设置拖拽数据
};
const onDragEnd = () => {
console.log('结束拖拽');
dragContext.endDrag();
};
const executeItemAction = () => {
dragContext.executeAction(props.item);
}
return {
onDragStart,
onDragEnd,
executeItemAction
};
},
};
</script>
// Dropzone.vue
<script lang="ts">
import { ref, inject } from 'vue';
import { dragContextKey } from './DragContext';
export default {
setup() {
const droppedItems = ref([]);
const dragContext = inject(dragContextKey);
const lastDroppedItem = ref(null);
const onDragOver = (event: DragEvent) => {
event.preventDefault(); // 必须阻止默认行为,才能触发 drop 事件
};
const onDrop = (event: DragEvent) => {
event.preventDefault();
const itemData = event.dataTransfer!.getData("text/plain");
const item = JSON.parse(itemData);
console.log('放置', item);
droppedItems.value.push(item);
lastDroppedItem.value = item;
};
const executeDroppedItemAction = () => {
if (lastDroppedItem.value) {
dragContext.executeAction(lastDroppedItem.value);
}
}
return {
droppedItems,
onDragOver,
onDrop,
lastDroppedItem,
executeDroppedItemAction
};
},
};
</script>
-
自定义事件:可以使用自定义事件来通知其他组件拖拽状态的变化。
-
拖拽反馈:在拖拽过程中,可以添加一些视觉反馈,比如改变 item 的背景颜色、显示拖拽图标等等,提高用户体验。
-
性能优化:对于大量 item 的拖拽,需要注意性能优化,比如使用虚拟列表、节流拖拽事件等等。
第四部分:总结
今天我们学习了如何使用 Vue
的 provide
/inject
机制,设计一个跨组件的拖拽功能,并且支持复杂的数据传递。
provide
/inject
是一种轻量级、解耦的跨组件通信方式。- 可以传递任何类型的数据,包括对象、函数等等。
- 需要注意函数序列化问题和对象引用问题。
- 可以使用 TypeScript 提供类型定义,提高代码的可维护性和可读性。
- 可以通过自定义事件、拖拽反馈、性能优化等手段,进一步完善拖拽功能。
希望今天的讲解对你有所帮助,下次再见!