学习最好的方式就是动手实践。
之前做后台的时候,需要附件上传功能的时候都是通过form表单的方式或者直接找现有的上传文件的JavaScript类库。但是有时候也需要做一些个性化的修改,不懂原理的时候就很蛋疼了。今天动手简单实现了通过AJAX(XMLHttpRequest)来实现图片的上传,通过也拓展了文件来源的方式,比如拖拽文件上传,复制粘贴上传。
最终效果
- 点击上传
- 拖拽上传
- 复制粘贴上传
- 上传进度条
代码
- 点击上传
点击上传利用 input[type=‘file’]标签来选取需要上传的照片,然后再通过选取document.getElementById的方式来获取用户选中的文件内容。然后通过XMLHttpRequest来向后台发送数据,数据的载体FormData。由于这一次同时实现了点击,拖拽,粘贴三种方式,如果单独把点击方式放一个地方显得有些突兀,所以这个地方通过隐藏input[type=‘file’]标签,然后点击时通过api来激活选取文件的弹窗。
html
<body>
<input id="file" style="display: none;" type="file" name="file" multiple='true' onchange="fileChange(this)" />
<!-- 功能框 -->
<div id="drag"
style="border: 1px dashed #dddddd;height: 200px;width: 200px;margin: 50px;display: flex;flex-direction: column;align-items: center;justify-content: center;">
<div><a href='javascript:linkClick();'>点击上传</a></div>
<div>拖拽上传</div>
<div>复制粘贴文件</div>
</div>
<!-- 选中列表 -->
<ul id="fileList" class="file-list">
</ul>
<!-- 进度条 -->
<div class="process-container">
<p id="process-number" class="process-number"></p>
<div class="process"></div>
</div>
<button onclick="uploadBatch()">上传</button>
</body>
css
.file-list {
margin: 10px;
border: 1px dashed #dddddd;
width: 200px;
display: none;
padding: 0;
margin: 0 50px;
}
.file-list li {
list-style: none;
margin: 5px;
}
.process-container {
border: 1px solid #dddddd;
margin: 20px 50px;
height: 10px;
width: 200px;
border-radius: 10px;
position: relative;
}
.process {
background: orange;
border-radius: 10px;
height: 100%;
width: 0%;
}
.process-number {
position: absolute;
left: 40%;
top: 0%;
font-size: 1px;
line-height: 7px;
}
button {
margin-left: 50px;
width: 200px;
height: 50px;
}
javaScript
<script>
//上传方法
function upload(event) {
let file = document.getElementById('file');
let data = new FormData();
data.append('file', file.files[0]);
ajax(data, (res) => {
console.log('success')
}, () => {
console.log('fail')
})
}
//自己封装一个简单的ajax请求方法
function ajax(data, successCallback, failCallback) {
let xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:8888/api/file', true)
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
typeof successCallback == 'function' && successCallback(JSON.parse(xhr.response));
} else {
typeof failCallback == 'function' && failCallback();
}
}
};
//进度条监听
xhr.upload.addEventListener("progress", function (event) {
if (event.lengthComputable) {
let processPrecent = Math.ceil(event.loaded * 100 / event.total) + "%";
let process = document.getElementsByClassName('process');
process[0].style.width = processPrecent
let processNumer = document.getElementById('process-number');
processNumer.innerHTML = processPrecent
}
}, false);
xhr.send(data);
}
</script>
-
拖拽上传实现
拖拽上传主要是通过drop事件来实现了,为了有更好的用户体验,也需要dragover,dragenter,dragleave这个三个事件来配合。dragover 当元素或者选择的文本被拖拽到一个有效的放置目标上时,触发 dragover 事件(每几百毫秒触发一次)。
dragenter 当拖动的元素或被选择的文本进入有效的放置目标时, dragenter 事件被触发。
dragleave 当一个被拖动的元素或者被选择的文本离开一个有效的拖放目标时,将会触发dragleave 事件。
说明:通过这个三个事件相互配合,当用户拖拽文件到指定功能框的时候或者从指定框中离开的时候,框内的内容会发生变化,以达到提示用户的操作的目的重点事件 drop 当一个元素或是选中的文字被拖拽释放到一个有效的释放目标位置时,drop 事件被抛出。
当用户把文件拖债释放到直接功能框中,就会触发这个事件。而这个事件会给我们一个DataTransfer 对象,DataTransfer对象中就有我们要的files内容,如下图。这样我们就能拿到file对象,跟点击上传同样的原理,组装FormData通过AJAX就可以上传到服务器。
-
复制粘贴上传实现
粘贴方式实现上传依赖 paste 事件。通过给目标功能框绑定粘贴事件后,当我们通过ctrl+v或者右键粘贴的方式激活 paste 事件,我们就可以获得一个clipboardData对象,如下图,通过原型指向我们可以看出clipboardData的父对象是DataTransfer。同时我们也可以获取到我们想要的file文件。然后同上方式上传服务器。
-
进度条实现
进度条XMLHttpRequest提供的一个监听方法xhr.upload.addEventListener(“progress”, function (event) {}),通过这个event对象,我们可以拿到文件已经上传的大小event.loaded 和 上传文件的总量 event.total,求一个百分比就可以实现进度条了。
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>文件上传--乐闻</title>
<style>
.file-list {
margin: 10px;
border: 1px dashed #dddddd;
width: 200px;
display: none;
padding: 0;
margin: 0 50px;
}
.file-list li {
list-style: none;
margin: 5px;
}
.process-container {
border: 1px solid #dddddd;
margin: 20px 50px;
height: 10px;
width: 200px;
border-radius: 10px;
position: relative;
}
.process {
background: orange;
border-radius: 10px;
height: 100%;
width: 0%;
}
.process-number {
position: absolute;
left: 40%;
top: 0%;
font-size: 1px;
line-height: 7px;
}
button {
margin-left: 50px;
width: 200px;
height: 50px;
}
</style>
</head>
<body>
<input id="file" style="display: none;" type="file" name="file" multiple='true' onchange="fileChange(this)" />
<!-- 功能框 -->
<div id="drag"
style="border: 1px dashed #dddddd;height: 200px;width: 200px;margin: 50px;display: flex;flex-direction: column;align-items: center;justify-content: center;">
<div><a href='javascript:linkClick();'>点击上传</a></div>
<div>拖拽上传</div>
<div>复制粘贴文件</div>
</div>
<!-- 选中列表 -->
<ul id="fileList" class="file-list">
</ul>
<!-- 进度条 -->
<div class="process-container">
<p id="process-number" class="process-number"></p>
<div class="process"></div>
</div>
<button onclick="uploadBatch()">上传</button>
</body>
<script>
let uploadfileList = [];
window.onload = function () {
let drag = document.getElementById('drag');
document.addEventListener('dragover', ev => {
drag.style.display = 'block';
ev.preventDefault()
}, false);
drag.addEventListener('dragenter', () => {
drag.innerHTML = '请松手';
}, false);
drag.addEventListener('dragleave', () => {
console.log('bbb')
drag.innerHTML = '请拖到这里';
})
drag.addEventListener('drop', (event) => {
let file = event.dataTransfer.files[0];
// let data = new FormData();
// data.append('file', file);
// ajax(data, (res) => {
// console.log(res)
// }, () => {
// console.log('error')
// })
appendFile(file.name, file.type, file)
})
//粘贴事件
drag.addEventListener('paste', (event) => {
debugger
if (event.clipboardData.files[0]) {
let file = event.clipboardData.files[0];
appendFile(file.name, file.type, file)
}
})
}
//文件选择变动
function fileChange(event) {
if (event.files) {
let file = event.files[0];
//可以在这个地方开始上传文件了
//这个地方实现的是批量上传
appendFile(file.name, file.type, file)
}
}
function linkClick() {
//激活文件选择框
document.getElementById('file').click()
}
function appendFile(name, type, file) {
uploadfileList.push(file)
let fileList = document.getElementById('fileList');
if (!fileList.style.display) {
fileList.style.display = 'block';
}
let li = document.createElement('li');
li.innerHTML = `${type}:${name}`
fileList.appendChild(li)
}
function upload(event) {
let file = document.getElementById('file');
let data = new FormData();
data.append('file', file.files[0]);
ajax(data, (res) => {
console.log('success')
}, () => {
console.log('fail')
})
}
function uploadBatch() {
let data = new FormData();
uploadfileList.forEach((item, index) => {
data.append(`file${index}`, item)
})
data.append('files', uploadfileList)
ajax(data, (res) => {
console.log('success')
}, () => {
console.log('fail')
})
}
function ajax(data, successCallback, failCallback) {
let xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:8888/api/file', true)
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
typeof successCallback == 'function' && successCallback(JSON.parse(xhr.response));
} else {
typeof failCallback == 'function' && failCallback();
}
}
};
//进度条监听
xhr.upload.addEventListener("progress", function (event) {
if (event.lengthComputable) {
let processPrecent = Math.ceil(event.loaded * 100 / event.total) + "%";
let process = document.getElementsByClassName('process');
process[0].style.width = processPrecent
let processNumer = document.getElementById('process-number');
processNumer.innerHTML = processPrecent
}
}, false);
xhr.send(data);
}
</script>
</html>
以上都是客户端的实现,接下来看下服务端的代码。
服务端实现是基于Node的Koa框架来实现,当然什么后台语言都能实现,比如之前的文章写过用Java实现文件的上传。
- 初始化服务端项目
npm init -y
npm install koa koa-body koa-router -D
koa 服务端基础包,koa-body 处理POST请求参数,koa-router 路由中间件
const Koa = require('koa');
const koaBody = require('koa-body');
const Router = require('koa-router');
const cors = require('koa2-cors');
const app = new Koa();
const router = new Router({ prefix: '/api' });
app.use(koaBody({
multipart: true
}))
router.post('/file', async (ctx, next) => {
const files = ctx.request.files
//循环处理,服务器保存还是上传到图床
ctx.response.status = 200;
ctx.body = {
code:'0'
}
})
app.use(router.routes()).use(router.allowedMethods());
let port = 8888;
app.listen(port, () => {
console.log(`server started osn ${port}`)
})
以上我们就可以通过router.post(‘file’, async (ctx,next)=>{})路由拿到前端的请求,从而拿到前端传过来的File对象,如下图。拿到File对象之后就看我们需要怎么处理这些文件了,是直接保存到服务器的文件系统中还是上传到图床上还是直接处理解析文件内容,这个就很随心所欲了。下面实现一下上传的文件直接保存到服务器文件系统。
服务器直接存储文件
const fs = require("fs");//需要头部引入
router.post('/file', async (ctx, next) => {
const files = ctx.request.files;
for (let key in files) {
let file = files[key];
const reader = fs.createReadStream(file.path); // 创建可读流
const ext = file.name.split('.').pop(); // 获取上传文件扩展名
const upStream = fs.createWriteStream(`${Math.random().toString()}.${ext}`);
reader.pipe(upStream);
}
ctx.response.status = 200;
ctx.body = {
code: 200
}
})
以上就可以完成文件的上传保存咯
这里埋了个坑,如果直接用上面的代码,会报如下图的错
没错,跨域问题。
const cors = require('koa2-cors');
app.use(cors());//在路由中间价生效前引入
app.use(router.routes()).use(router.allowedMethods());
到此结束,散会。