在所有的代码设计中,我们都可以渐进增强的思考模式,去指导我们的开发。渐进增强的核心原则是:基础的内容和功能开始,逐层添加表现层和行为层,确保每一层的增强都不会破坏基础层的功能。当增强功能不可用时,核心功能仍然可以正常工作。这其实就是现代 Ssr 等技术的核心支撑。先保证 Html 的正常渲染可用,再填充 css 的样式增强,以及 js 的功能增强。
我们的技术思考点,就可以按照以下思路去设计
-
基础功能的闭环: 我们最原始的功能,仅仅是能够将物料元素拖拽至渲染区域内。
在开始实现之前,我们优先考虑的是使用 mouse 事件还是 drag 事件。
- 从”渐进”的底线(兼容性)考虑:drag 事件在不同的浏览器上差异巨大,会带来额外的维护成本
- 从”渐进”的上限(控制力)考虑:drag 事件 API 是个黑盒,在未来我我们需要细粒度的”增强”,比如拖拽过程中指示器的实时反馈,构建虚拟区域计算碰撞边缘等,边缘滚动等。drag 都无法实现。
在选择使用
mouse事件后,我并没有从document.addEventListener('mousedown')开始“造轮子”,因为自己处理mouse和touch的差异、处理iframe边界等问题同样复杂。 而是使用interact.js代替封装底层的mouse、touch和pointer事件,同时又暴露了像dragstart、dragmove、dragend这样语义化的、可被我们完全自定义的钩子。接下来,我们就可以跑通一个最简单的
drag-start:只记录dragItem.dragIddrag-move:暂时什么都不做drag-end:清理dragID,并执行更新数据的流程。 这时,我们就有了一个功能:物料可以被拖拽、并且能被正确插入到数据中。这是我们最稳固的底线。
-
实时的视觉反馈交互增强 “底线功能有了”,但是用户体验很差。用户拖拽时,元素“瞬移”了,他根本不知道发生了什么。第一次增强,就是提供方即时的视觉反馈。就好像给 Html 加上 Css 一样。 -
drag-start中,不再只是记录 ID。我们会动态创建一个”幽灵元素”作为拖拽预览,并且把光标设为grabbing。告知用户拖拽的是具体哪一个物料以增强用户反馈。 -drag-move中,这个高频事件现在有了第一个职责:根据interact.js提供的deltaX和deltaY,实时更新overlayDom的transform属性,让“幽灵”跟随鼠标。 -drag-end中,负责清理,把 “幽灵”元素 移除,恢复光标。 做到这些,我们功能,就从基本”可用”变为了“友好” -
精准插入的功能增强 现在我们遇到了一个新的问题:物料只能被丢进外部的“大容器”,但是无法精准插入到”子容器内部”。
- 在
drag-move中,我们遍历所有可放置的子元素 dom,判断鼠标悬停在哪个元素上。 - 调用指示器: 一旦找到,我们就调用
createIndicator在那个元素上创建一个absolute元素,用于告诉用户当前鼠标悬停元素的范围。 drag-end.ts:drag-end时,我们会记录insertPayload.current,当前容器元素的位置信息,并在drop成功后,insertCallback会使用这个 payload,实现精确插入。
- 在
-
性能增强:功能完备后,再进行性能上的增强
当画布上有 1000 个组件时,上面那个
drag-move(在mousemove里遍历 1000 个 DOM)会导致页面卡顿到无法使用。自然地,我们就能想到于是我们不再在
drag-move中查询 dom,而是抽象成generageAreas方法,在drag-start时处理。- 这个方法会“层序遍历”一次 DOM,预先计算出所有可放置点(
nodeAreas)。 - 这些
nodeAreas是纯粹的、轻量的 JavaScript 坐标对象,并被缓存 于是drag-move的逻辑就被彻底简化 - 它不遍历 DOM
- 它只遍历那个轻量的
nodeAreas数组(纯 Js 计算)。 这种空间换时间的操作,即使是复杂页面,drag-move也不会有什么性能问题
- 这个方法会“层序遍历”一次 DOM,预先计算出所有可放置点(
-
边缘功能增强:在“功能完备”且”高性能”后。就可以考虑一些“边缘体验”
- 对于开发者的调试功能:有时候我们为了方便开发,需要在把所有的可放置区域都显示出来。我们可以通过一个本地
__debug__参数,当__debug__为true时,创建所有可放置元素的区域指示。由于可放置容器元素很多,需要一次性批量创建多个元素,可以使用DocumentFragment,减少重绘(Repaint)和回流(Reflow)。
- 对于开发者的调试功能:有时候我们为了方便开发,需要在把所有的可放置区域都显示出来。我们可以通过一个本地