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)

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

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

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏地方网络工作室的专栏

React + webpack 开发单页面应用简明中文文档教程(十一)将项目打包到子目录运行

版权声明:本文为 FengCms FungLeo 原创文章,允许转载,但转载必须注明出处并附带首发链接 ...

6130
来自专栏地方网络工作室的专栏

React + webpack 开发单页面应用简明中文文档教程(十)在 jsx 和 scss 中使用图片

版权声明:本文为 FengCms FungLeo 原创文章,允许转载,但转载必须注明出处并附带首发链接 ...

10730
来自专栏地方网络工作室的专栏

React + webpack 开发单页面应用简明中文文档教程

由上面的代码,我们可以看到 this.props 是用来接收父组件的传值的。怎么传值的呢?我们去修改我们的 page/site/index.jsx 文件

8950
来自专栏地方网络工作室的专栏

React + webpack 开发单页面应用简明中文文档教程(五)配置 api 接口请求文件

版权声明:本文为 FengCms FungLeo 原创文章,允许转载,但转载必须注明出处并附带首发链接 ...

13760
来自专栏地方网络工作室的专栏

React + webpack 开发单页面应用简明中文文档教程(八)Link 跳转以及编写内容页面

版权声明:本文为 FengCms FungLeo 原创文章,允许转载,但转载必须注明出处并附带首发链接 ...

7820
来自专栏地方网络工作室的专栏

React + webpack 开发单页面应用简明中文文档教程(三)目录说明以及调整项目构架文件

版权声明:本文为 FengCms FungLeo 原创文章,允许转载,但转载必须注明出处并附带首发链接 ...

9530
来自专栏地方网络工作室的专栏

React + webpack 开发单页面应用简明中文文档教程(四)调整项目文件以及项目配置

版权声明:本文为 FengCms FungLeo 原创文章,允许转载,但转载必须注明出处并附带首发链接 ...

13240
来自专栏地方网络工作室的专栏

React + webpack 开发单页面应用简明中文文档教程(六)渲染一个列表,初识 jsx 文件

版权声明:本文为 FengCms FungLeo 原创文章,允许转载,但转载必须注明出处并附带首发链接 ...

8420
来自专栏地方网络工作室的专栏

DeepinLinux 运行 React 项目出现 Error: watch ENOSPC 的解决方案

今天给新来的前端同事安装了 DeepinLinux,然后在运行 React 项目的时候出现了 Error: watch ENOSPC 的报错。我很奇怪为什么会出...

10540
来自专栏地方网络工作室的专栏

React + webpack 开发单页面应用简明中文文档教程(九)子组件给父组件传值

版权声明:本文为 FengCms FungLeo 原创文章,允许转载,但转载必须注明出处并附带首发链接 ...

12170

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励