渐进式集成:从浏览器渲染到框架设计的统一哲学

在所有的代码设计中,我们都可以渐进增强的思考模式,去指导我们的开发。渐进增强的核心原则是:基础的内容和功能开始,逐层添加表现层和行为层,确保每一层的增强都不会破坏基础层的功能。当增强功能不可用时,核心功能仍然可以正常工作。这其实就是现代 Ssr 等技术的核心支撑。先保证 Html 的正常渲染可用,再填充 css 的样式增强,以及 js 的功能增强。

我们的技术思考点,就可以按照以下思路去设计

  1. 基础功能的闭环: 我们最原始的功能,仅仅是能够将物料元素拖拽至渲染区域内。

    在开始实现之前,我们优先考虑的是使用 mouse 事件还是 drag 事件。

    • 从”渐进”的底线(兼容性)考虑:drag 事件在不同的浏览器上差异巨大,会带来额外的维护成本
    • 从”渐进”的上限(控制力)考虑:drag 事件 API 是个黑盒,在未来我我们需要细粒度的”增强”,比如拖拽过程中指示器的实时反馈,构建虚拟区域计算碰撞边缘等,边缘滚动等。drag 都无法实现。

    在选择使用 mouse 事件后,我并没有从document.addEventListener('mousedown') 开始“造轮子”,因为自己处理 mousetouch 的差异、处理 iframe 边界等问题同样复杂。 而是使用 interact.js 代替封装底层的 mousetouchpointer 事件,同时又暴露了像 dragstartdragmovedragend 这样语义化的、可被我们完全自定义的钩子。

    接下来,我们就可以跑通一个最简单的

    • drag-start:只记录 dragItem.dragId
    • drag-move:暂时什么都不做
    • drag-end:清理 dragID,并执行更新数据的流程。 这时,我们就有了一个功能:物料可以被拖拽、并且能被正确插入到数据中。这是我们最稳固的底线。
  2. 实时的视觉反馈交互增强 “底线功能有了”,但是用户体验很差。用户拖拽时,元素“瞬移”了,他根本不知道发生了什么。第一次增强,就是提供方即时的视觉反馈。就好像给 Html 加上 Css 一样。 - drag-start 中,不再只是记录 ID。我们会动态创建一个”幽灵元素”作为拖拽预览,并且把光标设为 grabbing。告知用户拖拽的是具体哪一个物料以增强用户反馈。 - drag-move 中,这个高频事件现在有了第一个职责:根据 interact.js 提供的 deltaXdeltaY,实时更新 overlayDomtransform 属性,让“幽灵”跟随鼠标。 - drag-end 中,负责清理,把 “幽灵”元素 移除,恢复光标。 做到这些,我们功能,就从基本”可用”变为了“友好”

  3. 精准插入的功能增强 现在我们遇到了一个新的问题:物料只能被丢进外部的“大容器”,但是无法精准插入到”子容器内部”。

    • drag-move 中,我们遍历所有可放置的子元素 dom,判断鼠标悬停在哪个元素上。
    • 调用指示器: 一旦找到,我们就调用 createIndicator 在那个元素上创建一个 absolute 元素,用于告诉用户当前鼠标悬停元素的范围。
    • drag-end.tsdrag-end 时,我们会记录 insertPayload.current,当前容器元素的位置信息,并在 drop 成功后,insertCallback 会使用这个 payload,实现精确插入
  4. 性能增强:功能完备后,再进行性能上的增强

    当画布上有 1000 个组件时,上面那个 drag-move(在 mousemove 里遍历 1000 个 DOM)会导致页面卡顿到无法使用。自然地,我们就能想到

    于是我们不再在 drag-move 中查询 dom,而是抽象成 generageAreas 方法,在 drag-start 时处理。

    • 这个方法会“层序遍历”一次 DOM,预先计算出所有可放置点(nodeAreas)。
    • 这些 nodeAreas纯粹的、轻量的 JavaScript 坐标对象,并被缓存 于是 drag-move 的逻辑就被彻底简化
    • 它不遍历 DOM
    • 它只遍历那个轻量的 nodeAreas 数组(纯 Js 计算)。 这种空间换时间的操作,即使是复杂页面,drag-move 也不会有什么性能问题
  5. 边缘功能增强:在“功能完备”且”高性能”后。就可以考虑一些“边缘体验”

    • 对于开发者的调试功能:有时候我们为了方便开发,需要在把所有的可放置区域都显示出来。我们可以通过一个本地 __debug__参数,当 __debug__true 时,创建所有可放置元素的区域指示。由于可放置容器元素很多,需要一次性批量创建多个元素,可以使用 DocumentFragment,减少重绘(Repaint)和回流(Reflow)。