如何在 React 中使用 HTML 拖放 API

已发表: 2022-03-10
快速总结↬在本教程中,我们将构建一个用于文件和图像上传的 React 拖放组件。 在此过程中,我们将了解 HTML 拖放 API。 我们还将学习如何使用 useReducer 钩子来管理 React 功能组件中的状态。

拖放 API 是 HTML 最酷的功能之一。 它帮助我们在 Web 浏览器中实现拖放功能。

在当前上下文中,我们将从浏览器外部拖动文件。 在删除文件时,我们将它们放在一个列表中并显示它们的名称。 有了这些文件,我们就可以对文件执行一些其他操作,例如将它们上传到云服务器。

在本教程中,我们将重点介绍如何在 React 应用程序中实现拖放操作。 如果您需要的是一个纯JavaScript实现,也许您首先想阅读“如何使用 Vanilla JavaScript 制作拖放文件上传器”,这是不久前由 Joseph Zimmerman 撰写的优秀教程。

dragenterdragleavedragoverdrop事件

有八种不同的拖放事件。 每个都在拖放操作的不同阶段触发。 在本教程中,我们将专注于将项目放入拖放区时触发的四个: dragenterdragleavedragoverdrop

  1. 当拖动的项目进入有效的放置目标时,会触发dragenter事件。
  2. 当拖动的项目离开有效的放置目标时,会触发dragleave事件。
  3. 当拖动的项目被拖动到有效的放置目标上时,会触发dragover事件。 (它每几百毫秒触发一次。)
  4. 当一个项目落在一个有效的放置目标上时触发drop事件,即拖过并释放。

我们可以通过定义ondragoverondrop事件处理程序属性将任何 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现在是一个有效的放置目标,因为我们已经定义了onDragOveronDrop事件处理程序属性。

我们还定义了处理这些事件的函数。 这些处理函数中的每一个都接收事件对象作为其参数。

对于每个事件处理程序,我们调用preventDefault()来阻止浏览器执行其默认行为。 默认浏览器行为是打开拖放的文件。 我们还调用stopPropagation()来确保事件不会从子元素传播到父元素。

DragAndDrop组件导入到App组件中,并将其呈现在标题下方。

 <div className="App"> <h1>React drag-and-drop component</h1> <DragAndDrop /> </div>

现在在浏览器中查看组件,您应该会看到如下图所示的内容。

拖放区
要转换为拖放区的div (大预览)

如果您正在关注 repo,则相应的分支是02-start-dragndrop

使用useReducer Hook 管理状态

我们的下一步将是为每个事件处理程序编写逻辑。 在我们这样做之前,我们必须考虑我们打算如何跟踪丢失的文件。 这是我们开始考虑状态管理的地方。

我们将在拖放操作期间跟踪以下状态:

  1. dropDepth
    这将是一个整数。 我们将使用它来跟踪我们在放置区中的深度。 稍后,我将用一个插图来解释这一点。 (感谢 Egor Egorov 为我照亮了这一点!
  2. inDropZone
    这将是一个布尔值。 我们将使用它来跟踪我们是否在放置区域内。
  3. FileList
    这将是一个列表。 我们将使用它来跟踪已放入拖放区的文件。

为了处理状态,React 提供了useStateuseReducer钩子。 我们将选择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 dispatchApp.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 和 ondragleave 事件的图示
ondragenterondragleave事件的插图(大预览)

当拖入拖放区时,每次我们碰到边界时,都会触发ondragenter事件。 这发生在边界A-inB-in 。 由于我们正在进入该区域,我们增加dropDepth

同样,当拖出拖放区时,每次我们碰到边界时,都会触发ondragleave事件。 这发生在边界A-outB-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对象以准备下一次拖放操作。 我们还重置了dropDepthinDropZone的值。

更新DragAndDrop组件中divclassName 。 这将根据data.inDropZone的值有条件地更改divclassName

 <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