H5拖放非同步檔案上傳之二——上傳進度監聽

lightwing發表於2021-09-09

 上一篇,我們走通了拖放檔案上傳的整個流程,但離實際使用場景還有差距。這篇,我們來新增幾個實際使用場景必要的功能,向實際使用再走一步。

新增功能

  1. 顯示待上傳檔案列表;

  2. 支援移除待上傳檔案;

  3. 使用upload.onprogress顯示上傳進度;

  4. 支援中斷上傳;

upload.progress

XMLHttpRequest.upload方法返回一個 XMLHttpRequestUpload物件,用來表示上傳的進度。這個物件是不透明的,但是作為一個XMLHttpRequestEventTarget,可以透過對其繫結事件來追蹤它的進度。onprogres監聽資料傳輸進行中(透過監聽這個事件,可獲得上傳進度)。

實現思路

  拖放檔案到上傳區域時,將檔案儲存在一個檔案陣列中,新增並顯示檔案相關資訊和移除按鈕,點選移除按鈕刪除檔案陣列中對應的檔案元素,點選上傳按鈕,遍歷檔案陣列,開始上傳待上傳檔案,此時點選移除按鈕則中止檔案上傳。

程式碼

//相關樣式.drop-area{
    margin:auto;
    width: 500px;
    height: 500px;
    border:1px pink dashed;
}
.close-btn{
    cursor: pointer;
}
.close-btn:after{
    float: right;
    content: 'X';
    color: red;
}#fileList{
    width: 95%;
}
.process-bar{
    position: relative;
    margin: 0 10px 0 10px;
    width: 198px;
    height: 18px;
    display: none;
    text-align: center;
    line-height: 20px;
    color: #6dbfff;
    border-radius: 3px;
    border: 1px solid #1483d8;    
    background: #fff;}
.success .process-bar,
.uploading .process-bar{
    display: inline-block;
}
.process-bar .process-text{
    position: relative;
    z-index: 1;
}
.process-bar .process-rate{
    position: absolute;
    width: 0;
    height: 100%;
    left: 0;
    top: 0;
    border-radius: 3px;
    background: #1483d8;}
.file-list .success .process-text,
.file-list .success .close-btn:after,
.file-list .error .process-text,
.file-list .error .close-btn:after{
    display: none;
}
 .success .process-bar :after,
.error .process-bar :after{
    content:'success';
    position: absolute;    
    margin: auto;
    left: 0;
    right: 0;
    z-index: 2;
}
.error .process-bar:after{
    content: "error";
    color: red;
}
<!--HTML--&gt
<!--監聽拖放DOM--&gt
<!--顯示待上傳檔案列表--&gt<!--上傳按鈕--&gt
//Javascript程式碼let filesSet = []; //檔案儲存陣列let fileList = document.getElementById('fileList'); //獲取顯示檔案列表DOM/**
 * 建立一個新的空白的文件片段frag
 * 用於附加多個待上傳檔案的DOM,可減小回流
 * https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createDocumentFragment
 */let frag = document.createDocumentFragment();let barDom = createProccessBar(); //建立進度條DOMlet submit = document.getElementById("submit") //獲取提交按鈕/* 拖動到放置區域時 */function dragover_hander (event) {    /* 必須同時阻止dragover和drop的預設事件
       否則會響應瀏覽器預設行為
       瀏覽器器能顯示的檔案會直接顯示,例如html檔案、圖片檔案
       瀏覽器器不能顯示的檔案會出現檔案下載彈窗
    */
    event.preventDefault(); 
}/*拖放完成事件*/function drop_hander (event) {

    event.preventDefault(); //阻止預設事件

    var files = event.dataTransfer.files; //透過dataTransfer物件獲取檔案物件陣列

    for(let file of files) { //遍歷檔案物件陣列

       //建立檔案資訊顯示DOM,並儲存在file物件的element屬性中,用於後續操作
       file.element = createFileDom(file, filesSet.length)       //複製進度條DOM,並儲存在file物件的processBar屬性中,用於後續操作
       file.processBar = filesSet.length?barDom.cloneNode(true):barDom;       //將進度條新增至檔案資訊顯示DOM中
       file.element.appendChild(file.processBar);       //file檔案物件新增到檔案儲存陣列
       filesSet.push(file);       //建立檔案資訊顯示DOM新增至文件片段frag
       frag.appendChild(file.element);
    }    //將檔案列表新增至顯示檔案列表的div
    fileList.appendChild(frag)
}/**
 * 建立檔案資訊顯示DOM
 * file  檔案物件,用於獲取檔案資訊
 * index 檔案物件在陣列中的索引,用於刪除
 */function createFileDom (file, index) {    let p = document.createElement('p'); 
    //file.name屬性可以獲得檔名稱
    //有興趣的童鞋,可以使用for...in迴圈檢視一下file物件的其它屬性值
    let text = document.createTextNode(file.name); 
    let span = document.createElement("span");
    span.setAttribute('index', index); //索引新增至按鈕的index屬性
    span.className = 'close-btn'; 
    p.appendChild(text);
    p.appendChild(span);    return p; //返回檔案資訊顯示DOM}/**
 * 建立進度條DOM,
 * 進度條DOM結構固定,可使用clone(true)進行復制
 * @return {[type]} [description]
 */function createProccessBar() {    let bar =  document.createElement("span");    let rate = document.createElement("span");    let text = document.createElement("span");
    bar.className = "process-bar";
    rate.className = "process-rate";
    text.className = "process-text";
    text.innerText="0%";
    bar.appendChild(text);
    bar.appendChild(rate);    return bar; 
} 

//透過事件代理,監聽移除或中止上傳fileList.addEventListener('click', (evt)=>{    let index = evt.target.getAttribute('index'); //獲得index屬性值
    if (index) { //存在index屬性值,表示點選了刪除按鈕

        if (filesSet[index].unloading && filesSet[index].req) { //檔案已經上傳中
            filesSet[index].req.abort(); //中止上傳
            filesSet[index].unloading = false;  //將上傳中的狀態設定為false   
        } else { //未開始上傳
            filesSet[index].element.remove(); //移除DOM
            filesSet[index].element = null; //釋放對DOM的引用
            filesSet[index].processBar = null;//釋放對DOM的引用
            delete filesSet[index];//刪除檔案陣列中對應的元素   
        }

    }
})

