H5拖放异步文件上传之二——上传进度监听

  上一篇《H5拖放+FormData接口+NodeJS,完整异步文件上传(一)》,我们走通了拖放文件上传的整个流程,但离实际使用场景还有差距。这篇,我们来添加几个实际使用场景必要的功能,向实际使用再走一步。

添加功能

  1. 显示待上传文件列表;
  2. 支持移除待上传文件;
  3. 使用upload.onprogress显示上传进度;
  4. 支持中断上传;

upload.progress

XMLHttpRequest.upload方法返回一个 XMLHttpRequestUpload对象,用来表示上传的进度。这个对象是不透明的,但是作为一个XMLHttpRequestEventTarget,可以通过对其绑定事件来追踪它的进度。onprogres监听数据传输进行中(通过监听这个事件,可获得上传进度)。摘自MDN

实现思路

  拖放文件到上传区域时,将文件保存在一个文件数组中,添加并显示文件相关信息和移除按钮,点击移除按钮删除文件数组中对应的文件元素,点击上传按钮,遍历文件数组,开始上传待上传文件,此时点击移除按钮则中止文件上传。

代码

//相关样式
.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-->
<div class="drop-area"  ondrop="drop_hander(event)" ondragover = "dragover_hander(event)"></div><!--监听拖放DOM-->
<div id="fileList" class="file-list"></div><!--显示待上传文件列表-->
<button id="submit">上传</button><!--上传按钮-->
//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(); //创建进度条DOM
let 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"                
    })
}

  到这里代码就结束了,完整代码可以查看Github。因为是本地上传,在测试的时候可以使用大一些的文件,或者限制一下上传。

结束语

  这些新的API,使得文件拖放上传变得简单起来。可惜低版本的IE并不支持,听说低版本的IE可以使用Falsh来进行文件上传,具体是如何实现的,要不我们下篇再来探讨一下。
  
  

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容