js大文件切片上传断点续传完整代码
在前面两篇文章JavaScript高级 - 纯js实现文件上传大文件切片上传断点续传(客户端)和JavaScript高级 - nodejs+koa2实现文件上传大文件切片上传断点续传(服务器端)中我们分别介绍了文件上传的服务端接口开发和客户端页面功能开发,接下来将为大家附上前后端完整代码
- 服务端完整代码
// server.js
const Koa = require('koa');
const Router = require('koa-router');
const koastatic = require('koa-static');
const fs = require('fs');
const formidable = require('formidable');
const multiparty = require('multiparty');
const SparkMD5 = require('spark-md5');
const path = require('path');
const app = new Koa();
let router = new Router();
//中间件:设置允许跨域
app.use(async (ctx, next) => {
ctx.set('Access-Control-Allow-Origin', '*');
//处理OPTIONS请求
ctx.request.methods === 'OPTIONS' ? ctx.body = '请求测试成功' : await next();
});
const host = '127.0.0.1',
port = 3000;
const HOSTNAME = `${
host}:${
port}`;
const SERVER_PATH = `${
__dirname}/upload`;
app.listen(port, function () {
console.log('======================================================================');
console.log(`The server started at port: ${
port}, you can access it by ${
HOSTNAME}`);
console.log('======================================================================');
});
//定义延迟函数
const delay = function delay(interval) {
typeof interval !== 'number' ? interval = 1000 : null;
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve();
}, interval);
});
}
//检测文件是否已经存在
const exists = function exists(path) {
return new Promise((resolve, reject) => {
fs.access(path, fs.constants.F_OK, err => {
if (err) {
resolve(false);
return;
}
resolve(true);
});
});
}
//利用multiparty插件解析前端传来的form-data格式的数据,并上传至服务器
const multipartyUpload = function multipartyUpload(req, autoUpload) {
let config = {
maxFieldsSize: 200 * 1024 * 1024
}
if (autoUpload) config.uploadDir = SERVER_PATH;
return new Promise((resolve, reject) => {
new multiparty.Form(config).parse(req, (err, fields, files) => {
if (err) {
reject(err);
return;
}
resolve({
fields,
files
});
});
});
}
//将传进来的文件数据写入服务器
//form-data格式的数据将以流的形式写入
//BASE64格式数据则直接将内容写入
const writeFile = function writeFile(serverPath, file, isStream) {
console.log(serverPath);
return new Promise((resolve, reject) => {
if (isStream) {
try {
let readStream = fs.createReadStream(file.path);
let writeStream = fs.createWriteStream(serverPath);
readStream.pipe(writeStream);
readStream.on('end', () => {
resolve({
result: true,
message: '上传成功!'
});
});
} catch (err) {
resolve({
result: false,
message: err
})
}
} else {
fs.writeFile(serverPath, file.path, err => {
if (err) {
resolve({
result: false,
message: err
})
return;
}
resolve({
result: true,
message: '上传成功!'
});
});
}
});
}
//上传单个文件(form-data),利用第三方插件multipary解析并上传
router.post('/upload_single_file', async (ctx, next) => {
try {
let {
files
} = await multipartyUpload(ctx.req, true);
let file = (files && files.file.length) ? files.file[0] : {
};
ctx.body = {
code: 0,
message: '文件上传成功',
originalFilename: file.originalFilename,
serverPath: file.path.replace(__dirname, HOSTNAME)
}
} catch (err) {
ctx.body = {
code: 1,
message: '文件上传失败'
}
}
});
//上传单个文件(form-data),利用第三方插件解析但不直接上传,而是将文件重命名后再单独上传
router.post('/upload_single_formdata_rename', async (ctx, next) => {
try {
let {
files,
fields
} = await multipartyUpload(ctx.req, false);
let file = (files && files.file.length) ? files.file[0] : {
};
let filename = (fields && fields.filename.length) ? fields.filename[0] : '';
const filePath = `${
SERVER_PATH}/${
filename}`;
let isExist = await exists(filePath);
if (isExist) {
ctx.body = {
code: 0,
message: '文件已经存在',
originalFilename: filename,
serverPath: file.path.replace(__dirname, HOSTNAME)
}
return;
}
let obj = await writeFile(filePath, file, true);
if (obj.result) {
ctx.body = {
code: 0,
message: '文件上传成功',
originalFilename: filename,
serverPath: filePath.replace(__dirname, HOSTNAME)
}
} else {
ctx.body = {
code: 0,
message: '文件上传失败'
}
}
} catch (ex) {
ctx.body = {
code: 0,
message: ex
}
}
});
//解析post请求参数,content-type为application/x-www-form-urlencoded 或 application/josn
const parsePostParams = function parsePostParams(req) {
return new Promise((resolve, reject) => {
let form = new formidable.IncomingForm();
form.parse(req, (err, fields) => {
if (err) {
reject(err);
return;
}
resolve(fields);
});
});
}
//BASE64上传,该方式只能上传小图片,大图片不建议使用这种方式会造成程序卡死,大图片使用form-data上传
router.post('/upload_base64', async (ctx, next) => {
try {
let {
file,
filename
} = await parsePostParams(ctx.req);
file = decodeURIComponent(file);
const suffix = /\.([0-9a-zA-Z]+)$/.exec(filename)[1];
let spark = new SparkMD5.ArrayBuffer();
file = file.replace(/^data:image\/\w+;base64,/, "");
file = Buffer.from(file, 'base64');
spark.append(file);
let filepath = `${
SERVER_PATH}/${
spark.end()}.${
suffix}`;
await delay();
const isExists = await exists(filepath);
if (isExists) {
ctx.body = {
code: 0,
message: '文件已经存在',
originalFilename: filename,
serverPath: file.path.replace(__dirname, HOSTNAME)
}
return;
}
let obj = await writeFile(filepath, file, false);
if (obj.result) {
ctx.body = {
code: 0,
message: '文件上传成功',
originalFilename: filename,
serverPath: filepath.replace(__dirname, HOSTNAME)
}
} else {
ctx.body = {
code: 0,
message: '文件上传失败'
}
}
} catch (err) {
console.log(err);
ctx.body = {
code: 0,
message: err
}
}
});
const mergeFiles = function mergeFiles(hash, count) {
return new Promise(async (resolve, reject) => {
const dirPath = `${
SERVER_PATH}/${
hash}`;
if (!fs.existsSync(dirPath)) {
reject('还没上传文件,请先上传文件');
return;
}
const filelist = fs.readdirSync(dirPath);
if (filelist.length < count) {
reject('文件还未上传完成,请稍后再试');
return;
}
let suffix;
filelist.sort((a, b) => {
const reg = /_(\d+)/;
return reg.exec(a)[1] - reg.exec(b)[1];
}).forEach(item => {
!suffix ? suffix = /\.([0-9a-zA-Z]+)$/.exec(item)[1] : null;
//将每个文件读取出来并append到以hash命名的新文件中
fs.appendFileSync(`${
SERVER_PATH}/${
hash}.${
suffix}`, fs.readFileSync(`${
dirPath}/${
item}`));
fs.unlinkSync(`${
dirPath}/${
item}`); //删除切片文件
});
await delay(1000); //等待1秒后删除新产生的文件夹
fs.rmdirSync(dirPath);
resolve({
path: `${
HOSTNAME}/upload/${
hash}.${
suffix}`,
filename: `${
hash}.${
suffix}`
})
});
}
//大文件切片上传
router.post('/upload_chunk', async (ctx, next) => {
try {
let {
files,
fields
} = await multipartyUpload(ctx.req, false);
let file = (files && files.file[0]) || {
};
let filename = (fields && fields.filename[0]) || '';
let [, hash] = /^([^_]+)_(\d+)/.exec(filename);
const dirPath = `${
SERVER_PATH}/${
hash}`;
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
const filePath = `${
dirPath}/${
filename}`;
const isExists = await exists(filePath);
if (isExists) {
ctx.body = {
code: 0,
message: '文件已经存在',
originalFilename: filename,
serverPath: filePath.replace(__dirname, HOSTNAME)
}
return;
}
let obj = await writeFile(filePath, file, true);
if (obj.result) {
ctx.body = {
code: 0,
message: '文件上传成功',
serverPath: filePath.replace(__dirname, HOSTNAME)
}
} else {
ctx.body = {
code: 1,
message: 'caolouyaqian',
serverPath: filePath.replace(__dirname, HOSTNAME)
}
}
} catch (err) {
ctx.body = {
code: 1,
message: err
}
}
});
//合并切片文件
router.post('/upload_merge', async (ctx, next) => {
const {
hash,
count
} = await parsePostParams(ctx.req);
const {
path,
filename
} = await mergeFiles(hash, count);
ctx.body = {
code: 0,
message: '文件上传成功',
path,
filename
}
});
//获取已上传的切片
router.get('/uploaded', async (ctx, next) => {
try {
const {
hash
} = ctx.request.query;
const dirPath = `${
SERVER_PATH}/${
hash}`;
const filelist = fs.readdirSync(dirPath);
filelist.sort((a, b) => {
const reg = /_([\d+])/;
return reg.exec(a)[1] - reg.exec(b)[1];
});
ctx.body = {
code: 0,
message: '获取成功',
filelist: filelist || []
}
} catch (err) {
ctx.body = {
code: 0,
message: '获取失败',
filelist: []
}
}
});
app.use(koastatic('./'));
app.use(router.routes());
app.use(router.allowedMethods());
//package.json
{
"name": "server-koa",
"version": "1.0.0",
"description": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Alvin",
"license": "MIT",
"dependencies": {
"formidable": "^1.2.2",
"koa": "^2.13.1",
"koa-router": "^10.0.0",
"koa-static": "^5.0.0",
"multiparty": "^4.2.2",
"nodemon": "^2.0.7",
"spark-md5": "^3.0.1"
}
}
- 客户端完整代码
scripts/axios.min.js
scripts/qs.js
scripts/spark-md5.min.js
以上这三个库可以到对应的官网下载
// scripts/axios2.js
let request = axios.create();
request.defaults.baseURL = 'http://127.0.0.1:3000';
request.defaults.headers['Content-Type'] = 'mutipart/form-data';
request.defaults.transformRequest = (data, headers) => {
let contentType = headers['Content-Type'];
if (contentType === 'application/x-www-form-urlencoded') return Qs.stringify(data);
return data;
}
request.interceptors.response.use(response => {
return response.data;
});
//scripts/upload.js
//单文件上传form-data
(function () {
let upload1 = document.querySelector("#upload1"),
upload_inp = upload1.querySelector('.upload-inp'),
upload_select = upload1.querySelector('.upload-btn.select'),
upload_upload = upload1.querySelector('.upload-btn.upload'),
sel_files = upload1.querySelector('.files'),
file1 = upload1.querySelector('.abbr'),
cur_pro = upload1.querySelector('.cur-pro'),
pro_val = upload1.querySelector('.pro-val'),
progress = upload1.querySelector('.progress'),
_file;
upload_select.addEventListener('click', () => {
upload_inp.click();
});
upload_inp.addEventListener('change', function () {
let file = this.files[0];
_file = file;
// if (file.size > 200 * 1024 * 1024) {
// alert('图片必须小于200M');
// // return;
// }
// if (!/(jpg|jpeg|png)/.test(file.type)) {
// alert('只能上传png或jpg或jpeg格式的图片');
// // return;
// }
sel_files.innerHTML = file.name;
progress.style.display = 'inline-block';
pro_val.innerHTML = '';
})
upload_upload.addEventListener('click', function () {
let formData = new FormData();
formData.append('file', _file);
formData.append('filename', _file.name);
request.post('/upload_single_file', formData, {
onUploadProgress: function (ev) {
let pro = ((ev.loaded / ev.total) * 100).toFixed(0) + '%';
cur_pro.style.width = pro;
pro_val.innerHTML = pro;
}
}).then(res => {
console.log(res);
file1.src = `http://${
res.serverPath}`;
file1.style.display = 'block';
}).catch(err => {
console.log(err);
});
});
})();
//单文件base64上传
(function () {
let upload2 = document.querySelector("#upload2"),
upload_inp = upload2.querySelector('.upload-inp'),
upload_upload = upload2.querySelector('.upload-btn.upload'),
sel_files = upload2.querySelector('.files'),
file2 = upload2.querySelector('.abbr'),
progress = upload2.querySelector('.progress'),
cur_pro = upload2.querySelector('.cur-pro'),
pro_val = upload2.querySelector('.pro-val'),
_file;
upload_upload.addEventListener('click', () => {
upload_inp.click();
});
upload_inp.addEventListener('change', function () {
progress.style.display = 'inline-block';
pro_val.innerHTML = '';
let file = this.files[0];
_file = file;
if (file.size > 200 * 1024) {
alert('图片必须小于200k');
return;
}
if (!/(jpg|jpeg|png)/.test(file.type)) {
alert('只能上传png或jpg或jpeg格式的图片');
return;
}
sel_files.innerHTML = file.name;
let fr = new FileReader();
fr.readAsDataURL(file);
fr.onload = ev => {
file2.src = ev.target.result;
file2.style.display = 'block';
console.log(file.name);
request.post('/upload_base64', {
file: encodeURIComponent(ev.target.result),
filename: file.name
}, {
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
onUploadProgress: function (ev) {
let pro = ((ev.loaded / ev.total) * 100) + '%';
pro_val.innerHTML = pro;
cur_pro.style.width = pro;
}
}).then(res => {
console.log(res);
alert('上传成功了');
return;
}).catch(err => {
console.log(err);
alert('失败了?')
});
};
})
})();
//多文件上传form-data
(function () {
let upload3 = document.querySelector("#upload3"),
upload_inp = upload3.querySelector('.upload-inp'),
upload_upload = upload3.querySelector('.upload-btn.upload'),
sel_files = upload3.querySelector('.list');
upload_upload.addEventListener('click', () => {
upload_inp.click();
});
upload_inp.addEventListener('change', function () {
let files = this.files;
sel_files.innerHTML = '';
[].forEach.call(files, (file, index) => {
sel_files.innerHTML += `<div><span class="files" style="margin-right:8px;font-size:12px">${
file.name}</span><span class="pro-val" id="myfile${
index}"></span></div>`
let formData = new FormData();
formData.append('file', file);
formData.append('filename', file.name);
request.post('/upload_single_file', formData, {
onUploadProgress: function (ev) {
let pro = ((ev.loaded / ev.total) * 100).toFixed(0) + '%';
document.querySelector(`#myfile${
index}`).innerHTML = pro;
// sel_files.innerHTML += `<span class="files">${file.name}</span> <span class="pro-val" >${pro}</span>`
}
}).then(res => {
console.log(res);
// alert('上传成功了');
}).catch(err => {
console.log(err);
});
});
});
})();
//拖拽上传form-data
(function () {
let upload5 = document.querySelector("#upload5"),
upload_inp = upload5.querySelector('.upload-inp'),
upload_upload = upload5.querySelector('.upload-btn'),
sel_files = upload5.querySelector('.list');
const uploadFiles = function uploadFiles(files) {
sel_files.innerHTML = '';
[].forEach.call(files, (file, index) => {
sel_files.innerHTML += `<div><span class="files" style="margin-right:8px;font-size:12px">${
file.name}</span><span class="pro-val" id="myfile${
index}"></span></div>`
let formData = new FormData();
formData.append('file', file);
formData.append('filename', file.name);
request.post('/upload_single_file', formData, {
onUploadProgress: function (ev) {
let pro = ((ev.loaded / ev.total) * 100).toFixed(0) + '%';
document.querySelector(`#myfile${
index}`).innerHTML = pro;
}
}).then(res => {
console.log(res);
// alert('上传成功了');
}).catch(err => {
console.log(err);
});
});
}
upload5.addEventListener('dragover', function (ev) {
ev.preventDefault();
});
upload5.addEventListener('drop', (ev) => {
ev.preventDefault();
uploadFiles(ev.dataTransfer.files);
});
upload_inp.addEventListener('change', function () {
uploadFiles(this.files);
});
upload5.addEventListener('click', (ev) => {
upload_inp.click();
});
})();
//大文件切片上传,断点续传
(function () {
let upload4 = document.querySelector("#upload4"),
upload_inp = upload4.querySelector('.upload-inp'),
upload_upload = upload4.querySelector('.upload-btn'),
sel_files = upload4.querySelector('.files'),
cur_pro = upload4.querySelector('.cur-pro'),
pro_val = upload4.querySelector('.pro-val'),
progress = upload4.querySelector('.progress');
const retriveHash = function retriveHash(file) {
return new Promise((resolve, reject) => {
let spark = new SparkMD5.ArrayBuffer();
let fr = new FileReader();
fr.readAsArrayBuffer(file);
fr.onload = (ev) => {
spark.append(ev.target.result);
let hash = spark.end();
let suffix = /\.([0-9a-zA-Z]+)$/.exec(file.name)[1];
resolve({
hash,
suffix
});
};
});
}
let complete = 0;
const uploadComplete = function uploadComplete(hash, count) {
complete++;
let progerss = (complete / count * 100).toFixed(2) + '%';
cur_pro.style.width = progerss;
pro_val.innerHTML = progerss;
if (complete < count) return;
cur_pro.style.width = '100%';
pro_val.innerHTML = '100%';
setTimeout(() => {
request.post('/upload_merge', {
hash,
count
}, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}).then(res => {
console.log(res);
// alert('上传成功了');
}).catch(err => {
console.log(err);
});
}, 3000);
}
upload_upload.addEventListener('click', function () {
upload_inp.click();
});
upload_inp.addEventListener('change', async function () {
let file = this.files[0];
progress.style.display = 'inline-block';
cur_pro.style.width = '0%';
pro_val.innerHTML = '0%';
let chunks = [];
let {
hash,
suffix
} = await retriveHash(file);
sel_files.innerHTML = `${
hash}.${
suffix}`;
let {
filelist
} = await request.get('/uploaded', {
params: {
hash
}
});
let maxSize = 100 * 1024; //100k
let count = Math.ceil(file.size / maxSize);
//限制切片的数量不能超过20个,并重新计算每个切片的大小
if (count > 20) {
maxSize = file.size / 20;
count = 20;
}
let index = 0;
while (index < count) {
chunks.push({
file: file.slice(index * maxSize, (index + 1) * maxSize),
filename: `${
hash}_${
index+1}.${
suffix}`
});
index++;
}
chunks.forEach((item, index) => {
//如果已经上传过就不再上传了
if (filelist && filelist.length > 0 && filelist.includes(item.filename)) {
uploadComplete(hash, count);
return;
}
let formData = new FormData();
formData.append('file', item.file);
formData.append('filename', item.filename);
request.post('/upload_chunk', formData).then(res => {
uploadComplete(hash, count);
// console.log(res);
// alert('上传成功了');
}).catch(err => {
console.log(err);
});
});
});
})()
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件上传</title>
<link rel="stylesheet" href="./css/upload.css">
</head>
<body>
<div class="container">
<div class="item" id="upload1">
<div class="title">单文件上传FORM-DATA,先选文件再上传</div>
<input type="file" class="upload-inp">
<button class="upload-btn select">选择文件</button>
<button class="upload-btn upload">上传到服务器</button>
<div class="list">
<span class="files"></span>
<div class="progress">
<div class="cur-pro">
</div>
</div>
<span class="pro-val"></span>
<img class="abbr" src="">
</div>
</div>
<div class="item" id='upload2'>
<div class="title">单文件上传BASE64,只能上传100k以内的png或jpg图片文件</div>
<input type="file" class="upload-inp">
<button class="upload-btn upload">选择并上传</button>
<div class="list">
<span class="files"></span>
<div class="progress">
<div class="cur-pro">
</div>
</div>
<span class="pro-val"></span>
<img class="abbr" src="">
</div>
</div>
<div class="item" id='upload3'>
<div class="title">多文件上传FORM-DATA</div>
<input type="file" class="upload-inp" multiple>
<!-- <button class="upload-btn select">选择文件</button> -->
<button class="upload-btn upload">选择并上传</button>
<div class="list">
</div>
</div>
<div class="item" id="upload4">
<div class="title">大文件切片上传,断点续传FORM-DATA</div>
<input type="file" class="upload-inp">
<!-- <button class="upload-btn select">选择文件</button> -->
<button class="upload-btn upload">选择并上传</button>
<div class="list">
<span class="files"></span>
<img class="abbr" src="">
<div class="progress">
<div class="cur-pro">
</div>
</div>
<span class="pro-val"></span>
</div>
</div>
<div class="item drag-upload" id='upload5'>
<div class="title">多文件拖拽上传FORM-DATA</div>
<input type="file" class="upload-inp" multiple>
<div class="upload-btn">将文件拖到此处,或<span style="color:cornflowerblue">点击上传</span></div>
<div class="list">
</div>
</div>
</div>
</body>
<script src="./scripts/axios.min.js"></script>
<script src="./scripts/qs.js"></script>
<script src="./scripts/spark-md5.min.js"></script>
<script src="./axios2.js"></script>
<script src="./upload.js"></script>
</html>
.container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.container .item {
border: 1px dashed #b7b7b7;
width: 350px;
height: 200px;
margin-right: 50px;
margin-bottom: 20px;
}
.item .title {
font-size: 12px;
color: #b7b7b7;
}
.upload-inp {
display: none;
}
.upload-btn {
padding: 10px 15px;
margin: 5px;
border: none;
cursor: pointer;
}
.upload-btn.select {
background-color: lightblue;
}
.upload-btn.upload {
background-color: lightgreen;
}
.disabled {
background-color: #b7b7b7 !important;
color: grey !important;
}
.progress {
display: none;
width: 280px;
height: 10px;
border: 1px solid green;
margin-left: 5px;
text-align: center;
color: red;
}
.progress .cur-pro {
width: 0%;
height: 10px;
background-color: green;
}
.list {
font-size: 12px;
}
.list .abbr {
display: none;
width: 100px;
height: 100px;
}
.list .files {
margin-right: 8px;
}
.drag-upload {
cursor: pointer;
background: url('./upload.png') no-repeat 120px 50px;
}
.drag-upload .upload-btn {
font-size: 12px;
margin-left: 80px;
margin-top: 100px;
}