submit.addEventListener('click',function(){// 為上傳按鈕繫結事件

    //這裡使用for...in迴圈,正好可以避免對陣列稀疏元素的遍歷
    for(let key in filesSet){        //如果正在上傳中或已經上傳完成,不再上傳
        if (filesSet[key].unloading || filesSet[key].uploaded) continue; 
        filesSet[key].unloading = true; //標記開始上傳

        //建立一個檔案上傳的Promise,並設定成功及失敗的回撥
        initUpload(filesSet[key]).then(file => {            //上傳成功
            file.element.className = "success"; //UI顯示成功資訊
            file.uploaded = true; //標記上傳成功
        }).catch((file, err) => {
            file.element.className = "error"; //UI顯示失敗資訊
            //取消開始上傳標記,點選上傳按鈕將嘗試再次上傳
            filesSet[key].unloading = false; 
        })
    }
})/**
 * 返回一個檔案上傳的Promise例項
 * @param  {[type]} file 要上傳的檔案
 */function initUpload(file){    return new Promise((res, rej) => {        let formData = new FormData();//宣告一個FormData例項
        let req = new XMLHttpRequest();//建立XHR例項
        let reta = file.processBar.querySelector('.process-rate');//獲得進度條DOM
        let text = file.processBar.querySelector('.process-text');//獲得進度文字DOM
        let pre;//儲存上傳百分比
        //監聽資料傳輸進行中
        req.upload.onprogress =function(data){ 
            pre = (data.loaded/data.total*100).toFixed(2);//計算百分比
            reta.style.width = pre +'%';//修改進度條
            text.innerText = pre +'%' ;//修改進度條文字
        }        //監聽請求完成
        req.onreadystatechange = function () {            if (req.readyState !==4 ) return ; 
            if (req.status === 200 ){                //完成,執行成功回撥
                res(file, req)
            } else {                //失敗,執行失敗回撥
                rej(file, req)
            }
        }
        formData.append('file',file); //使用append方法新增檔案到file鍵
        req.open('POST', '/process_post'); //初始化請求
        req.send(formData); //傳送請求
        file.req = req; //儲存req物件,用於中止請求
        //形如顯示上傳進度
        file.element.className = "uploading"                
    })
}

  到這裡程式碼就結束了,完整程式碼可以檢視。因為是本地上傳,在測試的時候可以使用大一些的檔案,或者限制一下上傳。

結束語

  這些新的API,使得檔案拖放上傳變得簡單起來。可惜低版本的IE並不支援,聽說低版本的IE可以使用Falsh來進行檔案上傳,具體是如何實現的,要不我們下篇再來探討一下。



作者:快減肥成功的胖紙
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3407/viewspace-2812770/,如需轉載,請註明出處,否則將追究法律責任。

相關文章