阿卡不拉阿卡不拉
Vue3
阿卡的博客
Vue3
阿卡的博客
  • CSS

    • ✨flex 布局 👌
  • Vue3

    • 快速入门

      • 搭建工程 👌
      • 模板语法
      • 响应式基础
      • 响应式常用 API
      • 计算属性
      • 类与样式绑定
      • 条件和列表渲染
      • 事件处理
      • 表单处理
      • 生命周期
      • 侦听器
      • 组件介绍
      • Props
      • 自定义事件
      • 组件v-model
      • 插槽
      • 前端路由介绍
      • KeepAlive内置组件
      • 状态管理库
      • 组件库介绍
    • 深入本质

      • 虚拟DOM本质
      • 模板的本质
      • 组件树和虚拟DOM树
      • 数据拦截的本质
      • 响应式数据的本质
      • 响应式的本质
      • 响应式和组件渲染
      • 实现响应式系统 1
      • 实现响应式系统 2
      • 图解EFFECT
      • 实现响应式系统 3
      • 手写computed
      • 手写watch
      • 指令的本质
      • 插槽的本质
      • v-model的本质
      • setup 语法标签
      • 组件生命周期
      • keepalive 生命周期
      • keepalive的本质
      • key的本质
    • 细节补充

      • 【Vue】属性透传
      • 【Vue】依赖注入
      • 【Vue】组合式函数 👌
      • 【Vue】自定义指令
      • 【Vue】插件
      • 【Vue】Transition
      • 【Vue】TransitionGroup
      • 【Vue】Teleport
      • 【Vue】异步组件
      • 【Vue】Suspense
      • 【Router】路由模式
      • 【Router】路由零碎知识
      • 【Router】路由匹配语法
      • 【Router】路由组件传参
      • 【Router】内置组件和函数
      • 【Router】导航守卫
      • 【Router】过渡特效
      • 【Router】滚动行为
      • 【Router】动态路由
      • 【State】通信方式总结
      • 【State】Pinia 自定义插件
      • 【场景】封装树形组件
      • 【场景】自定义 ref 实现防抖
      • 【场景】懒加载
      • 【场景】虚拟列表
      • 【场景】虚拟列表优化
      • 【第三方库】VueUse
      • 【第三方库】vuedragable
      • 【第三方库】vue-drag-resize
      • 【第三方库】vue-chartjs
      • 【第三方库】vuelidate
      • 【第三方库】vue3-lazyload
      • 【场景】Websocket 聊天室
      • 【Vite】✨ 认识 Vite👌
      • 【Vite】配置文件 👌
      • 【Vite】✨ 依赖预构建 👌
      • 【Vite】构建生产版本 👌
      • 【Vite】环境变量与模式
      • 【Vite】CLI
      • 【Vite】Vite 插件
  • 笔面试

    • HTML

      • HTML 面试题汇总
      • 文档声明
      • 语义化
      • W3C 标准组织
      • SEO
      • iframe
      • 微格式
      • 替换元素
      • 页面可见性
    • CSS

      • CSS 面试题汇总
      • CSS 单位总结
      • 居中方式总结
      • 隐藏元素方式总结
      • 浮动
      • 定位总结
      • BFC
      • CSS 属性计算过程
      • CSS 层叠继承规则总结
      • @import 指令
      • CSS3 calc 函数
      • CSS3 媒体查询
      • 过渡和动画事件
      • 渐进增强和优雅降级
      • CSS3 变形
      • 渐进式渲染
      • CSS 渲染性能优化
      • 层叠上下文
      • CSS3 遮罩
    • JavaScript

      • JavaScript 面试题汇总
      • ✨ let、var、const 的区别
      • JS中的数据类型
      • 包装类型
      • 数据类型的转换
      • 运算符
      • ✨ 原型链
      • ✨ this 指向
      • ✨ 垃圾回收与内存泄漏
      • ✨ 执行栈和执行上下文
      • ✨ 作用域和作用域链
      • ✨ 闭包
      • DOM 事件的注册和移除
      • DOM 事件的传播机制
      • 阻止事件默认行为
      • 递归
      • ✨ 属性描述符
      • class 和构造函数区别
      • 浮点数精度问题
      • 严格模式
      • ✨ 函数防抖和节流
      • ✨ WeakSet 和 WeakMap
      • ✨ 深浅拷贝
      • 函数柯里化
      • Node 事件循环
      • 尺寸和位置
      • ✨ 事件循环 👌
    • Promise

      • ✨Promise 面试题考点 👌
      • ✨Promise 基础 👌
      • ✨Promise 的链式调用 👌
      • ✨Promise 的静态方法 👌
      • ✨async 和 await👌
    • 浏览器

      • 浏览器面试题汇总
      • ✨ 浏览器的渲染流程
      • ✨ 资源提示关键词
      • 浏览器的组成部分
      • IndexedDB
      • ✨ File API
      • ✨ 浏览器缓存
      • ✨ 浏览器跨标签页通信
      • Web Worker
    • 网络

      • 网络面试题汇总
      • 五层网络模型 👌
      • 常见请求方法 👌
      • ✨cookie👌
      • 面试题
      • 加密
      • ✨JWT👌
      • ✨ 同源策略及跨域问题 👌
      • 文件上传
      • ✨ 输入 url 地址之后
      • 文件下载
      • ✨ session
      • ✨ TCP
      • ✨ CSRF 攻击
      • ✨XSS 攻击 👌
      • ✨ 网络性能优化
      • 断点续传
      • 域名和 DNS
      • ✨SSL、TLS、HTTPS 的关系
      • ✨ HTTP 各版本差异 👌
      • HTTP 缓存协议
      • ✨ WebSocket
    • 工程化

      • CMJ 和 ESM
      • npx
      • ESLint
    • Vue2

      • Vue 面试题汇总相关
      • 组件通信方式总结
      • 虚拟 DOM
      • v-model
      • 数据响应式原理
      • diff
      • 生命周期详解
      • computed
      • 过滤器
      • 作用域插槽
      • 过度和动画
      • 优化
      • keep-alive
      • 长列表优化
      • 其他 API
      • 模式和环境变量
      • 更多配置
      • 更多命令
      • 嵌套路由
      • 路由切换动画
    • Vue3

      • ✨ Vue3 整体变化 👌
      • ✨ Vue3 响应式变化 👌
      • ✨ nextTick 实现原理 👌
      • 两道代码题 👌
      • Vue 运行机制
      • 渲染器核心功能
      • 事件绑定与更新
    • Cypress

      • Cypress 测试框架面试题
    • 项目

      • FOFA 实习项目

        • /interview/project/fofa/FOFA%E5%AE%9E%E4%B9%A0%E9%A1%B9%E7%9B%AE%E9%9D%A2%E8%AF%95%E7%82%B9.html
      • 低代码问卷系统

        • 低代码问卷项目面试点
      • VR 全景看房

        • VR 全景看房项目面试点
  • TS

    • 快速入门

      • Playground 👌
      • 安装与运行 👌
      • 开发相关配置 👌
      • TS 常见类型 👌
      • 类型声明 👌
  • 工具库

    • 常用第三方工具库
    • JQuery
    • Lodash
    • Animate.css
    • Axios
    • MockJS
    • Moment
    • ECharts
  • 其他知识点

    • ✨ 前端项目打包流程与编译概念详解
    • ✨ 懒加载 👌
    • ✨ 前端路由的核心原理 👌

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

口头回答

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

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

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

核心考量:

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

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

具体实现细节

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

1.1 组件库的拖拽源实现

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

<!-- 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 画布的拖拽目标实现

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

<!-- 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 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>

关键技术点:

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

1.3 性能优化策略

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

优化考虑:

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

2. vuedraggable 的使用场景

2.1 画布内组件排序

<!-- 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 大纲面板同步排序

<!-- 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 的优势:

// 拖拽状态的视觉反馈
.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 状态管理集成

// 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 事件协调机制

// 拖拽开始时清空选中状态(两种方式都需要)
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:

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

vuedraggable:

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

5.3 扩展性考虑

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

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

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

最近更新:: 2025/7/18 12:04
Contributors: AK