拖拽是前端很是常見的交互操做,但顯然拖拽是強 DOM 交互的,而 React 繞過了 DOM 這一層,那麼基於 React 的拖拽方案就一定值得聊一聊。html
結合 How To Use The HTML Drag-And-Drop API In React 這篇文章,讓咱們談談 React 拖拽這些事。前端
原文說的比較簡單,筆者先快速介紹其中重點部分。react
首先拖拽主要的 API 有 4 個:dragEnter
dragLeave
dragOver
drop
,分別對應拖入、拖出、正在當前元素範圍內拖拽、完成拖入動做。git
基於這些 API,咱們能夠利用 React 實現一個拖入區域:github
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
是阻止冒泡,這樣一樣監聽了事件的父元素就不會收到響應,咱們能夠精準做用於嵌套的子元素。api
接下來是拖拽狀態管理,提到了 useReducer
,順便複習一下用法:微信
... 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
增長拖入文件、設置拖入狀態便可:dom
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。spa
原文內容仍是比較簡單的,筆者在精讀部分再拓展一些更體系化的內容。
現階段拖拽主要分爲兩種,一種是 HTML5 原生規範的拖拽,這種方式在拖拽過程當中不會影響 DOM 結構。另外一種是徹底所見即所得的拖拽方式,拖拽過程當中 DOM 位置會隨之變更,好處是能夠當即反饋拖拽結果,固然缺點是華而不實,一旦用在生產環境,這種拖拽過程可能致使頁面結構頻繁跳動,反而看不清拖拽效果。
因爲本文也採用了第一種拖拽方案,由於筆者再從新整理一遍本身的封裝思路。
從使用角度反推,假設咱們擁有一個拖拽庫,那一定要擁有兩個 API:
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
看成函數執行以達到效果:
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 能夠這麼寫:
const useDnd = ({ componentId }) => { const dragProps = {} const dropProps = {} return { dragProps, dropProps } }
接下來,咱們就要分別實現 drag
與 drop
了。
對 drag
來講,只要實現 onDragStart
與 onDragEnd
便可:
const dragProps = { onDragStart: ev => { ev.stopPropagation() ev.dataTransfer.setData('componentId', componentId) }, onDragEnd: ev => { // 作一些拖拽結束的清理工做 } }
stopPropagation
的做用在原文簡介中已經介紹過了,setData
則是通知拖拽方,當前拖拽的組件 id 是什麼,這是因爲拖拽由 drag
發起而由 drop
響應,所以必須有個數據傳輸過程,而 dataTransfer
就最適合作這件事。
對於 drop
來講,只要實現 onDragOver
與 onDrop
便可:
const dropProps = { onDropOver: ev => { // 作一些樣式處理,提示用戶此時鬆手會將元素防止在何處 }, onDrop: ev => { ev.stopPropagation() const componentId = ev.dataTransfer.getData('componentId') // 經過 componentId 修改數據,經過 React Rerender 刷新 UI } }
重點在 onDrop
,它是實現拖拽效果的 「真正執行處」,最終經過修改 UI 的方式更新數據。
存在一種場景,一個容器既能夠被拖動,也能夠被拖入,這種狀況通常這個組件是個容器,但這個容器能夠被拖入到其餘容器中,能夠自由嵌套。
實現這種場景的方式就是將 DragContainer
與 DropContainer
做用到一個組件上:
const Box = ( <DragContainer> {({ dragProps }) => ( <DropContainer> {({ dropProps }) => { <div {...dragProps} {...dropProps} /> }} </DropContainer> )} </DragContainer> )
之因此能嵌套,在於 HTML5 的 API 容許一個元素同時擁有 onDragStart
、onDrop
這兩種屬性,而上面的語法不過是同時將這兩種屬性傳給組件 DOM。
因此,動手實現一個拖拽庫就是這麼簡單,只要活用 HTML5 的拖拽 API,結合 React 一些特殊語法便夠了。
最後留下一個思考題,許多具備拖拽功能的系統都具有 「拖拽 placeholder」 的功能,即拖拽元素的過程當中,在其 「落點」 位置展現一條橫線或豎線,引導出鬆手後元素位置落點,如圖所示:
那麼這條輔助線是經過什麼方式實現的呢?歡迎在評論區留言!若是你有輔助線實現方案解析的文章,歡迎分享,也能夠期待筆者將來專門寫一篇 「拖拽 placeholder」 實現剖析的精讀。
討論地址是: 精讀《手寫 JSON Parser》 · Issue #233 · dt-fe/weekly
若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公衆號
版權聲明:自由轉載-非商用-非衍生-保持署名( 創意共享 3.0 許可證)