Skip to content

Q7: 为什么添加组件到画布使用了 H5 原始拖曳 API,调整画布题目和大纲顺序使用了 vuedraggable?两者有什么特别的考量吗?

口头回答

这是一个很好的技术选型问题。在我的低代码问卷平台中,我确实采用了两种不同的拖拽技术方案:

1. 添加组件使用 H5 原始拖曳 API:主要考虑是跨容器拖拽的需求。从左侧组件库拖拽到中间画布,这是两个完全不同的 DOM 容器,需要传递组件类型等数据信息,H5 原生 API 的 dataTransfer 机制非常适合这种场景。

2. 调整顺序使用 vuedraggable:主要考虑是同容器内排序的需求。画布内的组件排序和大纲面板的排序都是在同一个数组内调整元素位置,vuedraggable 提供了更好的 Vue 响应式集成和用户体验。

核心考量

  • 功能需求不同:一个是跨容器添加,一个是同容器排序
  • 数据传递方式:H5 API 适合传递序列化数据,vuedraggable 适合直接操作数组
  • 用户体验:vuedraggable 提供更流畅的排序动画和视觉反馈
  • 开发复杂度:针对不同场景选择最合适的技术方案

这种混合使用的方式让我能够在不同场景下都获得最佳的技术效果和用户体验。

具体实现细节

1. H5 原始拖曳 API 的使用场景

1.1 组件库的拖拽源实现

在左侧组件库中,每个组件都支持拖拽:

vue
<!-- src/components/Editor/QuestionTypeCom.vue -->
<template>
  <div
    @click="addQuestionCom"
    draggable="true"
    @dragstart="handleDragStart"
    @dragend="handleDragEnd"
    class="question-type-com-container pointer"
    :class="{ dragging: isDragging }"
  >
    {{ name }}
  </div>
</template>

<script setup lang="ts">
// 拖拽开始事件
const handleDragStart = (event: DragEvent) => {
  isDragging.value = true;
  // 将组件类型信息存储到拖拽数据中,用于在拖拽结束时(drop)获取组件类型信息
  if (event.dataTransfer) {
    event.dataTransfer.setData(
      "application/json",
      JSON.stringify({
        type: props.type,
        name: props.name,
      })
    );
    // 设置拖曳效果
    event.dataTransfer.effectAllowed = "copy";
  }
};

// 拖拽结束事件
const handleDragEnd = () => {
  isDragging.value = false;
};
</script>

关键技术点:

  • draggable="true":启用 HTML5 拖拽功能
  • dataTransfer.setData():传递组件类型和名称信息
  • effectAllowed = 'copy':设置拖拽效果为复制模式
  • 拖拽状态管理:提供视觉反馈

1.2 画布的拖拽目标实现

画布容器作为拖拽目标,接收组件:

vue
<!-- src/views/EditorView/Center.vue -->
<template>
  <div
    class="center-container"
    ref="centerContainerRef"
    @drop="handleDrop"
    @dragover="handleDragOver"
    @dragenter="handleDragEnter"
    @dragleave="handleDragLeave"
    :class="{ 'drag-over': isDragOver }"
  >
    <!-- vuedraggable 用于内部排序 -->
    <draggable
      v-model="editorStore.questionComs"
      item-key="id"
      @start="startDrag"
    >
      <!-- 组件列表 -->
    </draggable>
  </div>
</template>

<script setup lang="ts">
// 处理拖拽进入
const handleDragEnter = (event: DragEvent) => {
  event.preventDefault();
  isDragOver.value = true;
};

// 节流后的拖拽效果设置函数
const setDragEffect = throttle((dataTransfer: DataTransfer) => {
  dataTransfer.dropEffect = "copy";
}, 100);

// 处理拖拽悬停
const handleDragOver = (event: DragEvent) => {
  // 立即阻止默认行为,确保 drop 事件能正常触发
  event.preventDefault();

  // 节流设置拖拽效果,避免频繁操作
  if (event.dataTransfer) {
    setDragEffect(event.dataTransfer);
  }
};

