设置axios的统一配置信息
let instance = axios.create({
baseURL: "http://127.0.0.1:8888",
headers: { "Content-Type": "multipart/form-data" },
transformRequest: (data, headers) => {
// data就是请求主体数据
// headers就是设置的请求头Content-Type
if (headers["Content-Type"] === "application/x-www-form-urlencoded") {
// 是该请求头就将请求主体的对象格式转换为url格式
return Qs.stringify(data);
}
// 否则不进行处理
return data;
},
});
//上面代码等价于
// axios.defaults.baseURL = "http://127.0.0.1:8888";
// // 默认传输文件的请求头
// axios.defaults.headers["Content-Type"] = "mutipart/form-data";
// // `transformRequest` 允许在向服务器发送前,修改请求数据
// axios.defaults.transformRequest = (data, headers) => {
// // data就是请求主体数据
// // headers就是设置的请求头Content-Type
// if (headers["Content-Type"] === "application/x-www-form-urlencoded") {
// // 是该请求头就将请求主体的对象格式转换为url格式
// return Qs.stringify(data);
// }
// // 否则不进行处理
// return data;
// };
instance.interceptors.response.use(
function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data;
},
function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
}
);
FormData上传
文件上次使用的是input
标签中的type=file
属性,但是原生的文件按钮工具难以设置样式等,所以都将原生的文件工具隐藏,自定义一个按钮,在点击自定义按钮的时候触发文件按钮。
第一个文件上次案例的结构代码如下
<div class="item">
<h3>单一文件上传「FORM-DATA」</h3>
<section class="upload_box" id="upload1">
<!-- accept=".png" 限定上传文件的格式 -->
<input type="file" class="upload_inp" />
<div class="upload_button_box">
<button class="upload_button select">选择文件</button>
<button class="upload_button upload">上传到服务器</button>
</div>
<div class="upload_tip">
只能上传 PNG/JPG/JPEG 格式图片,且大小不能超过2MB
</div>
<ul class="upload_list">
<!-- <li>
<span>文件:...</span>
<span><em>移除</em></span>
</li> -->
</ul>
</section>
</div>
通过闭包处理不同案例的文件上次代码,以下基本代码中,提供了如何获取选取的文件信息。通过文件元素身上的files
属性获取,该值是一个类数组
(function () {
let upload1 = document.querySelector("#upload1"),
upload_inp = upload1.querySelector(".upload_inp"),
upload_inp_select = upload1.querySelector(".upload_button.select"),
upload_inp_upload = upload1.querySelector(".upload_button.upload"),
upload_tip = upload1.querySelector(".upload_tip"),
upload_list = upload1.querySelector(".upload_list");
// 给选择文件按钮绑定时间事件
upload_inp_select.addEventListener("click", () => {
// 触发自带的点击方法选取文件
upload_inp.click();
});
upload_inp.addEventListener("change", () => {
// 文件选取需要基于change事件完成
console.log(upload_inp.files);
//文件校验等
});
})();
如上图所示,一个文件对象包含的熟悉中type
对应文件的类型,size
单位是字节对于文件的大小,name
对象文件的名字,这些都可以作为文件校验的工具使用。
// 校验文件的类型和大小
let file = upload_inp.files[0];
if (!/(png|jpe?g)/i.test(file.type)) {
alert("请选择png或者jpg或者jpeg格式的图片");
return;
}
if (file.size > 2 * 1024 * 1024) {
alert("图片大小不能超过2M");
return;
}
当然也可以使用file
文件标签类型提供的accept
属性自定义设置accept=".png,.jpg,.jpeg"
当选取文件的校验通过后,需要设置已选取文件的列表显示。和删除已选确定文件列表。因为删除按钮设计到元素的动态添加和删除,因此采用事件委托的方式处理。
upload_inp.addEventListener("change", () => {
....
upload_tip.style.display = "none";
upload_list.style.display = "block";
upload_list.innerHTML = `
<li>
<span>文件:${file.name}</span>
<span><em>移除</em></span>
</li>
`;
});
upload_list.addEventListener("click", (e) => {
// 点击的是文件的删除按钮
if (e.target.tagName === "EM") {
// 显示提示
upload_tip.style.display = "block";
// 删除文件列表显示
upload_list.style.display = "none";
upload_list.innerHTML = ``;
}
});
然后针对文件上传按钮进行处理
// 全局创建file变量
let _file = null;
upload_inp.addEventListener("change", () => {
// 文件选取需要基于change事件完成
let file = upload_inp.files[0];
_file = file;
.....
}
upload_list.addEventListener("click", (e) => {
// 点击的是文件的删除按钮
if (e.target.tagName === "EM") {
// 移除文件了删除公共状态中保存的file对象
_file = null;
.....
}
});
// 文件上传按钮
upload_inp_upload.addEventListener("click", async () => {
// 需要获取文件对象
// 当没有选择文件的时候或者选择了文件又删除的情况下,不允许上传,_file=null
if (!_file) {
alert("请选择文件");
return;
}
let fm = new FormData();
// 后端服务器需要字段file和filename
fm.append("file", _file);
fm.append("filename", _file.name);
try {
let { code, servicePath } = await instance.post("/upload_single", fm);
if (+code === 0) {
alert(`上传成功:文件服务器地址${servicePath}`);
return;
}
} catch (error) {
console.log(error);
alert("请重新上传!");
}
});
出现如图代表成功,这样子上传的文件都会出现在服务端的upload文件夹,然后上传文件成功之后,需要做一些事情,比如将上传的文件列表清空等。最主要的是给正在上传中的按钮设置样式,还有节流防抖效果
提取公共的代码部分
const clearHandle = () => {
// 移除文件了删除公共状态中保存的file对象
_file = null;
// 显示提示
upload_tip.style.display = "block";
// 删除文件列表显示
upload_list.style.display = "none";
upload_list.innerHTML = ``;
};
单独编写处理一个样式类的代码
const handleStyle = (flag) => {
// flag为真代表选择文件按钮需要添加disable样式,上传服务器按钮需要添加loading样式
if (flag) {
upload_inp_select.classList.add("disable");
upload_inp_upload.classList.add("loading");
} else {
upload_inp_select.classList.remove("disable");
upload_inp_upload.classList.remove("loading");
}
};
upload_inp_upload.addEventListener("click", async () => {
// 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
if (
upload_inp_select.classList.contains("disable") ||
upload_inp_upload.classList.contains("loading")
) {
return;
}
// 需要获取文件对象
// 当没有选择文件的时候或者选择了文件又删除的情况下,不允许上传,_file=null
if (!_file) {
alert("请选择文件");
return;
}
// 设置按钮样式
handleStyle(true);
let fm = new FormData();
// 后端服务器需要字段file和filename
fm.append("file", _file);
fm.append("filename", _file.name);
try {
let { code, servicePath } = await instance.post("/upload_single", fm);
if (+code === 0) {
alert(`上传成功:文件服务器地址${servicePath}`);
return;
}
} catch (error) {
console.log(error);
alert("请重新上传!");
} finally {
// 统一调用公共逻辑代码部分
clearHandle();
// 设置按钮样式
handleStyle(false);
}
});
其次选择文件按钮还需要做节流防抖处理
upload_inp_select.addEventListener("click", () => {
// 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
if (
upload_inp_select.classList.contains("disable") ||
upload_inp_upload.classList.contains("loading")
) {
return;
}
// 触发自带的点击方法选取文件
upload_inp.click();
});
Base64上传
<div class="item">
<h3>单一文件上传「BASE64」,只适合图片</h3>
<section class="upload_box" id="upload2">
<input type="file" class="upload_inp" accept=".jpg,.jpeg,.png" />
<div class="upload_button_box">
<button class="upload_button select">上传图片</button>
</div>
<div class="upload_tip">
只能上传jpg/png格式图片,且大小不能超过2mb
</div>
</section>
</div>
基本的js代码
// Base64文件上传
(function () {
let upload2 = document.querySelector("#upload2"),
upload_inp = upload2.querySelector(".upload_inp"),
upload_inp_select = upload2.querySelector(".upload_button.select");
let _file = null;
const isGo = (ele) => {
return (
ele.classList.contains("disable") || ele.classList.contains("loading")
);
};
// 给选择文件按钮绑定时间事件
upload_inp_select.addEventListener("click", function () {
// 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
if (isGo(this)) return;
// 触发自带的点击方法选取文件
upload_inp.click();
});
upload_inp.addEventListener("change", () => {
// 文件选取需要基于change事件完成
let file = upload_inp.files[0];
_file = file;
if (!/(png|jpe?g)/i.test(file.type)) {
alert("请选择png或者jpg或者jpeg格式的图片");
return;
}
if (file.size > 2 * 1024 * 1024) {
alert("图片大小不能超过2M");
return;
}
});
})();
将文件转换为Base64借助FileReader
实例创建。然后通过实例身上的readAsDataURL(文件)
方法异步创建base64文件,该实例身上还有其他方法可以创建二进制等文件信息。区分URL.createObjectURL(blob)
可以获取当前文件的一个内存URL。FileReader
创建文件操作是异步的,如果下面代码底部有一个同步输出语句,希望文件操作输出base64文件后再输出,会发现结果和预想的不一样,会先输出111后输出文件内容。因此大部分时候,都是将FileReader
有关的封装再一个promise函数中使用。
let reader = new FileReader();
reader.readAsDataURL(file); //异步操作
reader.onload = (res) => {
//需要基于load事件获取异步操作的结果
console.log(res); // base64信息存储在:结果.target.result中保存
};
console.log('1111')
const changeBase64 = (file) => {
return new Promise((resolve) => {
let reader = new FileReader();
reader.readAsDataURL(file); //异步操作
reader.onload = (res) => {
//需要基于load事件获取异步操作的结果
resolve(res.target.result);
};
});
};
upload_inp.addEventListener("change", async () => {
....
let base64 = await changeBase64(file);
console.log(base64);
});
以下是点击按钮时候处理base64文件的部分代码,其中用到了encodeURIComponent()
用于将一些特殊字符进行转码如#,$
等,然后客户端采用decodeURIComponent()
进行解码处理
upload_inp.addEventListener("change", async () => {
// 文件选取需要基于change事件完成
let file = upload_inp.files[0];
_file = file;
if (!/(png|jpe?g)/i.test(file.type)) {
alert("请选择png或者jpg或者jpeg格式的图片");
return;
}
if (file.size > 2 * 1024 * 1024) {
alert("图片大小不能超过2M");
return;
}
// 设置按钮样式
upload_inp_select.classList.add("loading");
let base64 = await changeBase64(file);
try {
let { code, servicePath, codeText } = await instance.post(
"/upload_single_base64",
{
//将特殊字符进行编码,主要服务端进行了解码处理,因此这里需要进行处理
file: encodeURIComponent(base64),
filename: _file.name,
},
{
// 服务器对base64的Content-Type类型进行了限制,需要是urlencodede
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
if (+code === 0) {
alert(`上传成功:${servicePath}`);
return;
}
throw codeText;
} catch (error) {
console.log(error);
alert("请重新上传!");
} finally {
// 移除样式类
upload_inp_select.classList.remove("loading");
}
});
在针对base64的接口中,后台进行了处理,如果上传的文件是同一个,则不会进行重复上传(这里不仅是文件名一样,包括文件的内在属性,例如一个文件拷贝一份,两个文件之间只有文件名不同,服务器依旧能区分是同一个文件,从而不进行处理。)以下是后台处理
// 单文件上传处理「BASE64」
app.post("/upload_single_base64", async (req, res) => {
let file = req.body.file,
filename = req.body.filename,
// SparkMD5库会根据文件的内容生成hash名字,如果文件内容一致则不再处理
spark = new SparkMD5.ArrayBuffer(),
// 获取文件后缀名
suffix = /\.([0-9a-zA-Z]+)$/.exec(filename)[1],
isExists = false,
path;
// 服务端解码
file = decodeURIComponent(file);
// 将base64的格式去掉
file = file.replace(/^data:image\/\w+;base64,/, "");
// Node内置模块,指定编码格式为base64,将file字符串转换为Buffer
file = Buffer.from(file, "base64");
// 将Buffer写入spark中,之后根据文件的内容生成名字
spark.append(file);
// spark.end()用于生成hash名字
path = `${uploadDir}/${spark.end()}.${suffix}`;
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);
});
文件上传缩略图&客户端自己生成文件hash名
在之前的文件上传中,文件传给服务器后,都是由服务器处理文件名,而这次是由客户端处理文件名同时添加客户端图片预览效果。
<div class="item">
<h3>单一文件上传「缩略图处理」</h3>
<section class="upload_box" id="upload3">
<input type="file" class="upload_inp" accept=".jpg,.jpeg,.png" />
<div class="upload_button_box">
<button class="upload_button select">选择文件</button>
<button class="upload_button upload">上传到服务器</button>
</div>
<div class="upload_abbre">
<img src="" alt="" />
</div>
</section>
</div>
</div>
基础js代码
(function () {
let upload3 = document.querySelector("#upload3"),
upload_inp = upload3.querySelector(".upload_inp"),
upload_inp_select = upload3.querySelector(".upload_button.select"),
upload_inp_upload = upload3.querySelector(".upload_button.upload"),
upload_abbre = upload3.querySelector(".upload_abbre"),
upload_abbre_img = upload_abbre.querySelector("img");
let _file = null;
const isGo = (ele) => {
return (
ele.classList.contains("disable") || ele.classList.contains("loading")
);
};
// 给选择文件按钮绑定时间事件
upload_inp_select.addEventListener("click", function () {
// 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
if (isGo(this)) return;
// 触发自带的点击方法选取文件
upload_inp.click();
});
const changeBase64 = (file) => {
return new Promise((resolve) => {
let reader = new FileReader();
reader.readAsDataURL(file); //异步操作
reader.onload = (res) => {
//需要基于load事件获取异步操作的结果
resolve(res.target.result);
};
});
};
upload_inp.addEventListener("change", async () => {
// 文件选取需要基于change事件完成
let file = upload_inp.files[0];
_file = file;
if (!/(png|jpe?g)/i.test(file.type)) {
alert("请选择png或者jpg或者jpeg格式的图片");
return;
}
// 设置按钮样式
upload_inp_select.classList.add("disable");
let base64 = await changeBase64(file); //图片本地预览采用base64格式
upload_abbre.style.display = "block";
upload_abbre_img.src = base64;
upload_inp_select.classList.remove("disable");
});
})();
图片预览成功后就需要处理上传服务器,将之前的代码复制修改使用。经过测试就会发现一个问题,如果自己在客户端没有处理文件名,那么有两个文件,仅仅是文件名相同,文件内容完全不同,上传服务器,服务器会提示已经存在该文件了,只会保留旧的文件使用,这是预想的不一样,本意是上次的文件都保留。(也可以新的覆盖旧的)
const handleStyle = (flag) => {
// flag为真代表选择文件按钮需要添加disable样式,上传服务器按钮需要添加loading样式
if (flag) {
upload_inp_select.classList.add("disable");
upload_inp_upload.classList.add("loading");
} else {
upload_inp_select.classList.remove("disable");
upload_inp_upload.classList.remove("loading");
}
};
// 文件上传按钮
upload_inp_upload.addEventListener("click", async () => {
// 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
if (isGo(upload_inp_upload)) return;
// 需要获取文件对象
// 当没有选择文件的时候或者选择了文件又删除的情况下,不允许上传,_file=null
if (!_file) {
alert("请选择文件");
return;
}
// 设置按钮样式
handleStyle(true);
let fm = new FormData();
// 后端服务器需要字段file和filename
fm.append("file", _file);
fm.append("filename", _file.name); // 未自己处理生成hash文件名
try {
let { code, servicePath } = await instance.post(
"/upload_single_name",
fm
);
if (+code === 0) {
alert(`上传成功:文件服务器地址${servicePath}`);
return;
}
} catch (error) {
console.log(error);
alert("请重新上传!");
} finally {
// 设置按钮样式
handleStyle(false);
}
});
在客户端需要借助spark-md5
库处理文件名的问题
编写一个函数,处理生成hash名,然后再上传按钮中调用,只不过在这里的函数中,调用FileReader
实例身上的readAsArrayBuffer(文件)
方法,生成一段文件的buffer信息,该信息传入spark-md5
提供的函数中可以根据文件内容生成名字
// 处理hash名字
const changeBuffer = (file) => {
return new Promise((resolve) => {
let reader = new FileReader();
reader.readAsArrayBuffer(file); //异步操作
reader.onload = (res) => {
//需要基于load事件获取异步操作的结果
let buffer = res.target.result;
console.log(buffer); // 查看
resolve();
};
});
};
继续添加如下代码,实例化new SparkMD5.ArrayBuffer()
该插件提供的方法,然后调用append
方法将刚才生成的文件二进制信息放进去,最后调用end
方法生成hash名。如果两个文件是一模一样的(文件内容)则生成的hash文件名是一样的。
以下代码用于生成文件的名字,但是文件的后缀名还没有处理,文件的后缀可以根据传入的文件信息中的name字段进行提取
let spark = new SparkMD5.ArrayBuffer(); //创建md5对象,用于生成hash值
spark.append(buffer);
let hash = spark.end(); //自动根据文件二进制内容生成名字
console.log(hash);
编写一段正则,然后通过exec
方法进行搜索匹配,返回的是一个数组,数组中会返回匹配的结果
let suffix = /\.[a-zA-Z0-9]+/.exec(file.name); // +代表一个或多个,文件的后缀必须存在
console.log(suffix);
完整处理如下
const changeBuffer = (file) => {
return new Promise((resolve) => {
let reader = new FileReader();
reader.readAsArrayBuffer(file); //异步操作
reader.onload = (res) => {
//需要基于load事件获取异步操作的结果
let buffer = res.target.result;
let spark = new SparkMD5.ArrayBuffer(); //创建md5对象,用于生成hash值
spark.append(buffer);
let hash = spark.end(); //自动根据文件二进制内容生成名字
let suffix = /\.[a-zA-Z0-9]+/.exec(file.name)[0]; // +代表一个或多个,文件的后缀必须存在
resolve({
buffer,
hash,
suffix,
filename: `${hash}${suffix}`, //手动拼接文件和后缀
});
};
});
};
然后再上传服务器按钮中修改部分代码
let { filename } = await changeBuffer(_file);
fm.append("filename", filename); // 手动处理后的文件名
finally {
// 设置按钮样式
handleStyle(false);
upload_abbre.style.display = "none"; // 清除缩略图等
upload_abbre_img.src = "";
_file = null;
}
这样子就完成的缩略图在客户端手动处理文件名上传服务器了。
单文件上传进度管理
<div class="item">
<h3>单一文件上传「进度管控」</h3>
<section class="upload_box" id="upload4">
<input type="file" class="upload_inp" />
<div class="upload_button_box">
<button class="upload_button select">上传文件</button>
</div>
<div class="upload_progress">
/* 通过指定value进度条的width宽度属性设置进度条 */
<div class="value"></div>
</div>
</section>
</div>
// 进度条管控
(function () {
let upload4 = document.querySelector("#upload4"),
upload_inp = upload4.querySelector(".upload_inp"),
upload_inp_select = upload4.querySelector(".upload_button.select"),
upload_progress = upload4.querySelector(".upload_progress"),
upload_progress_value = upload_progress.querySelector(".value");
let _file = null;
const isGo = (ele) => {
return (
ele.classList.contains("disable") || ele.classList.contains("loading")
);
};
// 给选择文件按钮绑定时间事件
upload_inp_select.addEventListener("click", async function () {
// 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
if (isGo(this)) return;
// 触发自带的点击方法选取文件
upload_inp.click();
});
upload_inp.addEventListener("change", async () => {
// 文件选取需要基于change事件完成
let file = upload_inp.files[0];
_file = file;
if (!/(png|jpe?g)/i.test(file.type)) {
alert("请选择png或者jpg或者jpeg格式的图片");
return;
}
// 设置按钮样式
upload_inp_select.classList.add("loading");
try {
let fm = new FormData();
fm.append("file", _file);
fm.append("filename", _file.name);
let { code, codeText, servicePath } = await instance.post(
"/upload_single",
fm
);
if (+code === 0) {
alert("上传成功:文件服务器地址" + servicePath);
return;
}
throw codeText;
} catch (error) {
console.log(error);
} finally {
upload_inp_select.classList.remove("loading");
}
});
})();
目前功能就是上次一个FormData文件,处理完成后接着处理进度条问题
这里需要了解在axios中的请求配置中提供了 onUploadProgress: function (progressEvent) {// 处理原生进度事件},
方法处理进度条事件。其本质是基于XHR中的属性(xhr.upload.onprogress
)完成的。
将请求部分的代码修改如下,查看进度条事件对象提供的属性,其中loaded
代表当前进度,total
代表当前进度总数,通过计算比例设置进度条样式
let { code, codeText, servicePath } = await instance.post(
"/upload_single",
fm,
{
onUploadProgress: (e) => {
console.log(e);
},
}
);
完善后代码如下,基本效果进度条递增是完成了。但是存在一个小bug,在代码的code===0中,针对服务器返回结果了就代表上传成功,于是我们设置百分百进度,但是样式中针对width变化设置了300毫秒的过度transition,但是alert会阻塞页面渲染,因此在过度动画过程中,如果没有完成的时候,alert弹出,则过度不会继续执行停在那里。
upload_inp.addEventListener("change", async () => {
// 文件选取需要基于change事件完成
let file = upload_inp.files[0];
_file = file;
// 设置按钮样式
upload_inp_select.classList.add("loading");
try {
let fm = new FormData();
fm.append("file", _file);
fm.append("filename", _file.name);
let { code, codeText, servicePath } = await instance.post(
"/upload_single",
fm,
{
onUploadProgress: (e) => {
let { total, loaded } = e; //当前进度总数和进度现有位置
upload_progress.style.display = "block"; //开启显示容器
upload_progress_value.style.width = `${(loaded / total) * 100}%`;
},
}
);
if (+code === 0) {
upload_progress_value.style.width = `100%`; //成功直接显示100%
alert("上传成功:文件服务器地址" + servicePath);
return;
}
throw codeText;
} catch (error) {
console.log(error);
} finally {
upload_inp_select.classList.remove("loading");
upload_progress.style.display = "none";
upload_progress_value.style.width = 0;
}
});
解决方法在两者之间写一个延迟函数,确保过度执行完毕了再往下执行。
const delay = (time) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, time);
});
};
upload_progress_value.style.width = `100%`; //成功直接显示100%
await delay(300);
alert(`上传成功:文件服务器地址${servicePath}`);
多文件上传&进度条控制
多文件上传第一步,给标签设置multiple
属性
<input type="file" class="upload_inp" multiple />
<div class="item">
<h3>多文件上传</h3>
<section class="upload_box" id="upload5">
<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>
<ul class="upload_list">
<!-- <li key='xx'>
<span>文件:xxxxx</span>
<span><em>移除</em></span>
</li> -->
</ul>
</section>
</div>
(function () {
let upload = document.querySelector("#upload5"),
upload_inp = upload.querySelector(".upload_inp"),
upload_inp_select = upload.querySelector(".upload_button.select"),
upload_inp_upload = upload.querySelector(".upload_button.upload"),
upload_list = upload.querySelector(".upload_list");
let _files = [];
const isGo = (ele) => {
return (
ele.classList.contains("disable") || ele.classList.contains("loading")
);
};
const handleStyle = (flag) => {
// flag为真代表选择文件按钮需要添加disable样式,上传服务器按钮需要添加loading样式
if (flag) {
upload_inp_select.classList.add("disable");
upload_inp_upload.classList.add("loading");
} else {
upload_inp_select.classList.remove("disable");
upload_inp_upload.classList.remove("loading");
}
};
// 给选择文件按钮绑定时间事件
upload_inp_select.addEventListener("click", function () {
// 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
if (isGo(this)) return;
// 触发自带的点击方法选取文件
upload_inp.click();
});
upload_inp.addEventListener("change", async () => {
// 文件选取需要基于change事件完成
_files = Array.from(upload_inp.files); //FileList 类数组
// 显示已选文件
let str = "";
_files.forEach((item, index) => {
str += `
<li>
<span>文件:${item.name}</span>
<span><em>移除</em></span>
</li>
`;
});
// 插入页面
upload_list.style.display = "block";
upload_list.innerHTML = str;
});
})();
然后就是处理每一个文件的删除按钮逻辑,设计多个元素的动态添加,使用事件委托处理。但是如下代码仅仅是删除了结构样式,保存的file文件中的数据并没有删除,因此还需要删除文件中的数据。
// 删除按钮的逻辑
upload_list.addEventListener("click", (e) => {
let target = e.target;
let curLi = null;
if (target.tagName === "EM") {
// 从结构中删除该元素的整体内容(删除li节点及其子节点)
let curLi = target.parentNode.parentNode;
if (!curLi) return; //不存在直接退出,防止出错
upload_list.removeChild(curLi);
console.log(_files);
}
});
这个时候可以换位思考,给li元素绑定一个唯一标识,每次获取的时候拿着该标识到file数组中进行查找,这里又涉及到了自定义设置的标识如何在file数组中查找,这里可以重构file数组。重构完file数组后,需要修改原forEach中的代码
// 生成唯一id函数,利用时间戳+随机数
const createRandom = () => {
let key = Date.now() * Math.random(); //因为每次的数都足够大都会带小数点不用后面进行判断
return String(key.toString(16)).replace(".", ""); // 转换为16进制后从数字转换为字符串,然后将点去除
};
_files = Array.from(upload_inp.files); //FileList 类数组
// 重构数组,并设置唯一标识
_files = _files.map((file, index) => {
return {
file,
filename: file.name,
key: createRandom(),
};
});
console.log(_files);
// 删除按钮的逻辑
upload_list.addEventListener("click", (e) => {
let target = e.target;
let curLi = null;
if (target.tagName === "EM") {
// 从结构中删除该元素的整体内容(删除li节点及其子节点)
let curLi = target.parentNode.parentNode;
if (!curLi) return; //不存在直接退出,防止出错
upload_list.removeChild(curLi);
// 删除数据
let key = curLi.getAttribute("key"); //通过自定义属性获取唯一标识
_files = _files.filter((file) => {
return key !== file.key;
});
if (_files.length === 0) {
upload_list.style.display = "none";
upload_list.innerHTML = "";
}
}
});
这里不推荐使用forEach中提供的index作为li的索引因为:索引赋值给元素后就固定了,当删除的时候,用索引去file数组中查找的时候会出现问题。
然后处理提交服务器按钮的代码
// 服务器上传按钮
upload_inp_upload.addEventListener("click", () => {
if (isGo(upload_inp_upload)) return;
if (_files.length === 0) {
alert("请先选择文件");
return;
}
handleStyle(true);
// 获取所有的li元素,已经开始上传服务器,代表li元素存在
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);
// 进行当前li元素匹配,返回li元素
let curli = upload_list_arr.find((li) => {
return li.getAttribute("key") === item.key;
});
// 获取该li元素下的最后一个span元素
let curspan = curli?.querySelector("span:last-child");
return instance
.post("/upload_single", fm, {
// 给每一个文件设置进度条样式
onUploadProgress(ev) {
/* 这里设计让li元素的最后一个span元素中的内容变为进度百分比显示即可,因此在这里需要获取对于元素
只需要获取到当前的li元素即可,因为li元素身上绑定了唯一标识,因此可以获取所有的li元素和当map中item.key进行匹配
<li key='xx'>
<span>文件:xxxxx</span>
<span>进度显示:10%</span>
</li>
*/
if (curspan) {
curspan.innerHTML = `进度显示:${(
(ev.loaded / ev.total) *
100
).toFixed(2)}%`;
}
},
})
.then((res) => {
if (+res.code === 0) {
if (curspan) {
curspan.innerHTML = `进度显示:100%`;
return;
}
}
Promise.reject(res.codeText);
});
});
Promise.all(_files) //只要有一个请求失败了整体失败
.then((res) => {
alert("上传成功");
})
.catch((err) => {
console.log(err);
})
.finally(() => {
handleStyle(false);
_files = [];
upload_list.style.display = "none";
upload_list.innerHTML = "";
});
});
文件拖拽上传
<div class="item">
<h3>拖拽上传</h3>
<section class="upload_box" id="upload6">
<input type="file" class="upload_inp" />
<div class="upload_drag">
<i class="icon"></i>
<span class="text"
>将文件拖到此处,或<a href="javascript:;" class="upload_submit"
>点击上传</a
></span
>
</div>
<div class="upload_mark">正在上传中,请稍等...</div>
</section>
</div>
// 拖拽上传
(function () {
let upload = document.querySelector("#upload6"),
upload_inp = upload.querySelector(".upload_inp"),
upload_submit = upload.querySelector(".upload_submit"),
upload_mark = upload.querySelector(".upload_mark");
let _file = null;
const isGo = (ele) => {
return (
ele.classList.contains("disable") || ele.classList.contains("loading")
);
};
// 给选择文件按钮绑定时间事件
upload_submit.addEventListener("click", async function () {
// 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
if (isGo(this)) return;
// 触发自带的点击方法选取文件
upload_inp.click();
});
upload_inp.addEventListener("change", async () => {
// 文件选取需要基于change事件完成
_file = upload_inp.files[0];
console.log(_file);
});
})();
拖拽事件
dragenter
:拖拽的文件进入指定的容器触发
dragleave
:拖拽的文件离开了指定的容器触发
dragover
:拖拽的文件在指定的容器内会一直触发(因此需要进行节流防抖处理)
drop
:拖拽的文件放置到指定的容器中触发
但是任意一个文件,拖拽到浏览器页面中都会进入默认的预览,因此我们需要将默认行为取消
在这个项目中,前面两个事件用不到可以删除,代码主体基本在drop
事件中
// 容器添加拖拽事件
upload.addEventListener("dragenter", (e) => {
console.log("进入了");
});
upload.addEventListener("dragleave", (e) => {
console.log("离开了");
});
upload.addEventListener("dragover", (e) => {
e.preventDefault();
console.log("在里面了移动了");
});
upload.addEventListener("drop", (e) => {
e.preventDefault();
console.log("放置了");
});
在drop
事件中,最终事件对象身上的event.dataTransfer.files
字段保存拖拽上传的文件信息。
因为这里两个地方的逻辑代码上传服务器是一样的,因此封装为一个函数处理
let isRun = false; //节流防抖标志,true代表节流防抖,代表正在运行
// 上传文件方法
const uploadFile = async (file) => {
if (isRun) return;
isRun = true;
// 将遮罩层启动
upload_mark.style.display = "block";
try {
let fm = new FormData();
fm.append("file", file);
fm.append("filename", file.name);
let { code, codeText, servicePath } = await instance.post(
"/upload_single",
fm
);
if (+code === 0) {
alert(`上传成功:文件服务器地址${servicePath}`);
return;
}
throw codeText;
} catch (error) {
console.log(error);
} finally {
// 清除默认样式类
upload_mark.style.display = "none";
isRun = false;
}
};
upload.addEventListener("drop", (e) => {
e.preventDefault();
let file = e.dataTransfer.files[0];
uploadFile(file);
});
upload_inp.addEventListener("change", async () => {
// 文件选取需要基于change事件完成
_file = upload_inp.files[0];
uploadFile(_file);
});
切片上传
项目中如果存在一个很大体积的文件,如果不做任何处理直接上传给服务器,会造成服务器压力过大,且如果传输过程中失败了,还需要重新将文件上传。于是采用了切片上传,切片上传的思路就是在客户端选取文件即将上传的时候,将文件切成一个一个小切片进行传送,在服务端中,每次接受到一个切片后就将切片保存在一个地方收集,当客户端切片传送结束的时候,给服务端再次传送一个结束标志,服务端接受到该标志后就将刚才收集的切片进行组合,最终合并为一个文件。基于切片上传有一个好处是可以实现断点续传。 断点续传第一种是服务端提供接口,客户端切片上传部分后失败了,服务端会保存部分切片,然后客户端重新请求的时候会调用该接口查看已有服务端切片,然后存在的切片就不会继续上传了。第二中是不提供接口,客户端每次切片传送的时候,在服务端都进行校验查看该切片是否存在。
那么这么多切片如何判断哪些切片是一组的,并且切片之间的顺序是如何组合的。在客户端,可以基于SparkMD5
库完成,该库中如果是同一个文件,那么生成的hash文件名是一样的,那么我们的切片可以是这种格式:hash_1,hash_2 ...
这种格式的,同一个文件的hash是一样的。
<div class="item">
<h3>大文件上传</h3>
<section class="upload_box" id="upload7">
<input type="file" class="upload_inp" />
<div class="upload_button_box">
<button class="upload_button select">上传图片</button>
</div>
<div class="upload_progress">
<div class="value"></div>
</div>
</section>
</div>
// 切片上传&断电续传
(function () {
let upload = document.querySelector("#upload7"),
upload_inp = upload.querySelector(".upload_inp"),
upload_inp_select = upload.querySelector(".upload_button.select"),
upload_progress = upload.querySelector(".upload_progress"),
upload_progress_value = upload_progress.querySelector(".value");
let _file = null;
const isGo = (ele) => {
return (
ele.classList.contains("disable") || ele.classList.contains("loading")
);
};
// 处理hash名字
const createBuffer = (file) => {
return new Promise((resolve) => {
let reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = (res) => {
let buffer = res.target.result;
let spark = new SparkMD5.ArrayBuffer();
spark.append(buffer);
let hash = spark.end();
let suffix = /\.[a-zA-Z0-9]+/.exec(file.name)[0];
resolve({
buffer,
hash,
suffix,
filename: `${hash}${suffix}`, //手动拼接文件和后缀
});
};
});
};
// 给选择文件按钮绑定时间事件
upload_inp_select.addEventListener("click", async function () {
// 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
if (isGo(this)) return;
// 触发自带的点击方法选取文件
upload_inp.click();
});
upload_inp.addEventListener("change", async () => {
// 文件选取需要基于change事件完成
let file = upload_inp.files[0];
_file = file;
// 设置按钮样式
upload_inp_select.classList.add("loading");
await createBuffer(_file);
});
})();
实现切片处理有两种方法:固定数量和固定大小。但是实际开发通常两者结合,先设置最大切片数量,以固定大小进行切片,如果当前切片超过了最大数量,则以固定数量处理。固定大小进行切片分割如果切片过小,会造成数量过多,传给服务器也会造成压力,因此添加最大切片数量加以控制。
// 获取已有切片数量
let already = [];
try {
// 该接口调用会返回已有切片数量,如果fileList长度为0或进入catch都可以理解为服务器没有切片,
// 现有文件切片需要全部上传
let { code, fileList } = await instance.get("/upload_already", {
params: {
hash,
},
});
if (+code === 0) {
already = fileList;
}
} catch (error) {}
// 设置切片处理逻辑:固定数量&固定大小结合
let max = 20 * 1024; // 切片最大20kb
let count = Math.ceil(_file.size / max); //切片数量,同时向上取整
// 判断数量是否大于了最大切片数量,大于则以固定数量为基准实现
if (count > 100) {
count = 100; //固定切片数量最大为100个
max = _file.size / count;
}
在一个文件对象的原型上slice(start,end)
方法用于实现切片,不包含结束位置,且单位是字节
let chunks = []; //保存每一个切片
// 生成切片,调用file文件原型上提供的slice方法进行切片
let index = 0; //当前切片的序号
while (index < count) {
chunks.push({
file: _file.slice(max * index, max * (index + 1)), //每一个切片的大小
filename: `${hash}_${index + 1}${suffix}`,
});
index++;
}
console.log(chunks);
断点续传主要代码,先传送小切片,当传送完毕的时候通知服务器合并切片组成文件
// 无论上传切片失败还是切片合并后上传失败,或者最终成功都需要清空loading,因此封装为函数
const clear = () => {
upload_inp_select.classList.remove("loading");
upload_progress.style.display = "none";
upload_progress_value.style.width = "0%";
};
// 控制进度条,根据切片上传的情况控制
index = 0; //当前已上传切片数量
const complete = async () => {
index++;
// 设置进度条样式,当前切片的数量/总切片数量
upload_progress_value.style.width = `${(index / count) * 100}%`;
// 当切片的数量到达指定的数量的时候就开始合并切片
if (index < count) return;
// 切片合并
upload_progress_value.style.width = `100%`;
try {
let { code, servicePath } = await instance.post(
"/upload_merge",
{
HASH: hash,
count,
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
if (+code === 0) {
// 合并成功
alert(`上传成功:文件服务器地址${servicePath}`);
return;
}
} catch (error) {
alert("合并失败");
} finally {
clear();
}
};
// for (let i = 0; i < chunks.length; i++) {
// let chunk = chunks[i];
// // 进入该函数之前,判断该切片是否已经存在于服务器,服务器返回切片的名字
// if (already.length > 0 && already.includes(chunk.filename)) {
// complete(); //执行进度条控制
// return;
// }
// // 获取每一个切片信息上传服务器
// let fm = new FormData();
// fm.append("file", chunk.file);
// fm.append("filename", chunk.filename);
// instance
// .post("/upload_chunk", fm)
// .then((res) => {
// if (+res.code === 0) {
// // 上传成功,执行进度条控制模块
// complete();
// return;
// }
// Promise.reject(res.codeText);
// })
// .catch((err) => {
// alert("当前切片上传失败,请您稍后再试~~");
// clear();
// });
// }
chunks.forEach((chunk) => {
// 进入该函数之前,判断该切片是否已经存在于服务器,服务器返回切片的名字
if (already.length > 0 && already.includes(chunk.filename)) {
complete(); //执行进度条控制
return;
}
// 获取每一个切片信息上传服务器
let fm = new FormData();
fm.append("file", chunk.file);
fm.append("filename", chunk.filename);
instance
.post("/upload_chunk", fm)
.then((res) => {
if (+res.code === 0) {
// 上传成功,执行进度条控制模块
complete();
return;
}
Promise.reject(res.codeText);
})
.catch((err) => {
alert("当前切片上传失败,请您稍后再试~~");
clear();
});
// try {
// let { code, codeText } = await instance.post("/upload_chunk", fm);
// if (+code === 0) {
// // 上传成功,执行进度条控制模块
// complete();
// return;
// }
// throw codeText;
// } catch (err) {
// console.log(err, "单个切片上传失败");
// clear();
// }
});
});