松鼠的博客

导航

文件上传 切片与断点续传

主要讲前端 ,后端使用node。会写一下后端的处理
1.单个文件上传
请求头为 multipart/form-data
数据为 from-data格式

let formData = new FormData();
formData.append('file', _file);
formData.append('filename', _file.name);

然后使用post直接给后端。后端直接存起来

2.单个文件使用base64格式上传(针对于图片)
在拿到文件后

// 把选择的文件读取成为BASE64
const changeBASE64 = file => {
return new Promise(resolve => {
let fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = ev => {
resolve(ev.target.result);
};
});
};
BASE64 = await changeBASE64(file);

这里使用了一个内置类为 FileReader ,readAsDataURL方法是编为base64,也还有别的编码 比如二进制
fileReader.onload 是一个异步事件。这里封装了一个promise

instance 是一个axios实例
为了防止乱码,这里为了防止乱码使用encodeURIComponent做了一个编码,记得让后端解码呀

data = await instance.post('/upload_single_base64', {
file: encodeURIComponent(BASE64),
filename: file.name
}, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});

针对这个功能一个小提示 就是如果你同一个文件 上传第二次的时候,后端会提示你文件存在(这是优化点,后端可以不做也行)

后端处理

// 单文件上传处理「BASE64」
app.post('/upload_single_base64', async (req, res) => {
let file = req.body.file,
filename = req.body.filename,
spark = new SparkMD5.ArrayBuffer(), //SparkMD5 第三方 实例化一个对象
suffix = /\.([0-9a-zA-Z]+)$/.exec(filename)[1], // 获取文件类型
isExists = false,
path;
file = decodeURIComponent(file); // 解码
file = file.replace(/^data:image\/\w+;base64,/, ""); // 提取base64的内容
file = Buffer.from(file, 'base64'); // 二进制转换
spark.append(file); // 添加文件进行分析
path = `${uploadDir}/${spark.end()}.${suffix}`; //生成唯一hash文件名 spark.end()
await delay();
// 检测是否存在
isExists = await exists(path); // 根据文件名判断是否存在
if (isExists) {
res.send({
code: 0,
codeText: 'file is exists',
originalFilename: filename,
servicePath: path.replace(__dirname, HOSTNAME)
});
return;
}
writeFile(res, path, file, filename, false);
});


// 检测文件是否存在
const exists = function exists(path) {
return new Promise(resolve => {
fs.access(path, fs.constants.F_OK, err => {
if (err) {
resolve(false);
return;
}
resolve(true);
});
});
};

 

一般这个是后端处理,但是不排除让你自己来。了解一下机制就行

3.缩略图处理 ,并上传

请求头为 multipart/form-data
数据为 from-data格式
1.缩略图 的话是前端把文件转成base64地址,然后使用src属性

上传的时候,前端进行文件名的hash 处理,后端不处理

主要是先把文件转成二进制 再 使用 SparkMD5插件

const changeBuffer = file => {
return new Promise(resolve => {
let fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
fileReader.onload = ev => {
let buffer = ev.target.result,
spark = new SparkMD5.ArrayBuffer(),
HASH,
suffix;
spark.append(buffer);
HASH = spark.end();
suffix = /\.([a-zA-Z0-9]+)$/.exec(file.name)[1];
resolve({
buffer,
HASH,
suffix,
filename: `${HASH}.${suffix}`
});
};
});
};

这样后端就只要判断就行,不会进行处理
后端处理
multiparty_upload 用来解析 fromdata 格式 。

4 文件上传进度监控
这里主要是用到了axios中的 onuploadProgress 实现

try {
let formData = new FormData();
formData.append('file', file);
formData.append('filename', file.name);
data = await instance.post('/upload_single', formData, {
// 文件上传中的回调函数 xhr.upload.onprogress
onUploadProgress(ev) {
let {
loaded,
total
} = ev;
upload_progress.style.display = 'block';
upload_progress_value.style.width = `${loaded/total*100}%`;
}
});
if (+data.code === 0) {
upload_progress_value.style.width = `100%`;
await delay(300);
alert(`恭喜您,文件上传成功,您可以基于 ${data.servicePath} 访问该文件~~`);
return;
}
throw data.codeText;
} catch (err) {
alert('很遗憾,文件上传失败,请您稍后再试~~');

主要代码和单一文件的上传没有太大区别,只是多了一个axios回调
loaded是已上传 大小 total是总大小

// 延迟函数
const delay = function delay(interval) {
typeof interval !== "number" ? interval = 1000 : null;
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, interval);
});
};