// 处理拖拽离开
const handleDragLeave = (event: DragEvent) => {
  // 只有当离开整个容器时才设置为false
  if (!centerContainerRef.value?.contains(event.relatedTarget as Node)) {
    isDragOver.value = false;
  }
};

// 处理拖拽放置
const handleDrop = (event: DragEvent) => {
  event.preventDefault();
  isDragOver.value = false;

  if (event.dataTransfer) {
    try {
      const data = JSON.parse(event.dataTransfer.getData("application/json"));
      const questionType = data.type as MaterialComType;

      if (!questionType) {
        console.warn("无效的组件类型");
        return;
      }

      // 根据问题类型获取问题组件的默认状态
      const defaultStatus = defaultStatusMap[questionType]() as SchemaType;
      // 更新问题组件的初始状态
      updateInitStatus(defaultStatus, questionType);
      // 添加状态到仓库
      editorStore.addQuestionCom(defaultStatus);
      // 滚动到底部
      eventBus.emit("scrollToBottom");

      ElMessage.success(`已添加${data.name}组件`);
    } catch (error) {
      console.error("解析拖拽数据失败:", error);
      ElMessage.error("添加组件失败");
    }
  }
};
</script>

关键技术点:

  • 完整的拖拽事件处理:dragenterdragoverdragleavedrop
  • event.preventDefault():阻止默认行为,允许放置
  • dataTransfer.getData():获取传递的组件信息
  • 错误处理:确保数据解析的安全性
  • 视觉反馈:拖拽状态的 CSS 样式

1.3 性能优化策略

typescript
// 节流后的拖拽效果设置函数
const setDragEffect = throttle((dataTransfer: DataTransfer) => {
  dataTransfer.dropEffect = "copy";
}, 100);

优化考虑:

  • 使用节流函数避免 dragover 事件的频繁触发
  • 精确的拖拽离开判断,避免误触发

2. vuedraggable 的使用场景

2.1 画布内组件排序

vue
<!-- src/views/EditorView/Center.vue -->
<draggable v-model="editorStore.questionComs" item-key="id" @start="startDrag">
  <template #item="{ element, index }">
    <div
      class="content mb-10 relative"
      :class="{ active: editorStore.currentQuestionIndex === index }"
      @click="showEditor(index)"
      :key="element.id"
    >
      <component
        :is="element.type"
        :status="element.status"
        :serialNum="questionSerialNumber[index]"
      />
    </div>
  </template>
</draggable>

2.2 大纲面板同步排序

vue
<!-- src/views/EditorView/LeftSide/Outline.vue -->
<draggable v-model="editorStore.questionComs" item-key="id" @start="startDrag">
  <template #item="{ element, index }">
    <div class="mb-10" v-show="isQuestionType(element.name)">
      <div
        class="item"
        @click="handleClick(index)"
        :class="{ active: editorStore.currentQuestionIndex === index }"
      >
        {{ questionSerialNumber[index] }}.{{
          element.status.title.status.length > 10
            ? element.status.title.status.slice(0, 10) + '...'
            : element.status.title.status
        }}
      </div>
    </div>
  </template>
</draggable>

关键技术点:

  • v-model 双向绑定:直接操作响应式数组
  • item-key="id":提供唯一标识符
  • @start 事件:处理拖拽开始时的状态清理
  • 同步更新:两个拖拽区域绑定同一数据源

3. 技术选型的深层考量

3.1 功能需求差异分析

场景H5 原始拖曳 APIvuedraggable
跨容器拖拽✅ 支持数据传递❌ 主要用于同容器
同容器排序❌ 需要复杂实现✅ 原生支持
数据传递✅ dataTransfer 机制❌ 不适合跨容器
Vue 集成❌ 需要手动处理✅ 完美集成
动画效果❌ 需要自定义✅ 内置动画

3.2 用户体验对比

H5 原始拖曳 API 的优势:

