一. 設計理念
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 通常需要處理 hover、drop 等事件)等
三. 核心實現
./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 個參數(type,spec 和 collect()),例如:
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 個參數(type,spec 和 collect()):
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%'
}}
>
<HANDLE>
</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 文件一樣停不下來
暫無評論,快來發表你的看法吧