图片上传
基于FORM-DATA实现文件上传
<input type="file" class="upload_inp" accept=".png,.jpg,.jpeg">
<div class="upload_button_box">
<button class="upload_button select">选择文件</button>
<button class="upload_button upload">上传到服务器</button>
</div>
.upload_box .upload_inp {
display: none;
}
点击button 触发input的change
upload_button_select.addEventListener('click', function () {
if (upload_button_select.classList.contains('disable') || upload_button_select.classList.contains('loading')) return;
upload_inp.click();
});
FORM-DATA 请求头的设置
instance.defaults.headers['Content-Type'] = 'multipart/form-data';
使用表单上传文件时,multipart/form-data不会对参数编码,请求体被分割成多部分,让服务器知道如何拆分它接受的参数。每部分使用 --boundary分割,相当于&,boundary的值是----Web**PLx。
监听上传文件
// 监听用户选择文件的操作
upload_inp.addEventListener('change', function () {
// 获取用户选中的文件对象
// + name:文件名
// + size:文件大小 B
// + type:文件的MIME类型
let file = upload_inp.files[0];
if (!file) return;
// 限制文件上传的格式「方案一」
/* if (!/(PNG|JPG|JPEG)/i.test(file.type)) {
alert('上传的文件只能是 PNG/JPG/JPEG 格式的~~');
return;
} */
// 限制文件上传的大小
if (file.size > 2 * 1024 * 1024) {
alert('上传的文件不能超过2MB~~');
return;
}
_file = file;
// 显示上传的文件
upload_tip.style.display = 'none';
upload_list.style.display = 'block';
upload_list.innerHTML = `<li>
<span>文件:${file.name}</span>
<span><em>移除</em></span>
</li>`;
});
上传文件到服务器
upload_button_upload.addEventListener('click', function () {
if (upload_button_upload.classList.contains('disable') || upload_button_upload.classList.contains('loading')) return;
if (!_file) {
alert('请您先选择要上传的文件~~');
return;
}
;
// 把文件传递给服务器:FormData / BASE64
// 请求头:instance.defaults.headers['Content-Type'] = 'multipart/form-data';
let formData = new FormData();
formData.append('file', _file);
formData.append('filename', _file.name);
instance.post('/upload_single', formData).then(data => {
if (+data.code === 0) {
alert(`文件已经上传成功~~,您可以基于 ${data.servicePath} 访问这个资源~~`);
return;
}
return Promise.reject(data.codeText);
}).catch(reason => {
alert('文件上传失败,请您稍后再试~~');
}).finally(() => {
});
});
完整代码
(function () {
let upload = document.querySelector('#upload1'),
upload_inp = upload.querySelector('.upload_inp'),
upload_button_select = upload.querySelector('.upload_button.select'),
upload_button_upload = upload.querySelector('.upload_button.upload')
// 上传文件到服务器
upload_button_upload.addEventListener('click', function () {
if (upload_button_upload.classList.contains('disable') || upload_button_upload.classList.contains('loading')) return;
if (!_file) {
alert('请您先选择要上传的文件~~');
return;
}
// 把文件传递给服务器:FormData / BASE64
// 请求头:instance.defaults.headers['Content-Type'] = 'multipart/form-data';
let formData = new FormData();
formData.append('file', _file);
formData.append('filename', _file.name);
instance.post('/upload_single', formData).then(data => {
if (+data.code === 0) {
alert(`文件已经上传成功~~,您可以基于 ${data.servicePath} 访问这个资源~~`);
return;
}
return Promise.reject(data.codeText);
}).catch(reason => {
alert('文件上传失败,请您稍后再试~~');
}).finally(() => {
});
});
// 监听用户选择文件的操作
upload_inp.addEventListener('change', function () {
// 获取用户选中的文件对象
// + name:文件名
// + size:文件大小 B
// + type:文件的MIME类型
let file = upload_inp.files[0];
if (!file) return;
// 限制文件上传的格式「方案一」
/* if (!/(PNG|JPG|JPEG)/i.test(file.type)) {
alert('上传的文件只能是 PNG/JPG/JPEG 格式的~~');
return;
} */
// 限制文件上传的大小
if (file.size > 2 * 1024 * 1024) {
alert('上传的文件不能超过2MB~~');
return;
}
_file = file;
// 显示上传的文件
upload_tip.style.display = 'none';
upload_list.style.display = 'block';
upload_list.innerHTML = `<li>
<span>文件:${file.name}</span>
<span><em>移除</em></span>
</li>`;
});
// 点击选择文件按钮,触发上传文件INPUT框选择文件的行为
upload_button_select.addEventListener('click', function () {
if (upload_button_select.classList.contains('disable') || upload_button_select.classList.contains('loading')) return;
upload_inp.click();
});
})();
base64上传
把选择的文件读取成为BASE64
const changeBASE64 = file => {
return new Promise(resolve => {
let fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = ev => {
resolve(ev.target.result);
};
});
};
上传文件
upload_inp.addEventListener('change', async function () {
let file = upload_inp.files[0],
BASE64,
data;
if (!file) return;
if (file.size > 2 * 1024 * 1024) {
alert('上传的文件不能超过2MB~~');
return;
}
upload_button_select.classList.add('loading');
BASE64 = await changeBASE64(file);
try {
data = await instance.post('/upload_single_base64', {
file: encodeURIComponent(BASE64),
filename: file.name
}, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
if (+data.code === 0) {
alert(`恭喜您,文件上传成功,您可以基于 ${data.servicePath} 地址去访问~~`);
return;
}
throw data.codeText;
} catch (err) {
alert('很遗憾,文件上传失败,请您稍后再试~~');
} finally {
upload_button_select.classList.remove('loading');
}
});
文件预览&生成文件hash(MD5)值避免同名不同图片覆盖
文件预览
文件预览,就是把文件对象转换为BASE64,赋值给图片的SRC属性即可
upload_inp.addEventListener('change', async function () {
let file = upload_inp.files[0],
BASE64;
if (!file) return;
BASE64 = await changeBASE64(file);
upload_abbre_img.src = BASE64;
});
// 把选择的文件读取成为BASE64
const changeBASE64 = file => {
return new Promise(resolve => {
let fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = ev => {
resolve(ev.target.result);
};
});
};
生成文件hash值(MD5)避免同名不同图片覆盖
使用SparkMD5。根据文件buffer格式生成hash,是根据内容生成hash值,不是根据名字生成
npm install --save spark-md5
把文件转为buffer,生成md5
const changeBuffer = file => {
return new Promise(resolve => {
let fileReader = new FileReader();
//文件转为buffer,
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}`
});
};
});
};
上传
upload_button_upload.addEventListener('click', async function () {
if (checkIsDisable(this)) return;
if (!_file) {
alert('请您先选择要上传的文件~~');
return;
}
changeDisable(true);
// 生成文件的HASH名字
let {
filename
} = await changeBuffer(_file);
let formData = new FormData();
formData.append('file', _file);
formData.append('filename', filename);
instance.post('/upload_single_name', formData).then(data => {
if (+data.code === 0) {
alert(`文件已经上传成功~~,您可以基于 ${data.servicePath} 访问这个资源~~`);
return;
}
return Promise.reject(data.codeText);
}).catch(reason => {
alert('文件上传失败,请您稍后再试~~');
}).finally(() => {
changeDisable(false);
upload_abbre.style.display = 'none';
upload_abbre_img.src = '';
_file = null;
});
});
完整代码
(function () {
let upload = document.querySelector('#upload3'),
upload_inp = upload.querySelector('.upload_inp'),
upload_button_select = upload.querySelector('.upload_button.select'),
upload_button_upload = upload.querySelector('.upload_button.upload'),
upload_abbre = upload.querySelector('.upload_abbre'),
upload_abbre_img = upload_abbre.querySelector('img');
let _file = null;
// 验证是否处于可操作性状态
const checkIsDisable = element => {
let classList = element.classList;
return classList.contains('disable') || classList.contains('loading');
};
// 把选择的文件读取成为BASE64
const changeBASE64 = file => {
return new Promise(resolve => {
let fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = ev => {
resolve(ev.target.result);
};
});
};
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}`
});
};
});
};
// 把文件上传到服务器
const changeDisable = flag => {
if (flag) {
upload_button_select.classList.add('disable');
upload_button_upload.classList.add('loading');
return;
}
upload_button_select.classList.remove('disable');
upload_button_upload.classList.remove('loading');
};
upload_button_upload.addEventListener('click', async function () {
if (checkIsDisable(this)) return;
if (!_file) {
alert('请您先选择要上传的文件~~');
return;
}
changeDisable(true);
// 生成文件的HASH名字
let {
filename
} = await changeBuffer(_file);
let formData = new FormData();
formData.append('file', _file);
formData.append('filename', filename);
instance.post('/upload_single_name', formData).then(data => {
if (+data.code === 0) {
alert(`文件已经上传成功~~,您可以基于 ${data.servicePath} 访问这个资源~~`);
return;
}
return Promise.reject(data.codeText);
}).catch(reason => {
alert('文件上传失败,请您稍后再试~~');
}).finally(() => {
changeDisable(false);
upload_abbre.style.display = 'none';
upload_abbre_img.src = '';
_file = null;
});
});
// 文件预览,就是把文件对象转换为BASE64,赋值给图片的SRC属性即可
upload_inp.addEventListener('change', async function () {
let file = upload_inp.files[0],
BASE64;
if (!file) return;
_file = file;
upload_button_select.classList.add('disable');
BASE64 = await changeBASE64(file);
upload_abbre.style.display = 'block';
upload_abbre_img.src = BASE64;
upload_button_select.classList.remove('disable');
});
upload_button_select.addEventListener('click', function () {
if (checkIsDisable(this)) return;
upload_inp.click();
});
})();
多文件上传
<input type="file" class="upload_inp" multiple>
<div class="upload_button_box">
<button class="upload_button select">选择文件</button>
<button class="upload_button upload">上传到服务器</button>
</div>
循环发送请求
upload_button_upload.addEventListener('click', async function () {
if (_files.length === 0) {
alert('请您先选择要上传的文件~~');
return;
}
// 循环发送请求
let upload_list_arr = Array.from(upload_list.querySelectorAll('li'));
_files = _files.map(item => {
let fm = new FormData;
fm.append('file', item.file);
fm.append('filename', item.filename);
return instance.post('/upload_single', fm, {
onUploadProgress(ev) {
// 检测每一个的上传进度
if (curSpan) {
curSpan.innerHTML = `${(ev.loaded/ev.total*100).toFixed(2)}%`;
}
}
}).then(data => {
if (+data.code === 0) {
if (curSpan) {
curSpan.innerHTML = `100%`;
}
return;
}
return Promise.reject();
});
});
// 等待所有处理的结果
Promise.all(_files).then(() => {
alert('恭喜您,所有文件都上传成功~~');
}).catch(() => {
alert('很遗憾,上传过程中出现问题,请您稍后再试~~');
}).finally(() => {
changeDisable(false);
_files = [];
upload_list.innerHTML = '';
upload_list.style.display = 'none';
});
});
上传进度条
instance.post('/upload_single', fm, {
onUploadProgress(ev) {
// 检测每一个的上传进度
if (curSpan) {
curSpan.innerHTML = `${(ev.loaded/ev.total*100).toFixed(2)}%`;
}
}
})
大文件的切片上传和断点续传
切片上传
思路:
- 获取已上传文件的分片信息,服务端返回文件分片数量,每个分片大小和文件唯一的 fileKey(断点续传基于这个实现)
- 大文件切片,分别上传服务端
- 结算文件分片,后端会将各分片拼接成一个完整的文件并返回
获取分片信息
let file = upload_inp.files[0];
if (!file) return;
// 获取文件的HASH
let already = [],
data = null,
{
HASH,
suffix
} = await changeBuffer(file);
// 1.获取已经上传的切片信息
try {
data = await instance.get('/upload_already', {
params: {
HASH
}
});
if (+data.code === 0) {
already = data.fileList;
}
} catch (err) {}
实现文件切片处理
// 「固定数量 & 固定大小」,1024=1kb
let max = 1024 * 100,
//多少切片
count = Math.ceil(file.size / max),
index = 0,
chunks = [];
//超过限定100个切片,规定每个切片的大小
if (count > 100) {
max = file.size / 100;
count = 100;
}
while (index < count) {
//用file上的slice方法切
//file.slice(0,1024),file.slice(1024,2048)
chunks.push({
file: file.slice(index * max, (index + 1) * max),
filename: `${HASH}_${index+1}.${suffix}`
});
index++;
}
把每个切片上传到服务器
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();
});
});
上传成功的处理
结算文件分片,后端会将各分片拼接成一个完整的文件并返回
const complate = async () => {
// 管控进度条
index++;//传成功1个切片加1
upload_progress_value.style.width = `${index/count*100}%`;
// 3.当所有切片都上传成功,我们合并切片
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();
}
};
断点续传
定义:假设一个文件被切为10片,当上传3片的时候,停止了,下一次再上传的时候,请求服务器,已经上传成功的切片有3片,然后就从第七片开始继续传,不需要再从新开始传完整的10片。
获取分片信息
let file = upload_inp.files[0];
if (!file) return;
// 获取文件的HASH
let already = [],
data = null,
{
HASH,
suffix
} = await changeBuffer(file);
// 1.获取已经上传的切片信息
try {
data = await instance.get('/upload_already', {
params: {
HASH
}
});
if (+data.code === 0) {
already = data.fileList;
}
} catch (err) {}
切片上传时片判断是否已传过
chunks.forEach(chunk => {
// 已经上传的无需在上传
if (already.length > 0 && already.includes(chunk.filename)) {
complate();
return;
}
...
});
完整代码
(function () {
let upload = document.querySelector('#upload7'),
upload_inp = upload.querySelector('.upload_inp'),
upload_button_select = upload.querySelector('.upload_button.select'),
upload_progress = upload.querySelector('.upload_progress'),
upload_progress_value = upload_progress.querySelector('.value');
const checkIsDisable = element => {
let classList = element.classList;
return classList.contains('disable') || classList.contains('loading');
};
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}`
});
};
});
};
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);
// 1.获取已经上传的切片信息
try {
data = await instance.get('/upload_already', {
params: {
HASH
}
});
if (+data.code === 0) {
already = data.fileList;
}
} catch (err) {}
// 2.实现文件切片处理
// 「固定数量 & 固定大小」,1024=1kb
let max = 1024 * 100,
count = Math.ceil(file.size / max),//多少切片
index = 0,
chunks = [];
if (count > 100) {//超过限定100个切片,
max = file.size / 100;
count = 100;
}
while (index < count) {
//用file上的slice方法切
//file.slice(0,1024),file.slice(1024,2048)
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++;//传成功1个切片加1
upload_progress_value.style.width = `${index/count*100}%`;
// 3.当所有切片都上传成功,我们合并切片
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();
});
});
});
upload_button_select.addEventListener('click', function () {
if (checkIsDisable(this)) return;
upload_inp.click();
});
})();