ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

react-beautiful-dnd Sensor API 完全指南:用任意输入驱动拖拽与脚本化交互

react-beautiful-dnd Sensor API 完全指南:用任意输入驱动拖拽与脚本化交互 react-beautiful-dnd Sensor API 完全指南用任意输入驱动拖拽与脚本化交互【免费下载链接】react-beautiful-dndBeautiful and accessible drag and drop for lists with React项目地址: https://gitcode.com/gh_mirrors/re/react-beautiful-dnd本文以 docs/sensors/sensor-api.md 为主体结合仓库源码src/view/use-sensor-marshal/、src/types.js与stories/src/programmatic/展开讲解。Sensor API 是 react-beautiful-dnd 对外暴露的底层拖拽控制接口它既允许开发者从任意输入源语音、摄像头、脑电波乃至自定义脚本创建拖拽交互也允许构建完全程序化控制的脚本化体验如自动演示、引导式 onboarding。读完本文你将掌握 lock 锁机制、SensorAPI六个方法、PreDragActions/FluidDragActions/SnapDragActions三类动作对象以及如何编写自己的 sensor 并规避所有非法用法。一、Sensor API 是什么react-beautiful-dnd 内置了三套输入传感器鼠标docs/sensors/mouse.md、键盘docs/sensors/keyboard.md和触屏docs/sensors/touch.md。公开的Sensor API 与这三套内置传感器使用的是完全相同的底层接口因此它足够强大可以驱动项目开箱提供的任何一种拖拽体验。借助 Sensor API可以实现两类能力从任意输入类型创建拖拽交互不再局限于鼠标、键盘、触摸社区中已出现用语音指令、摄像头手势甚至脑电波信号触发拖拽的传感器示例如rbd-voice-sensor、rbd-webcam-sensor、rbd-thought-sensor等社区实验项目命名均遵循rbd-前缀约定注意这些示例主要面向演示、未经生产环境打磨。创建优美的脚本化体验以时间线或按钮驱动的方式自动执行拖拽。仓库的 stories/src/programmatic/ 目录就提供了三类程序化示例with-controls.jsx把控件映射为移动指令、runsheet.jsx在用户手动拖拽的同时运行脚本化拖拽、multiple-contexts.jsx多DragDropContext下的脚本化编排。二、核心概念Lock锁理解 Sensor API 之前必须先理解lock锁机制。一个sensor的核心职责是尝试认领一把锁锁允许对一个DragDropContext /内的拖拽进行独占控制。当交互结束时传感器释放锁。锁的存在保证了同一时刻只有一个Draggable /能被拖动对应规则一个DragDropContext /同时只能有一个Draggable /处于拖动状态。从源码结构看锁是一个单例对象见 src/view/use-sensor-marshal/lock.js其LockAPI提供了isClaimed()、isActive(lock)、claim(abandon)、release()、tryAbandon()五个方法。claim会在已有锁时通过invariant抛错Cannot claim lock as it is already claimedtryAbandon则在存在锁时调用其abandon回调并释放。一个最小的自定义 sensor 长这样取自原文档function mySimpleSensor(api: SensorAPI) { const preDrag: ?PreDragActions api.tryGetLock(item-1); // 未能获得锁 if (!preDrag) { return; } const drag: SnapDragActions preDrag.snapLift(); drag.moveDown(); drag.moveDown(); drag.moveDown(); drag.drop(); } function App() { return ( DragDropContext sensors{[mySimpleSensor]}{/*...*/}/DragDropContext ); }三、生命周期从认领锁到释放锁Sensor API 的完整生命周期分为三步对应仓库实现中的LockPhase枚举PRE_DRAG | DRAGGING | COMPLETED见 use-sensor-marshal.js尝试认领锁当一个sensor想要拖动某个条目时先调用api.tryGetLock(id)。认领可能失败原因多种多样——最常见的是另一个sensor已经持有锁。PreDrag 阶段锁已获得拖拽未开始拿到锁之后sensor会获得一组pre drag动作PreDragActions。这允许sensor在真正开始拖拽之前先占住锁再根据后续情况决定是否启动拖拽。这对先按住、移动超过阈值才真正开始拖的场景至关重要例如 sloppy click模糊点击检测鼠标按下后只有位移超过 5px 阈值源码常量sloppyClickThreshold 5见 use-mouse-sensor.js才升级为拖拽否则交互退化为普通点击。升级为拖拽锁pre drag锁可以升级为drag lock得到另一组 API——FluidDragActions流体拖拽或SnapDragActions吸附拖拽。一旦Draggable /被 lift提起就可以被移动了。从 use-sensor-marshal.js 的实现可以看到tryStart先经过canStart检查锁是否已被认领、DraggableId是否存在于 registry、isEnabled是否开启、应用当前状态是否允许开始拖拽对应 src/state/can-start-drag.js再执行交互元素检查随后lockAPI.claim(...)认领锁并进入PRE_DRAG阶段lift内部会校验当前必须处于PRE_DRAG阶段重复 lift 会抛错dispatch 对应的liftAction后将phase置为DRAGGINGdrop/cancel最终调用completed()释放锁、进入COMPLETED阶段。四、规则使用 Sensor API 必须遵守的规则其实只有两条一个DragDropContext /同一时刻只能有一个Draggable /处于拖动状态不能使用过期的outdated或被废弃的aborted锁详见下文Force abandoning locks。五、API 详解5.1 创建 sensorsensor本质上是一个 React hook。即便你完全用不到 React hook 的能力也可以把它当成普通函数来用——hook 只是让你在需要时能调用内置 hooks。把自定义 sensor 放进DragDropContext /的sensors数组即可function useMyCoolSensor(api: SensorAPI) { const start useCallback(function start(event: MouseEvent) { const preDrag: ?PreDragActions api.tryGetLock(item-2); if (!preDrag) { return; } preDrag.snapLift(); preDrag.moveDown(); preDrag.drop(); }, []); useEffect(() { window.addEventListener(click, start); return () { window.removeEventListener(click, start); }; }, []); } function App() { return ( DragDropContext sensors{[useMyCoolSensor]} Things / /DragDropContext ); }sensors数组不应动态变化——否则有违反 React hooks 规则 的风险。此外可以通过给DragDropContext /设置enableDefaultSensors{false}来禁用全部内置传感器鼠标、键盘、触屏。当你想让某个DragDropContext /只接受程序化控制时这个开关非常有用。从实现上看use-sensor-marshal.js 会先按enableDefaultSensors决定是否合并defaultSensors即useMouseSensor、useKeyboardSensor、useTouchSensor三者的数组再追加customSensors最后逐个以api为参数调用每个 sensor。5.2 尝试获取锁SensorAPI每个sensor都会收到一个SensorAPI对象用于尝试获取锁。其完整类型定义见 src/types.jstype Sensor (api: SensorAPI) void; type SensorAPI {| tryGetLock: TryGetLock, canGetLock: (id: DraggableId) boolean, isLockClaimed: () boolean, tryReleaseLock: () void, findClosestDraggableId: (event: Event) ?DraggableId, findOptionsForDraggable: (id: DraggableId) ?DraggableOptions, ||}; type DraggableOptions {| canDragInteractiveElements: boolean, shouldRespectForcePress: boolean, isEnabled: boolean, ||};各方法语义tryGetLock类型TryGetLock尝试为一个Draggable /获取锁的核心函数见下文。canGetLock(id)返回针对给定DraggableId是否有可能认领到锁。实现上即复用canStart检查use-sensor-marshal.js。isLockClaimed()若当前已有任意sensor持有锁则返回true。tryReleaseLock()释放当前活动的锁可用于程序化地取消拖拽。源码中它会先lockAPI.tryAbandon()若 store 状态不再是IDLE则再 dispatch 一个flush动作use-sensor-marshal.js。findClosestDraggableId(event)基于事件寻找最近的draggableId。它会从event.target向上DOM 祖先链查找最近的drag handle。对应实现 find-closest-draggable-id-from-event.js先按当前contextId构造[data-rbd-drag-handle-context-id...]选择器用closest向上查找拖拽手柄元素再读取其draggableId属性。findOptionsForDraggable(id)查找与某个Draggable /关联的DraggableOptions即canDragInteractiveElements、shouldRespectForcePress、isEnabled三项找不到则返回null。其实现直接查询 registry 中对应条目的optionsuse-sensor-marshal.js。TryGetLock的完整签名export type TryGetLock ( draggableId: DraggableId, forceStop?: () void, options?: TryGetLockOptions, ) ?PreDragActions;draggableId要拖动的Draggable /的DraggableId。forceStop可选当应用需要废弃abandon该锁时会被调用的函数。详见Force abandoning locks。TryGetLockOptionstype TryGetLockOptions { sourceEvent?: Event, };sourceEvent可选从用户输入事件启动拖拽时用于进一步校验。react-beautiful-dnd 会据此执行交互元素检查即isEventInInteractiveElement与canDragInteractiveElements的判定见 use-sensor-marshal.js。内置的鼠标、键盘传感器调用时都会传入原始事件例如api.tryGetLock(draggableId, stop, { sourceEvent: event })use-mouse-sensor.js。5.3 PreDrag 阶段PreDragActionsPreDragActions对象包含以下函数定义见 src/types.jstype PreDragActions {| // 发现锁是否仍然有效 isActive: () boolean, // 是否已指示应尊重 force press shouldRespectForcePress: () boolean, // 提起当前条目 fluidLift: (clientSelection: Position) FluidDragActions, snapLift: () SnapDragActions, // 不启动拖拽即取消 pre drag释放锁 abort: () void, ||};该阶段允许你在获得独占锁之后有条件地启动或放弃一次拖拽。当你还不确定拖拽是否应当开始时例如长按、sloppy click 检测这个阶段就很有价值。若想在不提起条目的情况下放弃 pre drag直接调用.abort()——对应源码中的abortPreDrag它校验当前仍处于PRE_DRAG阶段后执行lockAPI.release()use-sensor-marshal.js。5.4 拖拽阶段fluidLift 与 snapLift调用.fluidLift(clientSelection)或snapLift()会提起条目开始一次可见的拖拽并触发onDragStartresponder。之所以有两个lift函数是因为存在两种拖拽模式snap draggingSnapDragActions与fluid draggingFluidDragActions。共享部分DragActionstype DragActions {| drop: (args?: StopDragOptions) void, cancel: (args?: StopDragOptions) void, isActive: () boolean, shouldRespectForcePress: () boolean, ||}; type StopDragOptions {| shouldBlockNextClick: boolean, ||};StopDragOptions.shouldBlockNextClick用于在drop/cancel后阻止下一个 click 事件被消费——这正是点击拦截机制的一部分源码在需要拦截时会以capture: true, once: true, passive: false在window上绑定一次性的 clickpreventDefault处理器并在setTimeout后解绑use-sensor-marshal.js。流体拖拽FluidDragActionsDraggable /会跟随移动中的指针自然移动拖拽的impact影响即条目如何让位、能否组合由碰撞引擎collision engine控制——内置的鼠标传感器与触屏传感器使用的就是这种模式。type FluidDragActions {| ...DragActions, move: (clientSelection: Position) void, ||};.move()调用会通过requestAnimationFrame进行节流同一动画帧内的多次.move()调用只会合并为一次更新。从源码看fluidLift内部用rafSchd包装move并在lift的 cleanup 中调用move.cancel()use-sensor-marshal.jsconst drag: SnapDragActions preDrag.fluidLift({ x: 0, y: 0 }); // 以下调用会被合并成一次更新 drag.move({ x: 0, y: 1 }); drag.move({ x: 0, y: 2 }); drag.move({ x: 0, y: 3 }); // 动画帧结束后 // update(x: 0, y: 3)吸附拖拽SnapDragActionsDraggable /通过单条命令强制移动到新位置例如向下移动一格——内置的键盘传感器使用的就是这种模式。export type SnapDragActions {| ...DragActions, moveUp: () void, moveDown: () void, moveRight: () void, moveLeft: () void, ||};源码中snapLift会以条目当前 border-box 中心作为clientSelectionmovementMode: SNAPdispatch lift 动作并将moveUp/moveRight/moveDown/moveLeft分别映射到对应的 action creatoruse-sensor-marshal.js。内置键盘传感器正是这样工作的空格键触发snapLift()方向键触发actions.moveDown()等空格再按一次或按esc则drop()/cancel()use-keyboard-sensor.js。六、Force abandoning locks强制废弃锁应用可以在任意时刻废弃abandon一把锁例如发生错误时。如果对一把已被废弃的锁继续执行操作什么都不会发生。SensorAPI.tryGetLock()的第二个参数就是forceStop函数当应用需要废弃这把锁时forceStop会被调用。锁被废弃之后若再调用其上的任何函数都会无效并往控制台打印警告。典型用法function useMySensor(api: SensorAPI) { let unbindClick; function forceStop() { if (unbindClick) { unbindClick(); } } const preDrag: ?PreDragActions api.tryGetLock(item-1, forceStop); // 未能获得锁 if (!preDrag) { return; } const drag: SnapDragActions preDrag.snapLift(); const move () drag.moveDown(); window.addEventListener(click, move); unbindClick window.removeEventListener(click, move); }PreDragActions、FluidDragActions、SnapDragActions都提供isActive()方法用于探测锁是否仍然有效。如果你不想提供forceStop()最稳妥的做法是在每次调用前做防御性的isActive()检查function useMySensor(api: SensorAPI) { const preDrag: ?PreDragActions api.tryGetLock(); // 未能获得锁 if (!preDrag) { return; } const drag: SnapDragActions preDrag.snapLift(); const move () { if (drag.isActive()) { drag.moveDown(); return; } // 不再活跃时解绑 window.removeEventListener(click, move); }; window.addEventListener(click, move); }从实现看useSensorMarshal在两类时机会自动调用lockAPI.tryAbandon()其一是 store 状态从isDragging变为非isDragging时通过订阅 store 的前后状态对比其二是组件卸载时useLayoutEffect的清理函数返回lockAPI.tryAbandonuse-sensor-marshal.js。同时所有动作在 dispatch 前都会经过isActive({ expected, phase, isLockActive, shouldWarn })校验——锁失效或阶段不匹配时打印与Tips相关的警告文案use-sensor-marshal.js。七、非法行为Invalid behaviours以下行为全部源于未遵守生命周期见上文第三节。符号含义⚠️ 打印警告❌ 抛出错误。行为结果在forceStop()被调用后使用任何PreDragAction、FluidDragAction或SnapDragAction⚠️ 警告在.abort()被调用后使用任何PreDragAction⚠️ 警告在.cancel()或.drop()被调用后使用任何FluidDragAction或SnapDragAction⚠️ 警告对同一个PreDragAction调用两次lift函数❌ 抛错其中重复 lift 抛错在源码中有明确实现lift函数开头校验phase PRE_DRAG否则先执行completed()再触发invariant(phase PRE_DRAG, ...)断言失败use-sensor-marshal.js。八、程序化控制实战仓库中的示例如果你打算构建脚本化体验仓库 stories/src/programmatic/ 目录是极佳参考with-controls.jsx用 UI 控件映射拖拽移动。它通过sensorAPIRef保存SensorAPI点击控件时调用api.tryGetLock(quoteId, noop)后preDrag.snapLift()把上/下/左/右按钮映射为moveUp/moveDown/moveLeft/moveRight。runsheet.jsx在用户手动拖拽的同时运行脚本化拖拽。useDemoSensor中直接对1号条目tryGetLock(1, noop)后执行snapLift()与一系列moveDown()。multiple-contexts.jsx展示多个DragDropContext /各带独立 sensor 的脚本化编排通过getSensor(delay)工厂为每个 context 生成延迟启动的传感器。这些示例与文档中提到的社区rbd-*系列传感器一样主要面向演示而非生产环境但足以证明 Sensor API 的表达力任何你能监听到的事件或时序逻辑都可以转化为一次真实的、无障碍的、与内置传感器同等能力的拖拽交互。九、小结Sensor API 与内置鼠标/键盘/触屏传感器共用同一套接口因此能力边界与内置体验完全一致一切围绕lock展开tryGetLock认领 →PreDragActions预判 →fluidLift/snapLift升级 →drop/cancel/abort收尾全程遵循固定的PRE_DRAG → DRAGGING → COMPLETED阶段机SensorAPI的六个方法与三类动作对象PreDragActions、FluidDragActions、SnapDragActions构成了全部对外契约类型定义可在 src/types.js 中完整查阅务必通过forceStop或isActive()防御性检查处理锁的废弃避免触发警告与错误。继续阅读鼠标传感器 · 键盘传感器 · 触屏传感器 · 回到文档首页【免费下载链接】react-beautiful-dndBeautiful and accessible drag and drop for lists with React项目地址: https://gitcode.com/gh_mirrors/re/react-beautiful-dnd创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表