这里设置了一个延时函数,是因为过渡是有一个动画效果 所以为了顺滑再最后一个100% 动画完成后,再进行提示 (这个自己根据情况去设置)

5 多文件上传,进度监控
input 要设置 多选属性

主要是一个循环去发送请求,去监听上传进度,再用promise.all 去进行后续的一些处理工作

6 拖拽文件上传

这里用到了几个特殊事件

// 拖拽获取 dragenter dragleave dragover drop
/* upload.addEventListener('dragenter', function () {
console.log('进入');
});
upload.addEventListener('dragleave', function () {
console.log('离开');
}); */
upload.addEventListener('dragover', function (ev) {
ev.preventDefault();
});
upload.addEventListener('dragover', function (ev) {
ev.preventDefault();
let file = ev.dataTransfer.files[0];
if (!file) return;
uploadFile(file);
});

这里主要是使用drop事件,这里注意需要禁止dragover drop默认事件。因为浏览器会默认打开文件
文件得获取为 ev.dataTransfer.files

后续得上传操作就没啥,和别的没啥大区别

7 大文件得切片上传和断点续传

断点续传是要基于切片上传
切片上传后,文件的合并时由服务端根据 文件的hash名来进行的
这个文件的hash 可以由客户端来做,当然后端来也是可以的,这里是客户端来做
每次上传之前。我们都需要先把需要上传的文件hash 名给后端。后端返回我们已经上传过的切片数组,然后我们固定大小或者固定数量来进行切片 。这里是把两者结合了
然后开始循环切片数组,发送请求之前 先判断是否在已上传过的数组里面,如果纯在的话我们就不需要进行请求
在所有的请求都完成后,也就是我们的进度变为100% 我们再告知客户端,可以进行切片的合并了

upload_inp.addEventListener('change', async function () {
let file = upload_inp.files[0];
if (!file) return;
upload_button_select.classList.add('loading');
upload_progress.style.display = 'block';

// 获取文件的HASH
let already = [],
data = null,
{
HASH,
suffix
} = await changeBuffer(file);

// 获取已经上传的切片信息
try {
data = await instance.get('/upload_already', {
params: {
HASH
}
});
if (+data.code === 0) {
already = data.fileList;
}
} catch (err) {}

// 实现文件切片处理 「固定数量 & 固定大小」
let max = 1024 * 100,
count = Math.ceil(file.size / max),
index = 0,
chunks = [];
if (count > 100) {
max = file.size / 100;
count = 100;
}
while (index < count) {
chunks.push({
file: file.slice(index * max, (index + 1) * max),
filename: `${HASH}_${index+1}.${suffix}`
});
index++;
}

// 上传成功的处理
index = 0;
const clear = () => {
upload_button_select.classList.remove('loading');
upload_progress.style.display = 'none';
upload_progress_value.style.width = '0%';
};
const complate = async () => {
// 管控进度条
index++;
upload_progress_value.style.width = `${index/count*100}%`;

// 当所有切片都上传成功,我们合并切片
if (index < count) return;
upload_progress_value.style.width = `100%`;
try {
data = await instance.post('/upload_merge', {
HASH,
count
}, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
if (+data.code === 0) {
alert(`恭喜您,文件上传成功,您可以基于 ${data.servicePath} 访问该文件~~`);
clear();
return;
}
throw data.codeText;
} catch (err) {
alert('切片合并失败,请您稍后再试~~');
clear();
}
};

// 把每一个切片都上传到服务器上
chunks.forEach(chunk => {
// 已经上传的无需在上传
if (already.length > 0 && already.includes(chunk.filename)) {
complate();
return;
}
let fm = new FormData;
fm.append('file', chunk.file);
fm.append('filename', chunk.filename);
instance.post('/upload_chunk', fm).then(data => {
if (+data.code === 0) {
complate();
return;
}
return Promise.reject(data.codeText);
}).catch(() => {
alert('当前切片上传失败,请您稍后再试~~');
clear();
});
});
});

 基本上就这么多了,主要是对文件的处理 以及几个获取文件的事件 思路基本上都差不多,很好理解

 

参考文章:http://blog.ncmem.com/wordpress/2023/09/25/%e6%96%87%e4%bb%b6%e4%b8%8a%e4%bc%a0-%e5%88%87%e7%89%87%e4%b8%8e%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0/

欢迎入群一起讨论

 

posted on 2023-09-25 09:12  Xproer-松鼠  阅读(14)  评论(0编辑  收藏  举报