-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
234 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
## 1 引言 | ||
|
||
拖拽是前端非常常见的交互操作,但显然拖拽是强 DOM 交互的,而 React 绕过了 DOM 这一层,那么基于 React 的拖拽方案就必定值得聊一聊。 | ||
|
||
结合 [How To Use The HTML Drag-And-Drop API In React](https://www.smashingmagazine.com/2020/02/html-drag-drop-api-react/) 这篇文章,让我们谈谈 React 拖拽这些事。 | ||
|
||
## 2 概述 | ||
|
||
原文说的比较简单,笔者先快速介绍其中重点部分。 | ||
|
||
首先拖拽主要的 API 有 4 个:`dragEnter` `dragLeave` `dragOver` `drop`,分别对应拖入、拖出、正在当前元素范围内拖拽、完成拖入动作。 | ||
|
||
基于这些 API,我们可以利用 React 实现一个拖入区域: | ||
|
||
```jsx | ||
import React from "react"; | ||
|
||
const DragAndDrop = props => { | ||
const handleDragEnter = e => { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
}; | ||
const handleDragLeave = e => { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
}; | ||
const handleDragOver = e => { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
}; | ||
const handleDrop = e => { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
}; | ||
return ( | ||
<div | ||
className={"drag-drop-zone"} | ||
onDrop={e => handleDrop(e)} | ||
onDragOver={e => handleDragOver(e)} | ||
onDragEnter={e => handleDragEnter(e)} | ||
onDragLeave={e => handleDragLeave(e)} | ||
> | ||
<p>Drag files here to upload</p> | ||
</div> | ||
); | ||
}; | ||
export default DragAndDrop; | ||
``` | ||
|
||
`preventDefault` 指的是阻止默认响应,这个响应可能是跳转页面之类的,`stopPropagation` 是阻止冒泡,这样同样监听了事件的父元素就不会收到响应,我们可以精准作用于嵌套的子元素。 | ||
|
||
接下来是拖拽状态管理,提到了 `useReducer`,顺便复习一下用法: | ||
|
||
```jsx | ||
... | ||
const reducer = (state, action) => { | ||
switch (action.type) { | ||
case 'SET_DROP_DEPTH': | ||
return { ...state, dropDepth: action.dropDepth } | ||
case 'SET_IN_DROP_ZONE': | ||
return { ...state, inDropZone: action.inDropZone }; | ||
case 'ADD_FILE_TO_LIST': | ||
return { ...state, fileList: state.fileList.concat(action.files) }; | ||
default: | ||
return state; | ||
} | ||
}; | ||
const [data, dispatch] = React.useReducer( | ||
reducer, { dropDepth: 0, inDropZone: false, fileList: [] } | ||
) | ||
... | ||
``` | ||
|
||
最后一个关键点在于拖入后的处理,利用 `dispatch` 增加拖入文件、设置拖入状态即可: | ||
|
||
```js | ||
const handleDrop = e => { | ||
... | ||
let files = [...e.dataTransfer.files]; | ||
|
||
if (files && files.length > 0) { | ||
const existingFiles = data.fileList.map(f => f.name) | ||
files = files.filter(f => !existingFiles.includes(f.name)) | ||
|
||
dispatch({ type: 'ADD_FILE_TO_LIST', files }); | ||
e.dataTransfer.clearData(); | ||
dispatch({ type: 'SET_DROP_DEPTH', dropDepth: 0 }); | ||
dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: false }); | ||
} | ||
}; | ||
``` | ||
|
||
`e.dataTransfer.clearData` 函数用于清除拖拽过程中产生的临时变量,这些临时变量可以通过 `e.dataTransfer.xxx =` 的方式赋值,一般用于拖拽过程中值的传递。 | ||
|
||
总结一下,利用 HTML5 的 API 将拖拽转化为状态,最终通过状态映射到 UI。 | ||
|
||
原文内容还是比较简单的,笔者在精读部分再拓展一些更体系化的内容。 | ||
|
||
## 3 精读 | ||
|
||
现阶段拖拽主要分为两种,一种是 HTML5 原生规范的拖拽,这种方式在拖拽过程中不会影响 DOM 结构。另一种是完全所见即所得的拖拽方式,拖拽过程中 DOM 位置会随之变动,好处是可以立即反馈拖拽结果,当然缺点是华而不实,一旦用在生产环境,这种拖拽过程可能导致页面结构频繁跳动,反而看不清拖拽效果。 | ||
|
||
由于本文也采用了第一种拖拽方案,因为笔者再重新整理一遍自己的封装思路。 | ||
|
||
从使用角度反推,假设我们拥有一个拖拽库,那必定要拥有两个 API: | ||
|
||
```jsx | ||
import { DragContainer, DropContainer } from 'dnd' | ||
|
||
const DragItem = ( | ||
<DragContainer> | ||
{({ dragProps }) => ( | ||
<div {...dragProps} /> | ||
)} | ||
</DragContainer> | ||
) | ||
|
||
const DropItem = ( | ||
<DropContainer> | ||
{({ dropProps }) => ( | ||
<div {...dropProps} /> | ||
)} | ||
</DropContainer> | ||
) | ||
``` | ||
|
||
`DragContainer` 包裹可以被拖拽的元素,`DragContainer` 包裹可以被拖入的元素,而至于 `dragProps` 与 `dropProps` 需要透传到子元素的 dom 节点,是为了利用 DOM API 控制拖拽效果,这也是拖拽唯一对 DOM 的要求,双方元素都需要有实体 DOM 承载。 | ||
|
||
而上面例子中给出 `dragProps` 与 `dropProps` 的方式属于 RenderProps,我们可以将 `children` 当作函数执行以达到效果: | ||
|
||
```jsx | ||
const DragContainer = ({ children, componentId }) => { | ||
const { dragProps } = useDnd(componentId) | ||
|
||
return children({ | ||
dragProps | ||
}) | ||
} | ||
|
||
const DropContainer = ({ children, componentId }) => { | ||
const { dropProps } = useDnd(componentId) | ||
|
||
return children({ | ||
dropProps | ||
}) | ||
} | ||
``` | ||
|
||
那么这里创建了一个自定义 Hook `useDnd` 接收 `dragProps` 与 `dropProps`,这个自定义 Hook 可以这么写: | ||
|
||
```jsx | ||
const useDnd = ({ componentId }) => { | ||
const dragProps = {} | ||
const dropProps = {} | ||
|
||
return { dragProps, dropProps } | ||
} | ||
``` | ||
|
||
接下来,我们就要分别实现 `drag` 与 `drop` 了。 | ||
|
||
对 `drag` 来说,只要实现 `onDragStart` 与 `onDragEnd` 即可: | ||
|
||
```jsx | ||
const dragProps = { | ||
onDragStart: ev => { | ||
ev.stopPropagation() | ||
ev.dataTransfer.setData('componentId', componentId) | ||
}, | ||
onDragEnd: ev => { | ||
// 做一些拖拽结束的清理工作 | ||
} | ||
} | ||
``` | ||
|
||
`stopPropagation` 的作用在原文简介中已经介绍过了,`setData` 则是通知拖拽方,当前拖拽的组件 id 是什么,**这是由于拖拽由 `drag` 发起而由 `drop` 响应,因此必须有个数据传输过程,而 `dataTransfer` 就最适合做这件事。** | ||
|
||
对于 `drop` 来说,只要实现 `onDragOver` 与 `onDrop` 即可: | ||
|
||
```jsx | ||
const dropProps = { | ||
onDropOver: ev => { | ||
// 做一些样式处理,提示用户此时松手会将元素防止在何处 | ||
}, | ||
onDrop: ev => { | ||
ev.stopPropagation() | ||
const componentId = ev.dataTransfer.getData('componentId') | ||
// 通过 componentId 修改数据,通过 React Rerender 刷新 UI | ||
} | ||
} | ||
``` | ||
|
||
重点在 `onDrop`,它是实现拖拽效果的 “真正执行处”,最终通过修改 UI 的方式更新数据。 | ||
|
||
存在一种场景,一个容器既可以被拖动,也可以被拖入,这种情况一般这个组件是个容器,但这个容器可以被拖入到其他容器中,可以自由嵌套。 | ||
|
||
实现这种场景的方式就是将 `DragContainer` 与 `DropContainer` 作用到一个组件上: | ||
|
||
```jsx | ||
const Box = ( | ||
<DragContainer> | ||
{({ dragProps }) => ( | ||
<DropContainer> | ||
{({ dropProps }) => { | ||
<div {...dragProps} {...dropProps} /> | ||
}} | ||
</DropContainer> | ||
)} | ||
</DragContainer> | ||
) | ||
``` | ||
|
||
之所以能嵌套,在于 HTML5 的 API 允许一个元素同时拥有 `onDragStart`、`onDrop` 这两种属性,而上面的语法不过是同时将这两种属性传给组件 DOM。 | ||
|
||
所以,动手实现一个拖拽库就是这么简单,只要活用 HTML5 的拖拽 API,结合 React 一些特殊语法便够了。 | ||
|
||
## 4 总结 | ||
|
||
最后留下一个思考题,许多具有拖拽功能的系统都具备 “拖拽 placeholder” 的功能,即拖拽元素的过程中,在其 “落点” 位置展示一条横线或竖线,引导出松手后元素位置落点,如图所示: | ||
|
||
<img width=400 src="https://img.alicdn.com/tfs/TB11H04wbY1gK0jSZTEXXXDQVXa-1434-384.png"> | ||
|
||
那么这条辅助线是通过什么方式实现的呢?欢迎在评论区留言!如果你有辅助线实现方案解析的文章,欢迎分享,也可以期待笔者未来专门写一篇 “拖拽 placeholder” 实现剖析的精读。 | ||
|
||
> 讨论地址是:[精读《手写 JSON Parser》 · Issue #233 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/233) | ||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** | ||
|
||
> 关注 **前端精读微信公众号** | ||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg"> | ||
|
||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)) |