如何在 React 中使用 HTML 拖放 API
已发表: 2022-03-10拖放 API 是 HTML 最酷的功能之一。 它帮助我们在 Web 浏览器中实现拖放功能。
在当前上下文中,我们将从浏览器外部拖动文件。 在删除文件时,我们将它们放在一个列表中并显示它们的名称。 有了这些文件,我们就可以对文件执行一些其他操作,例如将它们上传到云服务器。
在本教程中,我们将重点介绍如何在 React 应用程序中实现拖放操作。 如果您需要的是一个纯JavaScript
实现,也许您首先想阅读“如何使用 Vanilla JavaScript 制作拖放文件上传器”,这是不久前由 Joseph Zimmerman 撰写的优秀教程。
dragenter
、 dragleave
、 dragover
和drop
事件
有八种不同的拖放事件。 每个都在拖放操作的不同阶段触发。 在本教程中,我们将专注于将项目放入拖放区时触发的四个: dragenter
、 dragleave
、 dragover
和drop
。
- 当拖动的项目进入有效的放置目标时,会触发
dragenter
事件。 - 当拖动的项目离开有效的放置目标时,会触发
dragleave
事件。 - 当拖动的项目被拖动到有效的放置目标上时,会触发
dragover
事件。 (它每几百毫秒触发一次。) - 当一个项目落在一个有效的放置目标上时触发
drop
事件,即拖过并释放。
我们可以通过定义ondragover
和ondrop
事件处理程序属性将任何 HTML 元素转换为有效的放置目标。
您可以从 MDN 网络文档中了解有关这八个事件的所有信息。
React 中的拖放事件
要开始,请从此 URL 克隆教程存储库:
https://github.com/chidimo/react-dnd.git
查看01-start
分支。 确保你也安装了yarn
。 您可以从 yarnpkg.com 获得它。
但如果您愿意,可以创建一个新的 React 项目并将App.js的内容替换为以下代码:
import React from 'react'; import './App.css'; function App() { return ( <div className="App"> <h1>React drag-and-drop component</h1> </div> ); } export default App;
此外,将App.css的内容替换为以下 CSS 样式:
.App { margin: 2rem; text-align: center; } h1 { color: #07F; } .drag-drop-zone { padding: 2rem; text-align: center; background: #07F; border-radius: 0.5rem; box-shadow: 5px 5px 10px #C0C0C0; } .drag-drop-zone p { color: #FFF; } .drag-drop-zone.inside-drag-area { opacity: 0.7; } .dropped-files li { color: #07F; padding: 3px; text-align: left; font-weight: bold; }
如果您克隆了 repo,请发出以下命令(按顺序)来启动应用程序:
yarn # install dependencies yarn start # start the app
下一步是创建拖放组件。 在src/
文件夹中创建一个文件DragAndDrop.js 。 在文件中输入以下函数:
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;
在返回div
中,我们定义了我们的焦点HTML
事件处理程序属性。 您可以看到与纯HTML
的唯一区别是驼峰式大小写。
div
现在是一个有效的放置目标,因为我们已经定义了onDragOver
和onDrop
事件处理程序属性。
我们还定义了处理这些事件的函数。 这些处理函数中的每一个都接收事件对象作为其参数。
对于每个事件处理程序,我们调用preventDefault()
来阻止浏览器执行其默认行为。 默认浏览器行为是打开拖放的文件。 我们还调用stopPropagation()
来确保事件不会从子元素传播到父元素。
将DragAndDrop
组件导入到App
组件中,并将其呈现在标题下方。
<div className="App"> <h1>React drag-and-drop component</h1> <DragAndDrop /> </div>
现在在浏览器中查看组件,您应该会看到如下图所示的内容。
如果您正在关注 repo,则相应的分支是02-start-dragndrop
使用useReducer
Hook 管理状态
我们的下一步将是为每个事件处理程序编写逻辑。 在我们这样做之前,我们必须考虑我们打算如何跟踪丢失的文件。 这是我们开始考虑状态管理的地方。
我们将在拖放操作期间跟踪以下状态:
-
dropDepth
这将是一个整数。 我们将使用它来跟踪我们在放置区中的深度。 稍后,我将用一个插图来解释这一点。 (感谢 Egor Egorov 为我照亮了这一点! ) -
inDropZone
这将是一个布尔值。 我们将使用它来跟踪我们是否在放置区域内。 -
FileList
这将是一个列表。 我们将使用它来跟踪已放入拖放区的文件。
为了处理状态,React 提供了useState
和useReducer
钩子。 我们将选择useReducer
钩子,因为我们将处理状态依赖于先前状态的情况。
useReducer
钩子接受类型为(state, action) => newState
的 reducer,并返回与dispatch
方法配对的当前状态。
您可以在 React 文档中阅读有关useReducer
的更多信息。
在App
组件内部(在return
语句之前),添加以下代码:
... 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: [] } ) ...
useReducer
钩子接受两个参数:reducer 和初始状态。 它返回当前状态和用于更新状态的dispatch
函数。 通过调度包含type
和可选负载的操作来更新状态。 对组件状态的更新取决于作为操作类型的结果从 case 语句返回的内容。 (注意这里我们的初始状态是一个object
。)
对于每个状态变量,我们定义了相应的 case 语句来更新它。 通过调用useReducer
返回的dispatch
函数来执行更新。
现在将data
并作为props
dispatch
到App.js文件中的DragAndDrop
组件:
<DragAndDrop data={data} dispatch={dispatch} />
在DragAndDrop
组件的顶部,我们可以从props
访问这两个值。
const { data, dispatch } = props;
如果您正在关注 repo,则相应的分支是03-define-reducers
。
让我们完成事件处理程序的逻辑。 请注意,省略号代表两条线:
e.preventDefault() e.stopPropagation() const handleDragEnter = e => { ... dispatch({ type: 'SET_DROP_DEPTH', dropDepth: data.dropDepth + 1 }); }; const handleDragLeave = e => { ... dispatch({ type: 'SET_DROP_DEPTH', dropDepth: data.dropDepth - 1 }); if (data.dropDepth > 0) return dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: false }) };
在下图中,我们嵌套了放置区域 A 和 B。A 是我们感兴趣的区域。 这是我们要监听拖放事件的地方。
当拖入拖放区时,每次我们碰到边界时,都会触发ondragenter
事件。 这发生在边界A-in
和B-in
。 由于我们正在进入该区域,我们增加dropDepth
。
同样,当拖出拖放区时,每次我们碰到边界时,都会触发ondragleave
事件。 这发生在边界A-out
和B-out
。 由于我们要离开该区域,因此我们减少了dropDepth
的值。 请注意,我们没有在边界B-out
处将inDropZone
设置为false
。 这就是为什么我们有这一行来检查 dropDepth 并从函数dropDepth
大于0
返回。
if (data.dropDepth > 0) return
这是因为即使ondragleave
事件被触发,我们仍然在区域 A 内。只有在我们击中A-out
并且dropDepth
现在为0
之后,我们才将inDropZone
设置为false
。 至此,我们已经离开了所有放置区。
const handleDragOver = e => { ... e.dataTransfer.dropEffect = 'copy'; dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: true }); };
每次触发此事件时,我们将inDropZone
设置为true
。 这告诉我们我们在放置区内。 我们还将dataTransfer
对象上的dropEffect
设置为copy
。 在 Mac 上,当您在拖放区中拖动项目时,这具有显示绿色加号的效果。
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.files
访问删除的文件。 该值是一个类似数组的对象,因此我们使用数组扩展语法将其转换为JavaScript
数组。
我们现在需要检查是否至少有一个文件,然后再尝试将其添加到我们的文件数组中。 我们还确保不包含文件列表中已经存在的fileList
。 清除dataTransfer
对象以准备下一次拖放操作。 我们还重置了dropDepth
和inDropZone
的值。
更新DragAndDrop
组件中div
的className
。 这将根据data.inDropZone
的值有条件地更改div
的className
。
<div className={data.inDropZone ? 'drag-drop-zone inside-drag-area' : 'drag-drop-zone'} ... > <p>Drag files here to upload</p> </div>
通过data.fileList
映射来渲染App.js中的文件列表。
<div className="App"> <h1>React drag-and-drop component</h1> <DragAndDrop data={data} dispatch={dispatch} /> <ol className="dropped-files"> {data.fileList.map(f => { return ( <li key={f.name}>{f.name}</li> ) })} </ol> </div>
现在尝试将一些文件拖放到放置区。 您会看到,当我们进入拖放区时,背景变得不那么不透明,因为inside-drag-area
类已激活。
当您在放置区中释放文件时,您会看到放置区下方列出的文件名:
本教程的完整版本位于04-finish-handlers
分支上。
结论
我们已经了解了如何使用HTML
拖放 API 在 React 中处理文件上传。 我们还学习了如何使用useReducer
钩子管理状态。 我们可以扩展文件handleDrop
函数。 例如,如果需要,我们可以添加另一个检查来限制文件大小。 这可以在检查现有文件之前或之后进行。 我们还可以在不影响拖放功能的情况下使拖放区可点击。
资源
- “Hooks API 参考:
useReducer
”,React 文档 - “HTML 拖放 API”,MDN 网络文档
- “使用 DOM 进行 Web 和 XML 开发的示例”,MDN 网络文档
- “如何使用 Vanilla JavaScript 制作拖放式文件上传器”,Smashing Magazine 的 Joseph Zimmerman
- “React 中的简单拖放文件上传”,Egor Egorov,Medium