scss
// 拖拽状态的视觉反馈
.question-type-com-container.dragging {
  opacity: 0.5;
  transform: scale(0.95);
  transition: all 0.2s ease;
}

// 画布拖拽悬停状态
.center-container.drag-over {
  border: 2px dashed #409eff;
  background-color: rgba(64, 158, 255, 0.05);
  transition: all 0.3s ease;
}

.center-container.drag-over::before {
  content: "拖拽组件到此处添加";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: #409eff;
  font-size: 16px;
  font-weight: 500;
}

vuedraggable 的优势:

  • 流畅的排序动画
  • 自动的占位符显示
  • 响应式数据同步
  • 更好的触摸设备支持

3.3 开发复杂度分析

H5 原始拖曳 API:

  • ✅ 灵活性高,可以完全自定义
  • ❌ 需要处理更多的边界情况
  • ❌ 跨浏览器兼容性需要考虑
  • ❌ 事件处理相对复杂

vuedraggable:

  • ✅ 开箱即用,配置简单
  • ✅ 与 Vue 生态完美集成
  • ✅ 内置性能优化
  • ❌ 定制化程度相对较低

4. 混合方案的架构设计

4.1 数据流设计

组件库拖拽 (H5 API)

  数据传递 (dataTransfer)

  画布接收 (drop 事件)

  创建组件实例

  添加到 Store 数组

vuedraggable 响应式更新

  画布和大纲同步显示

4.2 状态管理集成

typescript
// src/stores/useEditor.ts
const useEditorStore = defineStore("editor", {
  state: () => ({
    questionComs: [] as SchemaType[], // 同时被两种拖拽方式操作
    currentQuestionIndex: -1,
  }),
  actions: {
    // H5 API 调用:添加新组件
    addQuestionCom(com: SchemaType) {
      this.questionComs.push(com);
    },
    // vuedraggable 调用:删除组件
    removeQuestion(index: number) {
      this.questionComs.splice(index, 1);
    },
    // 两种方式都会调用:状态管理
    setCurrentQuestionIndex(index: number) {
      this.currentQuestionIndex = index;
    },
  },
});

4.3 事件协调机制

typescript
// 拖拽开始时清空选中状态(两种方式都需要)
const startDrag = () => {
  editorStore.setCurrentQuestionIndex(-1);
};

// 事件总线协调滚动行为
eventBus.emit("scrollToBottom"); // H5 API 添加后滚动
eventBus.emit("scrollToCenter", index); // vuedraggable 选中后滚动

5. 最佳实践总结

5.1 技术选型原则

  1. 场景驱动:根据具体使用场景选择最合适的技术
  2. 用户体验优先:优先考虑用户操作的流畅性和直观性
  3. 开发效率平衡:在功能需求和开发复杂度之间找到平衡
  4. 可维护性考虑:选择团队熟悉且社区支持良好的方案

5.2 实现要点

H5 原始拖曳 API:

  • 完整的事件处理链:dragstartdragenterdragoverdrop
  • 数据安全:JSON 序列化和错误处理
  • 性能优化:事件节流和精确的状态管理
  • 视觉反馈:清晰的拖拽状态提示

vuedraggable:

  • 正确的数据绑定:v-modelitem-key
  • 事件处理:@start 等关键事件
  • 同步更新:多个拖拽区域的数据一致性
  • 性能优化:避免不必要的重渲染

5.3 扩展性考虑

这种混合方案为后续功能扩展提供了良好的基础:

  1. 新增拖拽源:可以轻松添加更多的组件库或工具面板
  2. 拖拽目标扩展:可以支持多个画布或不同类型的容器
  3. 拖拽数据丰富:可以传递更复杂的组件配置信息
  4. 交互增强:可以添加更多的拖拽反馈和动画效果

通过这种技术选型,我在低代码问卷平台中实现了既灵活又高效的拖拽交互体验,满足了不同场景下的功能需求,同时保持了良好的代码可维护性和用户体验。