如何利用 Vue 的 `Teleport` 和 `provide`/`inject`,设计一个跨组件的拖拽功能,支持复杂的数据传递?

各位观众老爷,今天咱就来聊聊 Vue 里面那几个“空间传送”和“隔空投送”的武林绝学——Teleportprovide/inject,看看怎么把它们揉吧揉吧,做出一道美味的跨组件拖拽大菜。

开场白:拖拽,一个让用户欲罢不能的小妖精

拖拽功能,相信大家都见过。从简单的排序、移动元素,到复杂的看板系统、流程设计器,它就像个小妖精,能让用户体验蹭蹭往上涨。但是,这小妖精也挺难伺候,尤其是在组件化的大环境下,跨组件拖拽更是让人头疼。

  • 痛点一:组件层级深,数据传递难

    A 组件想把东西拖到 Z 组件,中间隔了千山万水,一层层 props 传递,想想都酸爽。

  • 痛点二:拖拽元素样式污染

    拖拽的时候,元素样式很容易被父组件的样式影响,导致看起来怪怪的。

  • 痛点三:维护起来头大

    代码散落在各个组件里,改起来牵一发而动全身,让人怀疑人生。

别怕,今天咱就用 Teleportprovide/inject 这两个神器,把这些痛点统统解决掉!

第一章:Teleport——乾坤大挪移,组件样式不再迷路

Teleport 这玩意儿,就像乾坤大挪移,能把组件的内容传送到 DOM 树上的任何地方。

  • 用法:

    <template>
      <div>
        <Teleport to="body">
          <div class="drag-element">我是要被拖拽的元素</div>
        </Teleport>
      </div>
    </template>
    
    <style scoped>
    .drag-element {
      /* 自己的样式,不受父组件影响 */
      position: absolute; /* 方便拖拽时定位 */
      background-color: lightblue;
      padding: 10px;
      border: 1px solid blue;
    }
    </style>

    解释一下:

    • to="body":把 div.drag-element 传送到 body 标签下,直接成了 body 的子元素。
    • position: absolute: 拖拽的时候,需要绝对定位,这样才能自由移动。
    • scopedstyle scoped 只对当前组件生效,避免样式污染。
  • 好处:

    1. 样式隔离: 拖拽元素不再受父组件样式的影响,可以安心写自己的样式。
    2. DOM 结构清晰: 把拖拽元素放到 body 下,可以避免被复杂的组件层级限制。
  • 示例:

    创建一个 DragElement.vue 组件,用 Teleport 把元素传送到 body 下:

    <template>
      <Teleport to="body">
        <div
          class="drag-element"
          :style="dragStyle"
          @mousedown="startDrag"
        >
          我是要被拖拽的元素
        </div>
      </Teleport>
    </template>
    
    <script>
    export default {
      data() {
        return {
          isDragging: false,
          startX: 0,
          startY: 0,
          dragStyle: {
            position: "absolute",
            left: "0px",
            top: "0px",
          },
        };
      },
      methods: {
        startDrag(event) {
          this.isDragging = true;
          this.startX = event.clientX - parseInt(this.dragStyle.left);
          this.startY = event.clientY - parseInt(this.dragStyle.top);
          document.addEventListener("mousemove", this.doDrag);
          document.addEventListener("mouseup", this.stopDrag);
        },
        doDrag(event) {
          if (this.isDragging) {
            this.dragStyle.left = event.clientX - this.startX + "px";
            this.dragStyle.top = event.clientY - this.startY + "px";
          }
        },
        stopDrag() {
          this.isDragging = false;
          document.removeEventListener("mousemove", this.doDrag);
          document.removeEventListener("mouseup", this.stopDrag);
        },
      },
    };
    </script>
    
    <style scoped>
    .drag-element {
      background-color: lightblue;
      padding: 10px;
      border: 1px solid blue;
      cursor: pointer;
    }
    </style>

    在父组件中使用 DragElement

    <template>
      <div>
        <p>我是父组件的内容</p>
        <DragElement />
      </div>
    </template>
    
    <script>
    import DragElement from "./DragElement.vue";
    
    export default {
      components: {
        DragElement,
      },
    };
    </script>

    现在,你就可以拖拽那个蓝色的方块了,而且它的样式不会受到父组件的影响。

第二章:provide/inject——隔空投送,数据共享不再费劲

