React DnD

一.设计理念

React DnD gives you a set of powerful primitives, but it does not contain any readymade components. It’s lower level than jQuery UI or interact.js and is focused on getting the drag and drop interaction right, leaving its visual aspects such as axis constraints or snapping to you. For example, React DnD doesn’t plan to provide a Sortable component. Instead it makes it easy for you to build your own, with any rendering customizations that you need.

(摘自Non-Goals)

简言之,把DnD特性拆解成一些基础interface(能抓东西,即萝卜;能放手容器,即坑),并把DnD内部状态暴露给实现了这些interface的实例。不像其它库一样提供无穷尽的Draggable Component应对常见业务场景,React DnD从相对底层的角度提供支持,是对拖放能力的抽象与封装,通过抽象来简化使用,通过封装来屏蔽下层差异

二.术语概念

Backend

HTML5 DnD API兼容性不怎么样,并且不适用于移动端,所以干脆把DnD相关具体DOM事件抽离出去,单独作为一层,即Backend:

Under the hood, all the backends do is translate the DOM events into the internal Redux actions that React DnD can process.

Item和Type

Item是对元素/组件的抽象理解,拖放的对象不是DOM元素或React组件,而是特定数据模型(Item):

An item is a plain JavaScript object describing what’s being dragged.

进行这种抽象同样是为了解耦:

Describing the dragged data as a plain object helps you keep the components decoupled and unaware of each other.

Type与Item的关系类似于Class与Class Instance,Type作为类型标识符用来表示同类Item:

A type is a string (or a symbol) uniquely identifying a whole class of items in your application.

Type作为萝卜(drag source)和坑(drop target)的匹配依据,相当于经典DnD库的group name

Monitor

Monitor是拖放状态的集合,比如拖放操作是否正在进行,是的话萝卜是哪个坑是哪个:

React DnD exposes this state to your components via a few tiny wrappers over the internal state storage called the monitors.

例如:

monitor.isDragging()
monitor.isOver()
monitor.canDrop()
monitor.getItem()

props注入的方式暴露DnD内部状态,类似于Redux的mapStateToProps

export default DragSource(Types.CARD, cardSource, (connector, monitor) => ({
 // You can ask the monitor about the current drag state:
 isDragging: monitor.isDragging()
}))(Card);

P.S.事实上,React DnD就是基于Redux实现的,见下文核心实现部分

Connector

Connector用来建立DOM抽象(React)与DnD Backend需要的具体DOM元素之间的联系:

The connectors let you assign one of the predefined roles (a drag source, a drag preview, or a drop target) to the DOM nodes

用法很有意思:

render() {
 const { highlighted, hovered, connectDropTarget } = this.props; // 1.声明DnD Role对应的DOM元素
 return connectDropTarget(
   <div className={classSet({
     'Cell': true,
     'Cell--highlighted': highlighted,
     'Cell--hovered': hovered
   })}>
     {this.props.children}
   </div>
 );
}// 2.从connector取出connect方法,并注入props
export default DragSource(Types.CARD, cardSource, (connector, monitor) => ({
 // Call this function inside render()
 // to let React DnD handle the drag events:
 connectDropTarget: connector.dropTarget()
}))(Card);

建立联系的部分connectDropTarget(<div/>)看起来相当优雅,猜测实际作用应该相当于:

render() {
 const { connectToRole } = this.props;
 return <div ref={(node) => connectToRole(node)}></div>
}

猜对了:

Internally it works by attaching a callback ref to the React element you gave it.

Drag Source与Drop Target

上面提到过这两个东西,可以称之为DnD Role,表示在DnD中所饰角色,除了drag source和drop target外,还有一个叫drag preview,一般可以看作另一种状态的drag source

DnD Role是React DnD中的基本抽象单元:

They really tie the types, the items, the side effects, and the collecting functions together with your components.

是该角色相关描述及动作的集合,包括Type,DnD Event Handler(例如drop target通常需要处理hoverdrop等事件)等

三.核心实现

./packages
├── dnd-core
├── react-dnd
├── react-dnd-html5-backend
└── react-dnd-test-backend

对应逻辑结构是这样:

API 接React
 react-dnd 定义Context,提供Provider、Container factory等上层API
-------
Core 抽象(定义interface)
 dnd-core 定义Action、Reducer,连接上下层
-------
Backends 接native,封装DnD特性(实现interface)
 react-dnd-xxx-backend 接具体环境,通过Dispatch Action把native DnD状态传递到上层

可以看作基于Redux的逻辑拆解,中间层Core持有DnD状态,下层Backends负责实现约定的interface,作为Core的数据源,上层API从Core取出状态并传递给业务层

四.基本用法

1.指定DragDropContext

给App根组件声明DragDropContext,例如:

import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';class App extends Component {}
export default DragDropContext(HTML5Backend)(App);

2.添加DragSource

DragSource高阶组件接受3个参数(typespeccollect()),例如:

export const ItemTypes = {
 KNIGHT: 'knight'
};const knightSpec = {
 beginDrag(props) {
   // 定义Item结构,通过monitor.getItem()读取
   return {
     pieceId: props.id
   };
 }
};function collect(connector, monitor) {
 return {
   connectDragSource: connectorconnector.dragSource(),
   isDragging: monitor.isDragging()
 }
}

最后与Component/Container连接起来(像Redux connect()一样):

export default DragSource(ItemTypes.KNIGHT, knightSpec, collect)(Knight);

