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