provide/inject 就像隔空投送,允许祖先组件向后代组件传递数据,而不需要一层层 props 传递。

  • 用法:

    • 祖先组件(提供数据):

      <template>
        <div>
          <slot></slot>
        </div>
      </template>
      
      <script>
      import { provide } from 'vue';
      
      export default {
        setup() {
          const dragData = {
            item: null, // 要拖拽的数据
            isDragging: false,
            startDrag: (item) => {
              dragData.item = item;
              dragData.isDragging = true;
            },
            endDrag: () => {
              dragData.item = null;
              dragData.isDragging = false;
            },
          };
      
          provide('dragData', dragData);
      
          return {};
        },
      };
      </script>
    • 后代组件(接收数据):

      <template>
        <div>
          {{ dragData.item }}
        </div>
      </template>
      
      <script>
      import { inject } from 'vue';
      
      export default {
        setup() {
          const dragData = inject('dragData');
      
          return {
            dragData,
          };
        },
      };
      </script>

    解释一下:

    • provide('dragData', dragData):祖先组件使用 provide 提供了一个名为 dragData 的数据对象。
    • inject('dragData'):后代组件使用 inject 接收 dragData
  • 好处:

    1. 避免 props 穿梭: 不再需要一层层 props 传递数据,代码更简洁。
    2. 跨组件共享状态: 可以方便地在多个组件之间共享拖拽状态。
  • 示例:

    创建一个 DragProvider.vue 组件,用 provide 提供拖拽数据:

    <template>
      <div>
        <slot></slot>
      </div>
    </template>
    
    <script>
    import { provide, reactive } from "vue";
    
    export default {
      setup() {
        const dragData = reactive({
          item: null, // 要拖拽的数据
          isDragging: false,
          startDrag: (item) => {
            dragData.item = item;
            dragData.isDragging = true;
          },
          endDrag: () => {
            dragData.item = null;
            dragData.isDragging = false;
          },
        });
    
        provide("dragData", dragData);
    
        return {};
      },
    };
    </script>

    创建一个 DraggableItem.vue 组件,使用 provideinject 来共享数据:

    <template>
      <div class="draggable-item" @mousedown="startDrag">
        {{ item.name }}
      </div>
    </template>
    
    <script>
    import { inject } from "vue";
    
    export default {
      props: {
        item: {
          type: Object,
          required: true,
        },
      },
      setup(props) {
        const dragData = inject("dragData");
    
        const startDrag = () => {
          dragData.startDrag(props.item);
        };
    
        return {
          startDrag,
          dragData
        };
      },
    };
    </script>
    
    <style scoped>
    .draggable-item {
      background-color: #eee;
      padding: 10px;
      margin: 5px;
      cursor: pointer;
      border: 1px solid #ccc;
    }
    </style>

    创建一个 DropTarget.vue 组件,使用 inject 来接收拖拽数据,并显示被拖拽的元素:

    <template>
      <div class="drop-target" @mouseup="endDrag" @mouseover="allowDrop">
        <p>我是放置区域</p>
        <p v-if="dragData.item">当前拖拽的元素:{{ dragData.item.name }}</p>
        <p v-else>请将元素拖拽到这里</p>
      </div>
    </template>
    
    <script>
    import { inject } from "vue";
    
    export default {
      setup() {
        const dragData = inject("dragData");
    
        const endDrag = () => {
          dragData.endDrag();
        };
        const allowDrop = (event) => {
            event.preventDefault();
        }
    
        return {
          dragData,
          endDrag,
          allowDrop
        };
      },
    };
    </script>
    
    <style scoped>
    .drop-target {
      background-color: #f9f9f9;
      padding: 20px;
      border: 2px dashed #ccc;
      margin-top: 20px;
    }
    </style>

    在父组件中使用 DragProviderDraggableItemDropTarget

    <template>
      <div>
        <h1>跨组件拖拽示例</h1>
        <DragProvider>
          <div class="draggable-items">
            <DraggableItem :item="item1" />
            <DraggableItem :item="item2" />
          </div>
          <DropTarget />
        </DragProvider>
      </div>
    </template>
    
    <script>
    import DragProvider from "./DragProvider.vue";
    import DraggableItem from "./DraggableItem.vue";
    import DropTarget from "./DropTarget.vue";
    
    export default {
      components: {
        DragProvider,
        DraggableItem,
        DropTarget,
      },
      data() {
        return {
          item1: { id: 1, name: "苹果" },
          item2: { id: 2, name: "香蕉" },
        };
      },
    };
    </script>
    
    <style scoped>
    .draggable-items {
      display: flex;
    }
    </style>

    现在,你可以拖拽 DraggableItemDropTargetDropTarget 会显示当前拖拽的元素。

第三章:Teleport + provide/inject——珠联璧合,打造完美拖拽体验