组件拿到注入的DnD状态渲染对应UI,例如:

render() {
 const { connectDragSource, isDragging } = this.props;
 return connectDragSource(
   <div style={{
     opacity: isDragging ? 0.5 : 1,
     cursor: 'move'
   }} />
 );
}

很自然地实现了被拖走的效果(拖放对象变成半透明),看不到复杂的DnD处理逻辑(这些都被封装到了React DnD Backend,仅暴露出业务需要的DnD状态)

3.添加DropTarget

同样需要3个参数(typespeccollect()):

const dropSpec = {
 canDrop(props) {
   return canMoveKnight(props.x, props.y);
 }, drop(props, monitor) {
   const { id } = monitor.getItem();
   moveKnight(id, props.x, props.y);
 }
};function collect(connector, monitor) {
 return {
   connectDropTarget: connector.dropTarget(),
   isOver: monitor.isOver(),
   canDrop: monitor.canDrop()
 };
}

最后连接起来:

export default DropTarget(ItemTypes.KNIGHT, dropSpec, collect)(BoardSquare);

组件取这些注入的DnD状态来展示对应的UI,例如:

render() {
 const { connectDropTarget, isOver, canDrop } = this.props; return connectDropTarget(
   <div>
     {isOver && !canDrop && this.renderOverlay('red')}
     {!isOver && canDrop && this.renderOverlay('yellow')}
     {isOver && canDrop && this.renderOverlay('green')}
   </div>
 );
}

坑根据拖动操作合法性变色的效果也实现了,看起来同样很自然

4.定制DragPreview

浏览器DnD默认会根据被拖动的元素创建drag preview(一般像个半透明截图),需要定制的话,与DragSource的创建方式类似:

function collect(connector, monitor) {
 return {
   connectDragSource: connector.dragSource(),
   connectDragPreview: connector.dragPreview()
 }
}

通过注入的connectDragPreview()来定制DragPreview,接口签名与connectDragSource()一致,都是dragPreview() => (elementOrNode, options?),例如常见的拖动抓手(handle)效果可以这样实现:

render() {
 const { connectDragSource, connectDragPreview } = this.props;   return connectDragPreview(
     <div
       style={{
         position: 'relative',
         width: 100,
         height: 100,
         backgroundColor: '#eee'
       }}
     >
       Card Content
       {connectDragSource(
         <div
           style={{
             position: 'absolute',
             top: 0,
             left: '100%'
           }}
         >
           &lt;HANDLE&gt;
         </div>
       )}
     </div>
 );
}

另外,还可以把Image对象作为DragPreview(IE不支持):

componentDidMount() {
 const img = new Image();
 img.src = 'http://mysite.com/image.jpg';
 img.onload = () => this.props.connectDragPreview(img);
}

五.在线Demo

Github仓库:ayqy/example-react-dnd-nested

在线Demo:https://ayqy.github.io/dnd/demo/react-dnd/index.html

参考资料

  • React DnD:又是如诗如画的文档,与Redux文档一样停不下来
  • react-dnd/react-dnd

本文分享自微信公众号 - 前端向后(backward-fe),作者:ayqy

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-03-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • JS内存泄漏排查方法

    内存泄漏是一个累积的过程,只有页面生命周期略长的时候才算是个问题(所谓“刷新一下满血复活”)。频繁交互能够加快累积过程,偏展示的页面很难把这样的问题暴露出来。最...

    ayqy贾杰
  • *aaS到底是什么?

    感谢支持ayqy个人订阅号,每周义务推送1篇(only unique one)原创精品博文,话题包括但不限于前端、Node、Android、数学...

    ayqy贾杰
  • 如何理解 Scalability?

    关注「前端向后」微信公众号,你将收获一系列「用心原创」的高质量技术文章,主题包括但不限于前端、Node.js以及服务端技术

    ayqy贾杰
  • WPF 使用 Skia 绘制 WriteableBitmap 图片

    本文告诉大家如何在 WPF 中使用 SkiaSharp 调用 Skia 这个全平台底层渲染框架,使用绘制命令在 WriteableBitmap 图片上绘制内容

    林德熙
  • 【收藏】深度学习在计算机视觉领域的应用一览!超全总结!

    转载自知乎https://zhuanlan.zhihu.com/p/55747295

    小白学视觉
  • 一文全览深度学习在计算机视觉领域的应用

    本文首发于知乎,作者为奇点汽车美研中心总裁兼自动驾驶首席科学家黄浴,AI 开发者经授权转载。

    AI研习社
  • 聊聊cheddar的DomainEvent

    Cheddar/cheddar/cheddar-domain/src/main/java/com/clicktravel/cheddar/domain/even...

    codecraft
  • 聊聊cheddar的DomainEvent

    Cheddar/cheddar/cheddar-domain/src/main/java/com/clicktravel/cheddar/domain/even...

    codecraft
  • 快云小助手网页版 Linux 面板安装过程记录

    前几天老魏在快云小助手(快云管理助手)windows 服务器快速部署 web 环境中提到了体验景安快云提供的 web 面板,可以提供简单的服务器管理功能,同时老...

    魏艾斯博客www.vpsss.net
  • WPF 实现滚动字幕动画

    程序要显示动态,日志之类的东西,在一个区域中显示一个文本,需要替换时,直接就换了也没啥,可是想要弄的美观一点,加个动画就美滋滋了

    zls365

扫码关注云+社区

领取腾讯云代金券