这篇文章利用H5的拖放API,实现拖拽文件到浏览器上传的功能。
业务需求:
1、拖拽上传文件
到浏览器可上传区域
。
2、上传文件
停留在可上传区域
,提示信息可上传。
3、释放上传文件
,释放位置在可上传区域
,执行上传操作,显示上传进度。
有点类似图片压缩网站 tinypng
拖放事件
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 处理上传文件
整个流程不算复杂,可以通过express
和multer
模拟整个流程。
打开控制台,初始化npm
文件:
npm init -y
安装express
和multer
:
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
,浏览器打开地址可以看到整个流程。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。