我們學(xué)習(xí)了 HTML 提供的原生拖放(drag & drop)后,是時候想一想這個東西可以用來作什么,可以在什么時候使用,使用的場景等等
場景分析
當(dāng)我們在注冊成功一個賬戶時,一般網(wǎng)站會讓我們上傳我們的用戶頭像,或者在實名認(rèn)證的時候會涉及到身份證圖片上傳到等,這時候我們可以使用input提供的file屬性進行選擇本地文件進行上傳。
我們再想一下,當(dāng)在電腦端的情況下,當(dāng)用戶打開文件選擇框時再尋找圖片對應(yīng)的文件夾,再進行選取文件的時候是不是會有點麻煩呢?我們可不可以讓用戶找到圖片文件,直接引入實現(xiàn)上傳呢?答案是可以的。
怎么做
經(jīng)過這些分析后,我們可以嘗試使用 HTML5 提供的拖拽,使得目標(biāo)元素增加讀取文件功能,然后使用 ajax 實現(xiàn)圖片上傳。
談一談我們需要使用到的技術(shù):
HTML5 拖拽事件
關(guān)于 Drag & Drop 拖拽事件,之前我寫過一篇專門介紹的文章,HTML5-拖拽,大家有興趣的話可以點擊鏈接查看,我在這里就不在多啰嗦了~下面直接出拖拽上傳的簡要代碼示例
var oDragWrap=document.body; //拖進 oDragWrap.addEventListener( "dragenter", function(e) { e.preventDefault(); }, false ); //拖離 oDragWrap.addEventListener( "dragleave", function(e) { dragleaveHandler(e); }, false ); //拖來拖去 , 一定要注意dragover事件一定要清除默認(rèn)事件 //不然會無法觸發(fā)后面的drop事件 oDragWrap.addEventListener( "dragover", function(e) { e.preventDefault(); }, false ); //扔 oDragWrap.addEventListener( "drop", function(e) { dropHandler(e); }, false ); var dropHandler=function(e) { //將本地圖片拖拽到頁面中后要進行的處理都在這 };
獲取文件數(shù)據(jù) HTML5 File API
File API 中的 FileReader 接口,作為 File API 的一部分,F(xiàn)ileReader 專門用來讀取文件。我們在這里主要介紹一些 File API 中的 FileList 接口,它主要通過兩個途徑獲取本地文件列表,一是<input type="file"/>的表單形式,另一種則是e.dataTransfer.files拖拽事件傳遞的文件信息。
var fileList=e.dataTransfer.files;
使用 files 方法將會獲取到拖拽文件的數(shù)組形式的數(shù)據(jù),每個文件占用一個數(shù)組的索引,如果索引不存在文件數(shù)據(jù),將返回 Null。可以通過length屬性獲取文件的數(shù)量。
var fileNum=fileList.length;
拖拽上傳需要注意的是需要判斷兩個條件
// 檢測是否是拖拽文件到頁面的操作 if (fileList.length===0) { return; } // 檢測文件是不是圖片 if (fileList[0].type.indexOf("image")===-1) { return; }
下面我們看看結(jié)合之前的拖拽事件,來實現(xiàn)拖拽圖片并在頁面中預(yù)覽
var dropHandler=function(e) { e.preventDefault(); //獲取文件列表 var fileList=e.dataTransfer.files; //檢測是否是拖拽文件到頁面的操作 if (fileList.length==0) { return; } //檢測文件是不是圖片 if (fileList[0].type.indexOf("image")===-1) { return; } //實例化file reader對象 var reader=new FileReader(); var img=document.createElement("img"); reader.onload=function(e) { img.src=this.result; oDragWrap.appendChild(img); }; reader.readAsDataURL(fileList[0]); };
當(dāng)完成以上操作后,相信你可以成功的完成了拖拽圖片預(yù)覽的操作。當(dāng)你查看 img 標(biāo)簽時會發(fā)現(xiàn),img的src屬性是一個超長的文件二進制數(shù)據(jù),當(dāng)你需要很多這種的img元素時,建議將展示區(qū)域脫離文檔流,讓其絕對定位減少頁面的 reflow
AJAX 上傳圖片
既然已經(jīng)獲取到拖拽到web頁面中的圖片數(shù)據(jù)了,下一步就是將其發(fā)送到服務(wù)器端。
總結(jié)
言
最近沒有更新文章,因為去字節(jié)實習(xí)了一陣,實在是沒有精力寫東西,所以就咕咕咕了。現(xiàn)在回學(xué)校了,就可以繼續(xù)更新啦,因為在字節(jié)做的業(yè)務(wù)和圖可視化還有拖拽關(guān)系比較大,所以這次就寫下拖拽相關(guān)的內(nèi)容。
HTML5 Drag and Drop 接口
html5中提供了一系列Drag and Drop 接口,主要包括四部分:DragEvent,DataTansfer,DataTransferItem 和DataTransferItemList。
DragEvent
源元素和目標(biāo)元素
image-20220314095928431.png
**源元素:**即被拖拽的元素。
**目標(biāo)元素:**即合法的可釋放元素。
每個事件的事件主體都是兩者之一。
拖拽事件
事件事件處理程序事件主體觸發(fā)時機dragstartondragstart源元素當(dāng)源元素開始被拖拽。dragondrag源元素當(dāng)源元素被拖拽(持續(xù)觸發(fā))。dragendondragend源元素當(dāng)源元素拖拽結(jié)束(鼠標(biāo)釋放或按下esc鍵)dragenterondragenter目標(biāo)元素當(dāng)被拖拽元素進入該元素。dragoverondragover目標(biāo)元素當(dāng)被拖拽元素停留在該元素(持續(xù)觸發(fā))。dragleaveondragleave目標(biāo)元素當(dāng)被拖拽元素離開該元素。dropondrop目標(biāo)元素當(dāng)拖拽事件在合法的目標(biāo)元素上釋放。
觸發(fā)順序及次數(shù)
我們綁定相關(guān)的事件,拖放一次來查看相關(guān)事件的觸發(fā)情況。
op.gif
我們讓相應(yīng)事件處理程序打印事件名稱及事件觸發(fā)的主體是誰,下面截取部分展示。
image-20220314103603324-16473183157994.png
我們可以看到對于被拖拽元素,事件觸發(fā)順序是 dragstart->drag->dragend;對于目標(biāo)元素,事件觸發(fā)的順序是 dragenter->dragover->drop/dropleave。
其中drag和dragover會分別在源元素和目標(biāo)元素反復(fù)觸發(fā)。整個流程一定是dragstart第一個觸發(fā),dragend最后一個觸發(fā)。
這里還有一個注意的點,如果某個元素同時設(shè)置了dragover和drop的監(jiān)聽,那么必須阻止dragover的默認(rèn)行為,否則drop將不會被觸發(fā)。
image-20220314111402189.png
DataTansfer
我們先用一張圖來直觀的感受一下:
image-20220315122157204-16473183290157.png
我們可以看到,DataTransfer如同它的名字,作用就是在拖放過程中對數(shù)據(jù)進行傳輸,其中setData用來存放數(shù)據(jù),getData用來獲取數(shù)據(jù),出于安全的考量,數(shù)據(jù)只能在drop時獲取,而effectAllowed和dropEffect則影響鼠標(biāo)展示的樣式,下面我們用一個例子來進行展示:
sourceElem.addEventListener('dragstart', (event) => {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', '放進來了');
});
targetElem.addEventListener('dragover', (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
});
targetElem.addEventListener('drop', (event) => {
event.target.innerHTML = event.dataTransfer.getData('text/plain');
});
復(fù)制代碼
drag2.gif
可以看到在藍(lán)色方塊設(shè)置的數(shù)據(jù)被成功取得了。
DataTransferItemList
屬性
length: 列表中拖動項的數(shù)量。
方法
add(): 向拖動項列表中添加新項 (File對象或String),該方法返回一個 DataTransferItem) 對象。
remove(): 根據(jù)索引刪除拖動項列表中的對象。
clear(): 清空拖動項列表。
DataTransferItem(): 取值方法:返回給定下標(biāo)的DataTransferItem對象.
DataTransferItem
屬性
kind: 拖拽項的種類,string 或是 file。
type: 拖拽項的類型,一般是一個MIME 類型。
方法
getAsString: 使用拖拽項的字符串作為參數(shù)執(zhí)行指定回調(diào)函數(shù)。
getAsFile: 返回一個關(guān)聯(lián)拖拽項的 File 對象 (當(dāng)拖拽項不是一個文件時返回 null)。
實踐
學(xué)習(xí)了上面的基礎(chǔ)知識,我們從幾個常見的應(yīng)用場景入手,來實踐上面的知識
可放置組件
知道上面幾個事件后,我們來完成一個簡單可放置組件,為了方便大家理解,這里不使用任何框架,以免增加不會框架同學(xué)的學(xué)習(xí)成本。
想讓組件可拖行,那么就要可以改變它的位置,有兩種思路:
我推薦第二種,首先translate是基于本身的移動,因此自身的坐標(biāo)就作為原點(0,0),但是第一種,元素本身的top/left等可能并不為0,計算起來比較復(fù)雜。其次,第一種是通過cpu去計算,而第二種是通過gpu去計算,并且會提升到一個新的層,這樣做非常有利于頁面的性能。原因是 Chrome 這樣將 DOM 轉(zhuǎn)變成一個屏幕圖像:
但更新的幀可以走捷徑,不必經(jīng)歷所有過程:
如果某些特定 CSS 屬性變化,并不需要發(fā)生重繪。Chrome 可以使用早已作為紋理而存在于 GPU 中的層來重新復(fù)合,但會使用不同的復(fù)合屬性(例如,出現(xiàn)在不同的位置,擁有不同的透明度等等)。
如果圖層中某個元素需要重繪,那么整個圖層都需要重繪 。所以提升為一個新的層,可以減少重繪的次數(shù)。因為只改變位置,所以可以復(fù)用紋理,提高性能。
更詳細(xì)的可以看我的另一篇文章:瀏覽器事件循環(huán)與渲染機制 \- 掘金 \(juejin.cn\)[1]
有了思路那么我們就開始吧!
首先我們要知道這次拖拽的向量是怎樣的,因為DragEvent繼承自MouseEvent ,所以我們可以通過MouseEvent接口的offsetX屬性和offsetY屬性獲取鼠標(biāo)現(xiàn)在相對于該物體的位置差。而transform設(shè)置多個屬性值,效果就可以疊加,所以我們要獲得之前的移動效果,再加上現(xiàn)在的移動效果即可,之前的移動效果可以通過window.getComputedStyle(e.target).transform獲得。
sourceElem.addEventListener('dragend', (e) => {
const startPosition = window.getComputedStyle(e.target).transform;
e.target.style.transform = `${startPosition} translate(${e.offsetX}px, ${e.offsetY}px)`;
}, true);
復(fù)制代碼
我們給要拖拽的元素加上這段處理程序似乎就大功告成了。
wrong.gif
但是實際使用時,這個元素并沒有停在預(yù)覽的位置,而是左上角移動到鼠標(biāo)的位置,顯然不符合預(yù)期,相信大家都能猜到,我們少考慮了鼠標(biāo)在元素的位置,而鼠標(biāo)初始的位置同樣可以通過MouseEvent接口的offsetX屬性和offsetY屬性獲取(dragstart)。改善如下:
function enableDrag(element) {
let mouseDiff = null;
element.addEventListener('dragstart', (e) => {
//初始時鼠標(biāo)與元素的位置差
mouseDiff = `translate(${-e.offsetX}px, ${-e.offsetY}px)`
}, true);
element.addEventListener('dragend', (e) => {
//開始時元素的位置
const startPosition = window.getComputedStyle(e.target).transform;
//鼠標(biāo)移動的位置
const mouseMove = `translate(${e.offsetX}px, ${e.offsetY}px)`;
e.target.style.transform = `${mouseDiff} ${startPosition} ${mouseMove}`;
}, true);
}
enableDrag(souceElement);
復(fù)制代碼
drag.gif
圖的連線
節(jié)點使用DOM渲染,連線我們使用SVG來渲染,框架使用React,但除了state盡量使用較少的框架相關(guān)的,以防非React技術(shù)棧的同學(xué)看不懂。
首先我們要先組織我們的state,作為一個圖,顯然應(yīng)該由nodes和edges兩部分組成,我們都使用數(shù)組存儲,我們給每個node一個唯一的id,使用Map去映射id與對應(yīng)的positon 形如 [x,y]的關(guān)系,而edge有源端與終端的id,通過id去獲得對應(yīng)的坐標(biāo)。
我們先假設(shè)節(jié)點可以完成所有功能了,只考慮連線,可以定義如下的組件
const Edge = ({nodes:[sourceNode,targetNode]}) =>(
<svg key={sourceNode.id + targetNode.id||''}
style={{position:'absolute',
overflow:'visible',
zIndex:'-1',
transform:'translate(15px,15px)'}}>
<path d={`M ${sourceNode.position[0]} ${sourceNode.position[1]}
C
${(targetNode.position[0] + sourceNode.position[0])/2} ${sourceNode.position[1]}
${(targetNode.position[0] + sourceNode.position[0])/2} ${targetNode.position[1]}
${targetNode.position[0]} ${targetNode.position[1]} `}
strokeWidth={6}
stroke={'red'}
fill='none'
></path>
</svg>
)
復(fù)制代碼
首先我們應(yīng)該從什么時候生成一個連線呢,顯然是dragstart,但這時還沒有對應(yīng)的終端,因此不應(yīng)該通過加入edges來循環(huán)渲染,而是單獨渲染一個出來。在dragstart我們設(shè)置一個虛擬節(jié)點temNode,并記錄開始節(jié)點的id。
并如果有temNode則渲染一條預(yù)覽的edge。
temNode && (<Edge nodes = {[sourceNode,temNode]}></Edge>)
復(fù)制代碼
drag3.gif
然后加入這條edge后,我們刪除虛擬節(jié)點,變?yōu)檠h(huán)渲染來展示所有的邊。
edges.map(([sourceId,targetId])=>{
const sourceNode = getNode(sourceId);
const targetNode = getNode(targetId);
return (
<Edge nodes = {[sourceNode,targetNode]}></Edge>);
})
復(fù)制代碼
那么我們只考慮邊的展示了,節(jié)點的功能應(yīng)該如何完善呢?
首先是dragstart,我們要設(shè)置起始節(jié)點和虛擬節(jié)點
onDragStart={()=>{
setStartNodeId(uid);
setTemNode({position:[x,y]})
}}
復(fù)制代碼
然后既然邊可以跟著動,我們必然要在drag中動態(tài)的改變虛擬節(jié)點的位置
onDrag={(event)=>{
position=[x+event.nativeEvent.offsetX,y+event.nativeEvent.offsetY];
setTemNode({position})
}}
復(fù)制代碼
然后drop時,我們加入一條新的邊
onDrop={
(event)=>{
event.preventDefault();
setEdges(edges.concat([[startNodeId,uid]]))
}
}
復(fù)制代碼
最重要的是,不論在哪里事件結(jié)束了,要刪除虛擬節(jié)點
onDragEnd={
()=>{
setTemNode(null);
}
}
復(fù)制代碼
下面是最終成果:
drag4.gif
參考
關(guān)于本文
https://juejin.cn/post/7075918201359433758
開源精選》是我們分享Github、Gitee等開源社區(qū)中優(yōu)質(zhì)項目的欄目,包括技術(shù)、學(xué)習(xí)、實用與各種有趣的內(nèi)容。本期推薦的是 AJ-Report是一個完全開源,拖拽編輯的可視化設(shè)計工具。三步快速完成大屏:配置數(shù)據(jù)源---->寫SQL配置數(shù)據(jù)集---->拖拽生成大屏。讓管理層隨時隨地掌控業(yè)務(wù)動態(tài),讓每個決策都有數(shù)據(jù)支撐。
數(shù)據(jù)流程圖
打包目錄
bin 啟動命令腳本
│ ├── restart.sh
│ ├── start.bat
│ ├── start.sh
│ └── stop.sh
├── conf 配置文件目錄
│ └── bootstrap-dev.yml
├── logs 啟動日志目錄
├── cache 本地緩存目錄
├── lib 自定義擴展包&report-core核心包
系統(tǒng)目錄
├── doc 文檔源碼
│ ├── docs
│ ├── package.json
│ └── README.md
├── pom.xml 父pom,jar版本管理
├── report-core java源碼
│ ├── pom.xml gaea父pom,jar版本管理
│ └── README.md
├── report-ui 前端vue源碼
├── LICENSE
├── README.md
語言
后端
前端
新增用戶
用戶授權(quán)
注:這里沒有給新用戶賦予默認(rèn)角色的原因是,在角色管理中角色是可以被刪除和修改的,因此在新建用戶時需要手動的去授權(quán)角色
角色管理
表格報表
文檔:https://report.anji-plus.com/report-doc/
*請認(rèn)真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。