vant组件实现上传图片裁剪_如何用 120 行代码,实现交互完整的拖拽上传组件?...
作者 | 前端勸退師
責編 | 伍杏玲
你將在該篇學到:
如何將現(xiàn)有組件改寫為 React Hooks函數(shù)組件
useState、useEffect、useRef是如何替代原生命周期和Ref的。
一個完整拖拽上傳行為覆蓋的四個事件:dragover、dragenter、drop、dragleave
如何使用React Hooks編寫自己的UI組件庫。
逛國外社區(qū)時看到這篇:
How To Implement Drag and Drop for Files in React
文章講了React拖拽上傳的精簡實現(xiàn),但直接翻譯照搬顯然不是我的風格。
于是我又用React Hooks 重寫了一版,除CSS的代碼總數(shù) 120行。
效果如下:
添加基本目錄骨架
app.js
import?React?from?'react';import?PropTypes?from?'prop-types';import?{?FilesDragAndDrop?}?from?'../components/Common/FilesDragAndDropHook';export?default?class?App?extends?React.Component?{static?propTypes?=?{};
????onUpload?=?(files)?=>?{console.log(files);
????};
????render()?{return?(<div><FilesDragAndDroponUpload={this.onUpload}
????????????????/>div>
????????);
????}
}FilesDragAndDrop.js(非Hooks):
import?React?from?'react';import?PropTypes?from?'prop-types';import?'../../scss/components/Common/FilesDragAndDrop.scss';export?default?class?FilesDragAndDrop?extends?React.Component?{static?propTypes?=?{onUpload:?PropTypes.func.isRequired,
????};
????render()?{return?(<div?className='FilesDragAndDrop__area'>
????????????????傳下文件試試?<spanrole='img'aria-label='emoji'className='area__icon'
????????????????>
????????????????????😎span>div>
????????);
????}
}1. 如何改寫為 Hooks 組件?
請看動圖:
2. 改寫組件Hooks版組件屬于函數(shù)組件,將以上改造:
import?React,?{?useEffect,?useState,?useRef?}?from?"react";import?PropTypes?from?'prop-types';import?classNames?from?'classnames';import?classList?from?'../../scss/components/Common/FilesDragAndDrop.scss';
const?FilesDragAndDrop?=?(props)?=>?{return?('FilesDragAndDrop__area'>
????????????傳下文件試試?????????????????role='img'
????????????????aria-label='emoji'
????????????????className='area__icon'
????????????>
????????????????😎
????);
}
FilesDragAndDrop.propTypes?=?{
????onUpload:?PropTypes.func.isRequired,
????children:?PropTypes.node.isRequired,
????count:?PropTypes.number,
????formats:?PropTypes.arrayOf(PropTypes.string)
}export?{?FilesDragAndDrop?};
FilesDragAndDrop.scss
.FilesDragAndDrop?{
??.FilesDragAndDrop__area?{
????width:?300px;
????height:?200px;
????padding:?50px;
????display:?flex;
????align-items:?center;
????justify-content:?center;
????flex-flow:?column?nowrap;
????font-size:?24px;
????color:?#555555;
????border:?2px?#c3c3c3?dashed;
????border-radius:?12px;
????.area__icon?{
??????font-size:?64px;
??????margin-top:?20px;
????}
??}
}然后就可以看到頁面:實現(xiàn)分析
從操作DOM、組件復用、事件觸發(fā)、阻止默認行為、以及Hooks應用方面分析。
1. 操作DOM:`useRef`
由于需要拖拽文件上傳以及操作組件實例,需要用到ref屬性。
React Hooks中 新增了useRef API
語法
const?refContainer?=?useRef(initialValue);- useRef 返回一個可變的 ref 對象
其 .current 屬性被初始化為傳遞的參數(shù)(initialValue)
返回的對象將存留在整個組件的生命周期中。
...const?drop?=?useRef();return?(<divref={drop}className='FilesDragAndDrop'
????/>
????...
????)
2. 事件觸發(fā)
完成具有動態(tài)交互的拖拽行為并不簡單,需要用到四個事件控制:
區(qū)域外:dragleave,離開范圍
區(qū)域內(nèi):dragenter,用來確定放置目標是否接受放置。
區(qū)域內(nèi)移動:dragover,用來確定給用戶顯示怎樣的反饋信息
完成拖拽(落下):drop,允許放置對象。
這四個事件并存,才能阻止 Web 瀏覽器默認行為和形成反饋。
3. 阻止默認行為
代碼很簡單:
e.preventDefault()?//阻止事件的默認行為(如在瀏覽器打開文件)
e.stopPropagation()?//?阻止事件冒泡每個事件階段都需要阻止,為啥呢?舉個?栗子:const?handleDragOver?=?(e)?=>?{//?e.preventDefault();//?e.stopPropagation();
};4. 組件內(nèi)部狀態(tài): useState
拖拽上傳組件,除了基礎的拖拽狀態(tài)控制,還應有成功上傳文件或未通過驗證時的消息提醒。
狀態(tài)組成應為:
state?=?{
????dragging:?false,
????message:?{
????????show:?false,
????????text:?null,
????????type:?null,
????},
};寫成對應useState前先回歸下寫法:const?[屬性,?操作屬性的方法]?=?useState(默認值);于是便成了:const?[dragging,?setDragging]?=?useState(false);const?[message,?setMessage]?=?useState({?show:?false,?text:?null,?type:?null?});5. 需要第二個疊加層
除了drop事件,另外三個事件都是動態(tài)變化的,而在拖動元素時,每隔 350 毫秒會觸發(fā) dragover事件。
此時就需要第二ref來統(tǒng)一控制。
所以全部的ref為:
const?drop?=?useRef();?//?落下層const?drag?=?useRef();?//?拖拽活動層6. 文件類型、數(shù)量控制
我們在應用組件時,prop需要傳入類型和數(shù)量來控制
<FilesDragAndDroponUpload={this.onUpload}count={1}formats={['jpg',?'png']}
><div?className={classList['FilesDragAndDrop__area']}>
????????傳下文件試試?<spanrole='img'aria-label='emoji'className={classList['area__icon']}
????????>
????????????😎span>div>FilesDragAndDrop>- onUpload:拖拽完成處理事件
count: 數(shù)量控制
formats: 文件類型。
對應的組件Drop內(nèi)部事件:handleDrop:
const?handleDrop?=?(e)?=>?{
????e.preventDefault();
????e.stopPropagation();
????setDragging(false)const?{?count,?formats?}?=?props;const?files?=?[...e.dataTransfer.files];if?(count?&&?count?????????showMessage(`抱歉,每次最多只能上傳${count}?文件。`,?'error',?2000);return;
????}if?(formats?&&?files.some((file)?=>?!formats.some((format)?=>?file.name.toLowerCase().endsWith(format.toLowerCase()))))?{
????????showMessage(`只允許上傳?${formats.join(',?')}格式的文件`,?'error',?2000);return;
????}if?(files?&&?files.length)?{
????????showMessage('成功上傳!',?'success',?1000);
????????props.onUpload(files);
????}
};.endsWith是判斷字符串結尾,如:"abcd".endsWith("cd"); ? // true
showMessage則是控制顯示文本:
const?showMessage?=?(text,?type,?timeout)?=>?{
????setMessage({?show:?true,?text,?type,?})
????setTimeout(()?=>
????????setMessage({?show:?false,?text:?null,?type:?null,?},),?timeout);
};需要觸發(fā)定時器來回到初始狀態(tài)
7. 事件在生命周期里的觸發(fā)與銷毀
原本EventListener的事件需要在componentDidMount添加,在componentWillUnmount中銷毀:
componentDidMount?()?{this.drop.addEventListener('dragover',?this.handleDragOver);
}
componentWillUnmount?()?{this.drop.removeEventListener('dragover',?this.handleDragOver);
}但Hooks中有內(nèi)部操作方法和對應useEffect來取代上述兩個生命周期useEffect示例:
useEffect(()?=>?{document.title?=?`You?clicked?${count}?times`;
},?[count]);?//?僅在?count?更改時更新而 每個effect都可以返回一個清除函數(shù)。如此可以將添加(componentDidMount)和移除(componentWillUnmount) 訂閱的邏輯放在一起。于是上述就可以寫成:
useEffect(()?=>?{
????drop.current.addEventListener('dragover',?handleDragOver);return?()?=>?{
????????drop.current.removeEventListener('dragover',?handleDragOver);
????}
})這也太香了吧!!!完整代碼
FilesDragAndDropHook.js:
import?React,?{?useEffect,?useState,?useRef?}?from?"react";import?PropTypes?from?'prop-types';import?classNames?from?'classnames';import?classList?from?'../../scss/components/Common/FilesDragAndDrop.scss';const?FilesDragAndDrop?=?(props)?=>?{const?[dragging,?setDragging]?=?useState(false);const?[message,?setMessage]?=?useState({?show:?false,?text:?null,?type:?null?});const?drop?=?useRef();const?drag?=?useRef();
????useEffect(()?=>?{//?useRef?的?drop.current?取代了?ref?的?this.drop
????????drop.current.addEventListener('dragover',?handleDragOver);
????????drop.current.addEventListener('drop',?handleDrop);
????????drop.current.addEventListener('dragenter',?handleDragEnter);
????????drop.current.addEventListener('dragleave',?handleDragLeave);return?()?=>?{
????????????drop.current.removeEventListener('dragover',?handleDragOver);
????????????drop.current.removeEventListener('drop',?handleDrop);
????????????drop.current.removeEventListener('dragenter',?handleDragEnter);
????????????drop.current.removeEventListener('dragleave',?handleDragLeave);
????????}
????})const?handleDragOver?=?(e)?=>?{
????????e.preventDefault();
????????e.stopPropagation();
????};const?handleDrop?=?(e)?=>?{
????????e.preventDefault();
????????e.stopPropagation();
????????setDragging(false)const?{?count,?formats?}?=?props;const?files?=?[...e.dataTransfer.files];if?(count?&&?count?????????????showMessage(`抱歉,每次最多只能上傳${count}?文件。`,?'error',?2000);return;
????????}if?(formats?&&?files.some((file)?=>?!formats.some((format)?=>?file.name.toLowerCase().endsWith(format.toLowerCase()))))?{
????????????showMessage(`只允許上傳?${formats.join(',?')}格式的文件`,?'error',?2000);return;
????????}if?(files?&&?files.length)?{
????????????showMessage('成功上傳!',?'success',?1000);
????????????props.onUpload(files);
????????}
????};const?handleDragEnter?=?(e)?=>?{
????????e.preventDefault();
????????e.stopPropagation();
????????e.target?!==?drag.current?&&?setDragging(true)
????};const?handleDragLeave?=?(e)?=>?{
????????e.preventDefault();
????????e.stopPropagation();
????????e.target?===?drag.current?&&?setDragging(false)
????};const?showMessage?=?(text,?type,?timeout)?=>?{
????????setMessage({?show:?true,?text,?type,?})
????????setTimeout(()?=>
????????????setMessage({?show:?false,?text:?null,?type:?null,?},),?timeout);
????};return?(<divref={drop}className={classList['FilesDragAndDrop']}
????????>
????????????{message.show?&&?(<divclassName={classNames(classList['FilesDragAndDrop__placeholder'],classList[`FilesDragAndDrop__placeholder--${message.type}`],
????????????????????)}
????????????????>
????????????????????{message.text}<spanrole='img'aria-label='emoji'className={classList['area__icon']}
????????????????????>
????????????????????????{message.type?===?'error'???<>😢>?:?<>😘>}span>div>
????????????)}
????????????{dragging?&&?(<divref={drag}className={classList['FilesDragAndDrop__placeholder']}
????????????????>
????????????????????請放手<spanrole='img'aria-label='emoji'className={classList['area__icon']}
????????????????????>
????????????????????????😝span>div>
????????????)}
????????????{props.children}div>
????);
}
FilesDragAndDrop.propTypes?=?{onUpload:?PropTypes.func.isRequired,children:?PropTypes.node.isRequired,count:?PropTypes.number,formats:?PropTypes.arrayOf(PropTypes.string)
}export?{?FilesDragAndDrop?};App.js:import?React,?{?Component?}?from?'react';import?{?FilesDragAndDrop?}?from?'../components/Common/FilesDragAndDropHook';import?classList?from?'../scss/components/Common/FilesDragAndDrop.scss';export?default?class?App?extends?Component?{
????onUpload?=?(files)?=>?{console.log(files);
????};
????render?()?{return?(<FilesDragAndDroponUpload={this.onUpload}count={1}formats={['jpg',?'png',?'gif']}
????????????><div?className={classList['FilesDragAndDrop__area']}>
????????????????????傳下文件試試?<spanrole='img'aria-label='emoji'className={classList['area__icon']}
????????????????????>
????????????????????????😎span>div>FilesDragAndDrop>
????????)
????}
}FilesDragAndDrop.scss:.FilesDragAndDrop?{
??position:?relative;
??.FilesDragAndDrop__placeholder?{
????position:?absolute;
????top:?0;
????left:?0;
????right:?0;
????bottom:?0;
????width:?100%;
????height:?100%;
????z-index:?9999;
????display:?flex;
????align-items:?center;
????justify-content:?center;
????flex-flow:?column?nowrap;
????background-color:?#e7e7e7;
????border-radius:?12px;
????color:?#7f8e99;
????font-size:?24px;
????opacity:?1;
????text-align:?center;
????line-height:?1.4;
????&.FilesDragAndDrop__placeholder--error?{
??????background-color:?#f7e7e7;
??????color:?#cf8e99;
????}
????&.FilesDragAndDrop__placeholder--success?{
??????background-color:?#e7f7e7;
??????color:?#8ecf99;
????}
????.area__icon?{
??????font-size:?64px;
??????margin-top:?20px;
????}
??}
}
.FilesDragAndDrop__area?{
??width:?300px;
??height:?200px;
??padding:?50px;
??display:?flex;
??align-items:?center;
??justify-content:?center;
??flex-flow:?column?nowrap;
??font-size:?24px;
??color:?#555555;
??border:?2px?#c3c3c3?dashed;
??border-radius:?12px;
??.area__icon?{
????font-size:?64px;
????margin-top:?20px;
??}
}然后你就可以拿到文件慢慢耍了。?30 歲的程序員,我沒有活成理想的模樣,失敗嗎?
?華為方舟編譯器開源!前華為人重磅解讀!
?漫畫:關于 Context 不得不說的細節(jié)
?2019 編程語言排行榜:Java、Python 龍爭虎斗!PHP 屹立不倒!
?看懂“大數(shù)據(jù)”,這一篇就夠了!?只給測試集不給訓練集,要怎么做自己的物體檢測器??倒計時3天 | 專屬AI技術人的盛會,為你而來!?以太坊新生合約總數(shù)驟減; 比特幣大跌, 本周主鏈排名震蕩 | 數(shù)據(jù)周榜?她說:行!沒事別嫁程序員!你點的每個“在看”,我都認真當成了喜歡
總結
以上是生活随笔為你收集整理的vant组件实现上传图片裁剪_如何用 120 行代码,实现交互完整的拖拽上传组件?...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 值多少??
- 下一篇: python实时读取日志并打印关键字怎么