如何在 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