拖拽参考线及吸附工具react-dragline

大概在2017年7月,我司计划开发一款可视化建站的项目。除了老员工外,当时的公司现代化前端只有三人。于是,只有一年工作经验的我被“赶鸭子上架”,开始了为期一年半的折腾之旅。在众多复杂的交互中,有一项需求是“拖拽对齐吸附及显示参考线”,当时也希望在社区寻找解决方案。很可惜,除了一些简单的DEMO外,并没有可用于生产环境的实践。一年半过去了,项目接近尾声不那么忙。我逐步整理出自己在工作中的解决方案,于是就有了这个开源项目react-dragline

示例

开门见山,首先上一个简单的例子。

import { DraggableContainer, DraggableChild } from 'react-dragline'

const children = [
  { id: 1, position: { x: 100, y: 10 } },
  { id: 2, position: { x: 400, y: 200 } },
]

const containerStyle = {
  height: 600,
  position: 'relative',
}

const childStyle = {
  width: 100,
  height: 100,
  cursor: 'move',
  background: '#8ce8df',
}

export default function Example() {
  return (
    <DraggableContainer style={containerStyle}>
      {
        children.map(({ id, position }) => (
          <DraggableChild key={id} defaultPosition={position}>
            <div style={childStyle} />
          </DraggableChild>
        ))
      }
    </DraggableContainer>
  )
}

然后你就可以动手拖一拖体验一下啦~ 在线DEMO戳我

关于调用方式,起初参考了react-sortable-hoc,计划使用HOC的写法,但感觉使用HOC会把代码从JSX中分离,复杂度不高的情况下有些过度设计,所以这选择了更加“组件化”的写法。

强调一点的是,DraggableContainer需要使用者自行加上定位属性relative/absolute/fixed,本意是检测到没有定位属性时自动加上relative,但是这种方式在服务端渲染的场景下会有丑陋的“跳动”(因为只有在客户端才能检测DOM嘛),因此就把这项功能给去了。

更多的options都写在README里了,出自我的“中式英语”大家阅读起来也没什么难度。

原理

关于原理,拖拽功能是基于react-draggable的Uncontrolled组件DraggableCore,统一使用left和top作为x,y坐标的映射。DraggableContainerDraggableChild之间的通信是通过React.cloneElement实现的。剩下的,就是把精力集中于实现核心功能参考线和吸附。以下根据拖拽的事件周期onstartondragonstop分别阐述。

  • onstart
    在拖拽初始中获取每个DraggableChild的坐标、宽高、索引等信息。可以将所有的DraggableChild分为两类,target为当前拖拽的元素,compares为其余的元素,可以称为对照组(为了方便行为,下文中target即代表拖拽目标元素,compare即代表当前与target比较的元素)。为什么不在componentDidMount中就获取好呢?因为这些元素的信息可能会变得,比如说增删。相比起这细微的性能损失,维护信息变化的成本显然要高得多。

  • ondrag
    核心代码主要在ondrag的过程中,我们需要不断的去比较targetcompares之间的距离是否小于阈值threshold(默认5px)。考虑过是否需要加上debounce,但是似乎对灵敏度还是有些影响,不是一个太好的选择。

吸附功能的实现相对简单,坐标和对照组的某元素的距离小于阈值threshold时,让其等于对照组的坐标即可:

  // a 为对照组某元素的坐标
  if (Math.abs(a - x) < threshold + 1) {
    x = a
  }

参考线的实现略微复杂一些,以Y轴方向为例,最初的实现是分别取target元素和compare元素上下位置(Element.getBoundingClientRect),组成一个包含四个值的数组[t, b, T, B]target用小写字母表示,compare用大写字母表示),最大差值(排序取首尾值相减)即为参考线的长度,取最小值作为参考线的起点。
这么做似乎也没有什么问题,实际上是有一些细微的误差的。使用DOM元素的位置信息计算具有一定的滞后性,DOM表示的是当前的位置,而计算的是拖拽下一帧的位置,这样“细微的误差”也就可以解释了。解决方式也很简单,将数组中的tb替换为yy + height即可。
结束了吗? 并没有。最初的设计是将计算xy是否需要吸附和参考线在统一流程里,因为有吸附才有会出现参考线,避免了重复的计算。然而,当target元素同时和X轴和Y轴两个compare元素吸附时,Y轴的参考线是会受到Y轴吸附的影响(X轴同理)。见下图:

xy同时吸附时参考线突然的变化

当水平方向上两个绿色的元素吸附时,Y轴的参考线也必须“突然地增加了一段”。因此后续又做了一次代码封装粒度更小的重构,以在计算完成xy吸附之后再对参考线作出一次修正。

  • onstop
    拖拽结束就比较简单了,将参考线的和一些其它状态清除就好了。

收获

在整理这些项目的过程中,除了核心代码本身,还有一些我觉得更为宝贵的收获。

在构建方式上,我们日常的项目开发都是把源码和第三方依赖等打包成“可执行的js文件”,即可以直接扔到浏览器上跑。但是在打造一款第三方项目时,这样做是不可行的。试想一下,如果一个项目有10个第三方依赖,而每个依赖都引入classnames,如果这些第三方依赖包都把classnames打包到源码中,那对于使用者来说,岂不是有10份重复的classnames代码?实际上我们需要做的是“只编译,不打包”。可否记得你在使用npm install的时候安装数量都是远远大于写在package.json内依赖的数量?没错,所有依赖及依赖的依赖...都是由用户统一安装,这样就可以避免了上述“10份重复的代码”的问题。另外,一般考虑到浏览器用户,确实会提供一份把依赖也打包进源码的UMD文件戳我

关于前端测试,这也是我之前了解较少的领域。一般前端业务变化频繁,生命周期相对较短,不太具备持续迭代的可能,因此写测试倒说不上是一个性价比高的选择。但是对于需要持续迭代的底层UI(例:组件库),单元测试的必要性还是很高的。因此我也假模假样地基于jestenzyme写了一些测试,以保证后续迭代的不会因为粗心大意而对之前功能有所影响。

总结

细心的朋友可能会发现,我在DraggableChild中使用的defaultPosition而不是position,这里就涉及到一些uncontrolled components的知识。一般来说,合格的React组件是需要提供positiondefaultPosition两种使用方式的。但是考虑到吸附功能是需要对元素的位置具备完全地控制能力,因为初步决定只提供defaultPosition的使用方式。

react-dragline算是我的第一个不那么玩具的开源项目了,欢迎大家交流拍砖~


原文首发于我的博客:https://www.vq0599.com/p/44
转载请注明出处

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,725评论 1 92
  • 很小很小的时候喜欢一大群人,喜欢跳好多人好多人在一起的舞蹈,喜欢喜欢一个人。 六七年后的现在,喜欢一个人做一件...
    凌晨柒染阅读 417评论 0 2
  • 上午大家正在热火朝天地磨课时,门外忽然一阵动静,正待出去探一究竟,一个大嗓门在外面响起:“同志们,今天都有...
    飞儿_008f阅读 254评论 2 1
  • “其实很寂寞只是不想说,说给你听又如何”,听着欢子的这首《其实很寂寞》,好有感觉,这不是完全在说...
    追梦路上123阅读 254评论 0 1
  • As we all know,men always have more priorities than women...
    龙爷爷7阅读 476评论 0 0