Teleportprovide/inject 结合起来,就能打造出近乎完美的跨组件拖拽体验。

  1. Teleport 负责样式隔离: 把拖拽元素传送到 body 下,避免样式污染。
  2. provide/inject 负责数据共享: 在祖先组件提供拖拽数据,后代组件接收数据,方便地共享拖拽状态。
  • 完整示例:

    修改 DraggableItem.vue 组件,使用 Teleport 把拖拽元素传送到 body 下:

    <template>
      <Teleport to="body">
        <div
          v-if="dragData.isDragging && dragData.item === item"
          class="drag-element"
          :style="dragStyle"
        >
          {{ item.name }}
        </div>
      </Teleport>
      <div class="draggable-item" @mousedown="startDrag">
        {{ item.name }}
      </div>
    </template>
    
    <script>
    import { inject, ref, onMounted } from "vue";
    
    export default {
      props: {
        item: {
          type: Object,
          required: true,
        },
      },
      setup(props) {
        const dragData = inject("dragData");
        const dragStyle = ref({
          position: "absolute",
          left: "0px",
          top: "0px",
          zIndex: 9999
        });
    
        const startDrag = (event) => {
          dragData.startDrag(props.item);
          // 设置拖拽元素的初始位置
          dragStyle.value.left = event.clientX + "px";
          dragStyle.value.top = event.clientY + "px";
    
          // 监听鼠标移动事件,更新拖拽元素的位置
          document.addEventListener("mousemove", doDrag);
          document.addEventListener("mouseup", stopDrag);
        };
    
        const doDrag = (event) => {
          if (dragData.isDragging && dragData.item === props.item) {
            dragStyle.value.left = event.clientX + "px";
            dragStyle.value.top = event.clientY + "px";
          }
        };
    
        const stopDrag = () => {
          document.removeEventListener("mousemove", doDrag);
          document.removeEventListener("mouseup", stopDrag);
        };
    
        return {
          startDrag,
          dragData,
          dragStyle
        };
      },
    };
    </script>
    
    <style scoped>
    .draggable-item {
      background-color: #eee;
      padding: 10px;
      margin: 5px;
      cursor: pointer;
      border: 1px solid #ccc;
    }
    
    .drag-element {
      background-color: lightblue;
      padding: 10px;
      border: 1px solid blue;
      cursor: grabbing;
    }
    </style>

    现在,拖拽元素会以 Teleport 的方式传送到 body 下,并且它的样式不会受到父组件的影响。同时,DropTarget 也能正确显示当前拖拽的元素。

第四章:进阶技巧,让拖拽更上一层楼

  • 拖拽反馈: 在拖拽过程中,可以给用户一些视觉反馈,比如改变鼠标样式、高亮目标区域等。
  • 数据转换: 在拖拽开始和结束的时候,可以对数据进行转换,比如把数据从一个格式转换为另一个格式。
  • 性能优化: 对于大量元素的拖拽,需要进行性能优化,比如使用虚拟列表。
  • 拖拽排序: 使用 SortableJS 等库实现拖拽排序功能。

总结:

Teleportprovide/inject 是 Vue 中非常强大的两个特性,它们可以帮助我们解决跨组件拖拽的难题。通过 Teleport 进行样式隔离,通过 provide/inject 进行数据共享,就能打造出流畅、稳定的拖拽体验。

特性 作用 优点 缺点
Teleport 将组件渲染到 DOM 树的不同位置 1. 样式隔离,避免样式污染; 2. 可以将组件渲染到任何位置,方便布局; 3. 简化 DOM 结构。 1. 需要注意 Teleport 目标位置的可用性; 2. 需要手动管理拖拽元素的定位。
provide/inject 在祖先组件和后代组件之间共享数据 1. 避免了 props 穿梭; 2. 方便地在多个组件之间共享状态; 3. 代码更简洁。 1. 依赖注入关系不明显,难以追踪; 2. 不适合传递大量数据,因为是响应式的; 3. 不适合用于组件库的公共 API,因为会造成 API 污染。
Teleport + provide/inject 跨组件拖拽解决方案 1. 结合了 Teleportprovide/inject 的优点; 2. 解决了跨组件拖拽的样式隔离和数据共享问题; 3. 可以打造出流畅、稳定的拖拽体验。 1. 代码复杂度较高; 2. 需要对 Teleportprovide/inject 有深入的理解; 3. 需要进行更多的测试。

好了,今天的讲座就到这里,希望大家都能掌握 Teleportprovide/inject 这两个武林绝学,做出让人眼前一亮的拖拽功能! 如果大家有什么问题,欢迎在评论区留言,我会尽力解答。下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注