这篇文章利用H5的拖放API,实现拖拽文件到浏览器上传的性能。

业务需要:

1、拖拽上传文件到浏览器可上传区域
2、上传文件停留在可上传区域,提示信息可上传。
3、开释上传文件,开释地位在可上传区域,执行上传操作,显示上传进度。

拖放事件

HTML 的 drag & drop 应用了 DOM event model 以及从 mouse events 继承而来的 drag events 。一个典型的拖拽操作是这样的:用户选中一个可拖拽的(draggable)元素,并将其拖拽(鼠标不放开)到一个可搁置的(droppable)元素,而后开释鼠标。

依据业务需要,这里将应用到的拖拽事件包含:

  • dragover 文件被进入到可上传区域时触发
  • dragleave 拖拽文件来到可上传区域时触发
  • drop 拖拽文件在可上传区域开释时触发

接下来编写可上传区域的dom元素:

<div id="target">    <p class="tip">拖拽文件到此处上传</p></div>

增加事件:

const dragEl=document.getElementById("target");dragEl.addEventListener("dragover",handleOver)dragEl.addEventListener("drop",handleDrop)dragEl.addEventListener("dragleave",handleLeave)// 开释在指标区域function handleDrop(ev){    ev.preventDefault();    // 获取开释文件    console.log(ev.dataTransfer.files[0])}function handleOver(ev){    ev.preventDefault();    console.log("进入指标区域")}function handleLeave(ev){    ev.preventDefault();    console.log("来到指标区域")}
增加的拖拽事件都要先阻止浏览器默认事件

提示信息

良好的提示信息能够让用户有更好的应用体验,接下来给上传区域增加提示信息:

<div class="u init" id="target">    <p class="tip-start">拖拽文件到此处上传</p>    <p class="tip-over">松开上传</p>    <p class="tip-uploading">上传中...</p>    <p class="tip-done">上传胜利</p>    <p class="tip-error">上传失败</p></div>

通过款式管制信息的显示暗藏:

.u{    width: 100%;    height: 95vh;    display: flex;    align-items: center;    justify-content: center;    transition:background .3s;}.u [class^="tip"]{    opacity: .5;    font-size: 23px;    text-align: center;}.u [class^="tip"]{    display: none;}.u.init{    background-color: #f0f0f0;}.u.init .tip-start{    display: block;}.u.actived{    background-color: #f9f9f9;    border: 1px dashed #ddd;}.u.actived .tip-over{    display: block;}.u.uploading{    background-color: #ddd;}.u.uploading .tip-uploading{    display: block;}.u.success{    background-color: #f5f5f5;}.u.success .tip-done{    display: block;}.u.error{    background-color: #ffd0d0;}.u.error .tip-error{    display: block;}

这里通过给target元素增加不同的类名,管制不同信息的款式,如下:

  • className=".u.init" 初始状态
  • className=".u.actived" 指标进入可上传区域
  • className=".u.uploading" 文件正在上传
  • className=".u.success" 文件上传胜利
  • className=".u.error" 文件上传失败

接下来给拖拽事件增加上对应的显示类名:

function handleDrop(ev){    ev.preventDefault();    dragEl.className="u uploading"    ...}function handleOver(ev){    ev.preventDefault();    dragEl.className="u actived"}function handleLeave(ev){    ev.preventDefault();    dragEl.className="u init"}

文件上传

当文件在上传区域开释之后,获取到上传文件信息,利用FormData对象生成表单数据,接着执行上传操作。

function handleDrop(ev){    ev.preventDefault();    dragEl.className="u uploading"    // 生成表单信息    const fd = new FormData();    const file = ev.dataTransfer.files[0];    fd.append("file",file);    // 上传文件函数    upload(fd)}

生成XMLHttpRequest发送表单信息给后端:

function upload(data){    const xhr = new XMLHttpRequest();    // 上传办法和门路    xhr.open("POST","/upload");    xhr.responseType = "json";        xhr.onload = function(){        if(xhr.response && xhr.response.success){            // 上传胜利后,显示胜利款式            dragEl.className="u success"            // 复原初始状态            setTimeout(()=>dragEl.className="u init",3000);        }else{            // 上传失败            dragEl.className="u error";            console.error(xhr.response.error);        }    }    // 发送    xhr.send(data);}

为了避免不必要的bug产生,这里执行上传操作之前还须要做一些判断:

  • 文件还在上传状态,此时文件上传区域不可用
  • 拖拽的文件是否合乎上传要求(文件类型、文件大小)
function handleDrop(ev){    ev.preventDefault();    // 还有文件在上传    if(dragEl.className.indexOf("uploading")>=0) return;    ...    const MAX_SIZE =  200 * 1024 * 1024;    if(file.size>= MAX_SIZE) return alert("文件大于 200mb");}

上传进度条

通过xhr.upload.onprogress能够获取到文件的上传进度信息:

xhr.upload.onprogress=function({loaded,total}){    const precent = (loaded/total)*100;    console.log(precent)}

给dom元素增加进度条:

<div class="tip-uploading">    <p>上传中...</p>    <p class="progress"><span class="line" id="precent"></span></p></div>

增加进度条款式:

.progress{    height: 5px;    width: 300px;    overflow: hidden;    position: relative;    border: 1px solid #999;}.line{    top: 0;    left: 0;    width: 100%;    height: 100%;    transition:.1s;    position: absolute;    background-color: #999;    transform: translateX(-100%);}

进度条onprogress事件:

function upload(data){    ...    //进度条元素    const precentEl = document.getElementById("precent");    xhr.upload.onprogress=function({loaded,total}){        const precent = (loaded/total)*100;        // 进度地位        precentEl.style.transform = `translateX(-${100-precent}%)`        // 实现上传后,显示上传胜利信息        if(precent>=100) setTimeout(() => dragEl.className="u success", 500);    }    ...}

后端 Express + multer 解决上传文件

整个流程不算简单,能够通过expressmulter模仿整个流程。

关上控制台,初始化npm文件:

npm init -y

装置expressmulter

npm install express multer --save-dev

文件目录:

/- package.json/- server.js/- index.html/- uploads
  • server.js
const { resolve } = require("path");const express = require("express");const app = express();const multer = require("multer");const upload = multer({ dest: resolve(__dirname, "./uploads") });app.use(express.static(__dirname));app.post("/upload", upload.single("file"), (req, res) => {  res.send({ success: true, message: req.file });});app.listen(3000, () => console.log(`Serving on localhost:3000`));
  • index.html
<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>拖拽上传+进度条显示</title>    <style>        .u{            width: 100%;            height: 95vh;            display: flex;            align-items: center;            justify-content: center;            transition:background .3s;        }        .u.init{            background-color: #f0f0f0;        }        .u.init .tip-start{            display: block;        }        .u.actived{            background-color: #f9f9f9;            border: 1px dashed #ddd;        }        .u.actived .tip-over{            display: block;        }        .u.uploading{            background-color: #ddd;        }        .u.uploading .tip-uploading{            display: block;        }        .u.success{            background-color: #f5f5f5;        }        .u.success .tip-done{             display: block;        }        .u.error{            background-color: #ffd0d0;        }        .u.error .tip-error{             display: block;        }        .u [class^="tip"]{            opacity: .5;            font-size: 23px;            text-align: center;        }        .u [class^="tip"]{            display: none;        }        .progress{            height: 5px;            width: 300px;            overflow: hidden;            position: relative;            border: 1px solid #999;        }        .line{            top: 0;            left: 0;            width: 100%;            height: 100%;            transition:.1s;            position: absolute;            background-color: #999;            transform: translateX(-100%);        }    </style></head><body><div class="u init" id="target">    <p class="tip-start">拖拽文件到此处上传</p>    <p class="tip-over">松开上传</p>    <div class="tip-uploading">        <p>上传中...</p>        <p class="progress"><span class="line" id="precent"></span></p>    </div>    <p class="tip-done">上传胜利</p>    <p class="tip-error">上传失败</p></div><script>    const dragEl=document.getElementById("target");    dragEl.addEventListener("dragover",handleOver)    dragEl.addEventListener("drop",handleDrop)    dragEl.addEventListener("dragleave",handleLeave)    function handleDrop(ev){        ev.preventDefault();        // 还有文件正在上传        if(dragEl.className.indexOf("uploading")>=0) return;            dragEl.className="u uploading"        const file = ev.dataTransfer.files[0];        const fd = new FormData();        fd.append("file",file);        upload(fd)    }        function handleOver(ev){        ev.preventDefault();        dragEl.className="u actived"    }    function handleLeave(ev){        ev.preventDefault();        dragEl.className="u init"    }    function upload(data){        const xhr = new XMLHttpRequest();        xhr.open("POST","/upload");        xhr.responseType = "json";                const precentEl = document.getElementById("precent");        xhr.upload.onprogress=function({loaded,total}){            const precent = (loaded/total)*100;            precentEl.style.transform = `translateX(-${100-precent}%)`            if(precent>=100) setTimeout(() => dragEl.className="u success", 500);        }        xhr.onload = function(){            if(xhr.response && xhr.response.success){                setTimeout(()=>dragEl.className="u init",3000);            }else{                dragEl.className="u error";                console.error(xhr.response.error);            }        }        xhr.send(data);    }</script></body></html>

控制台执行node server.js,浏览器关上地址能够看到整个流程。