基于JS管理实现 大文件上传和断点续传(近两万字解析超详细)

您所在的位置:网站首页 长龙航空免费退票到什么时候 基于JS管理实现 大文件上传和断点续传(近两万字解析超详细)

基于JS管理实现 大文件上传和断点续传(近两万字解析超详细)

2024-07-16 18:05:05| 来源: 网络整理| 查看: 265

1.工程结构目录 整体项目

image.png

客户端项目目录

image.png

服务器端项目目录

image.png

安装的一些模块 { "name": "project", "version": "1.0.0", "description": "", "main": "index.js", "dependencies": { "axios": "^0.21.1", //发请求 "spark-md5": "^3.0.1", //随机生成文件名 "qs": "^6.9.6" }, "keywords": [], "author": "", "license": "ISC" } 启动服务器

image.png

也可以使用pm2启动服务器

2.Axios二次封装及前后端通信常用的数据格式 将需要的js导入

image.png

js目录下的instance.js实现对axios的二次配置封装

请求主体传递给服务器的数据格式:FormData/x-ww-form-urlencoded/json字符串/普通文本宇符串/Buffer..

let instance = axios.create();//创建一个单独的实例,为了防止影响别人的项目代码 instance.defaults.baseURL = "http://127.0.0.1:8888"; instance.defaults.headers["Content-Type"] = "multipart/form-data"; instance.defaults.transformRequest = (data, headers) => { const contentType = headers["Content-Type"]; if (contentType === "application/x-www-form-urlencoded") return Qs.stringify(data); return data; }; instance.interceptors.response.use((response) => { return response.data; });

这段代码使用了Axios库来创建一个实例,然后设置了一些默认配置和拦截器。

首先,使用axios.create()方法创建了一个名为instance的Axios实例。接下来,通过instance.defaults对象设置了一些默认配置:

baseURL属性设置了请求的基础URL为http://127.0.0.1:8888。 headers属性设置了请求头的Content-Type为multipart/form-data,这是一种常用于上传文件的数据格式。 transformRequest属性设置了一个函数,用于在发送请求前对请求数据进行处理。这个函数会检查请求头的Content-Type,如果是application/x-www-form-urlencoded,则使用Qs.stringify()方法将请求数据转换为URL编码格式;否则直接返回请求数据。

最后,使用instance.interceptors.response.use()方法设置了一个响应拦截器,用于在接收到响应后对响应数据进行处理。这个拦截器会将响应数据中的data字段提取出来并返回,以便后续处理。

upload.js实现相应的功能

image.png

3.上传文件大小或者格式等限制的处理 3.1 基于FORM-DATA实现文件上传

iShot_2023-05-15_13.34.12.gif

单一文件上传「FORM-DATA」 选择文件 上传到服务器 只能上传 PNG/JPG/JPEG 格式图片,且大小不能超过2MB /* 基于FORM-DATA实现文件上传 */ (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_tip = upload.querySelector('.upload_tip'), upload_list = upload.querySelector('.upload_list'); let _file = null; // 上传文件到服务器 代码在下方具体分析 // 移除按钮的点击处理 代码在下方具体分析 // 监听用户选择文件的操作 代码在下方具体分析 // 点击选择文件按钮,触发上传文件INPUT框选择文件的行为 代码在下方具体分析 })();

image.png

上传文件到服务器

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", function () { if (upload_button_upload.classList.contains("disable") || upload_button_upload.classList.contains("loading")) return; if (!_file) { alert("请您先选择要上传的文件~~"); return; } changeDisable(true); // 把文件传递给服务器:FormData / BASE64 let formData = new FormData(); /*`FormData`对象的`append`方法将要上传的文件添加到`formData`中。 `append`方法接受两个参数,第一个参数是要添加的字段名,这里是`file`, 第二个参数是要添加的文件对象,这里是`_file`。这样,`_file`就被添加到了`formData`中, 可以通过`instance.post`方法将其上传到服务器。*/ 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(() => { clearHandle(); changeDisable(false); }); });

这段代码实现了一个点击事件监听器,用于将已选择的文件上传到服务器。

具体来说,代码首先定义了一个名为changeDisable的函数,用于在上传文件的过程中禁用上传按钮,并显示一个加载中的状态。这个函数接受一个名为flag的参数,如果flag为true,则表示需要禁用上传按钮并显示加载中的状态,否则表示需要恢复上传按钮的状态。

接下来,代码获取了一个名为upload_button_upload的DOM元素,并为其添加了一个点击事件监听器。当用户点击这个按钮时,会执行一个回调函数。

在回调函数中,代码首先检查上传按钮是否被禁用或处于加载中的状态,如果是,则直接返回,不执行后续操作。接着,代码检查是否已选择要上传的文件,如果没有选择,则弹出一个提示框并返回,不执行后续操作。

如果文件已选择,则调用changeDisable函数,禁用上传按钮并显示加载中的状态。接着,代码创建一个名为formData的FormData对象,并将要上传的文件添加到其中。然后,代码使用instance.post方法将文件上传到服务器,并在上传成功后弹出一个提示框,显示上传成功的信息和访问资源的路径。如果上传失败,则弹出一个提示框,显示上传失败的信息。

最后,无论上传成功还是失败,代码都会调用clearHandle函数,清除已选择的文件信息,并调用changeDisable函数,恢复上传按钮的状态。

移除按钮的点击处理

const clearHandle = () => { _file = null; upload_tip.style.display = "block"; upload_list.style.display = "none"; upload_list.innerHTML = ``; }; upload_list.addEventListener("click", function (ev) { let target = ev.target; if (target.tagName === "EM") { // 点击的是移除按钮 clearHandle(); } });

这段代码实现了一个点击事件监听器,用于在用户点击移除按钮时清除已选择的文件信息。

具体来说,代码首先定义了一个名为clearHandle的函数,用于清除已选择的文件信息。这个函数将_file变量赋值为null,并将上传文件的提示信息显示出来,同时隐藏已选择的文件信息。

接下来,代码获取了一个名为upload_list的DOM元素,并为其添加了一个点击事件监听器。当用户点击这个元素时,会执行一个回调函数。

在回调函数中,代码首先获取了用户点击的元素,并将其赋值给一个名为target的变量。如果用户点击的是一个名为EM的元素,则说明用户点击的是移除按钮,此时会调用clearHandle函数,清除已选择的文件信息。

监听用户选择文件的操作

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 = ` 文件:${file.name} 移除 `; });

这段代码实现了一个监听器,用于在用户选择文件后获取文件信息并进行一些限制和操作。

具体来说,代码首先获取了一个名为upload_inp的DOM元素,并为其添加了一个change事件监听器。当用户选择文件后,会执行一个回调函数。

在回调函数中,代码首先通过upload_inp.files[0]获取用户选择的文件对象,并将其赋值给一个名为file的变量。如果用户没有选择文件,则直接返回,不执行后续操作。 输出file: image.png

如果单独输出 console.log(upload_inp.files): image.png 它是一个集合,因为当前只选中一个文件,如果以后选择多个文件也可以实现;

接下来,代码对文件进行了两个限制:

限制文件上传的大小,如果文件大小超过2MB,则弹出一个提示框并返回,不执行后续操作。 (注释掉的代码)限制文件上传的格式,如果文件格式不是PNG、JPG或JPEG,则弹出一个提示框并返回,不执行后续操作。

如果文件符合限制条件,则将文件对象赋值给一个名为_file的变量。

最后,代码将上传文件的提示信息隐藏,并显示一个名为upload_list的元素,用于显示已选择的文件信息。这个元素的HTML内容是一个包含文件名和移除按钮的列表项。

点击选择文件按钮,触发上传文件INPUT框选择文件的行为

upload_button_select.addEventListener("click", function () { if (upload_button_select.classList.contains("disable") || upload_button_select.classList.contains("loading")) return; upload_inp.click(); });

这段代码实现了一个点击事件监听器,当用户点击一个选择文件的按钮时,会触发上传文件的操作。

具体来说,代码首先获取了一个名为upload_button_select的DOM元素,并为其添加了一个点击事件监听器。当用户点击这个按钮时,会执行一个回调函数。

在回调函数中,代码首先检查upload_button_select元素是否包含disable或loading这两个CSS类名,如果包含,则直接返回,不执行后续操作。这个检查可能是为了避免用户在上传文件过程中重复点击按钮。

如果upload_button_select元素不包含这两个CSS类名,则会调用upload_inp.click()方法,触发一个名为upload_inp的上传文件的INPUT框的点击事件。这个INPUT框可能是一个隐藏的元素,用于选择要上传的文件。

3.2 服务器端的处理

创建文件并写入到指定的目录 & 返回客户端结果

后端接口

image.png

const writeFile = function writeFile(res, path, file, filename, stream) {//这段代码实现了一个名为writeFile的函数,用于将上传的文件写入到服务器的指定路径中。 /* 具体来说,函数接受五个参数,分别是res、path、file、filename和stream。其中,res是响应对象,path是要写入的文件路径,file是要写入的文件内容,filename是要写入的文件名,stream是一个布尔值,表示是否使用流的方式写入文件。 */ return new Promise((resolve, reject) => { /* 函数首先返回一个Promise对象,用于异步处理文件写入的过程。接着,函数根据stream的值来决定是使用流的方式写入文件,还是使用fs.writeFile方法写入文件。 */ if (stream) { /* 如果使用流的方式写入文件,函数会创建一个可读流和一个可写流,并将可读流的数据写入到可写流中。在可读流的end事件触发时,表示文件写入完成,此时函数会删除上传的临时文件,并通过响应对象res发送一个包含上传成功信息和访问资源路径的响应。 */ try { let readStream = fs.createReadStream(file.path), writeStream = fs.createWriteStream(path); readStream.pipe(writeStream); readStream.on("end", () => { resolve(); fs.unlinkSync(file.path); res.send({ code: 0, codeText: "upload success", originalFilename: filename, servicePath: path.replace(__dirname, HOSTNAME), }); }); } catch (err) { reject(err); res.send({ code: 1, codeText: err, }); } return; } fs.writeFile(path, file, (err) => {/* 如果不使用流的方式写入文件,函数会直接调用fs.writeFile方法将文件写入到指定路径中。在写入完成后,函数会通过响应对象res发送一个包含上传成功信息和访问资源路径的响应。 */ if (err) { reject(err); res.send({ code: 1, codeText: err, }); return; } resolve(); res.send({ code: 0, codeText: "upload success", originalFilename: filename, servicePath: path.replace(__dirname, HOSTNAME), }); }); }); /* 无论使用哪种方式写入文件,如果写入过程中出现错误,函数会通过响应对象res发送一个包含错误信息的响应,并将Promise对象的状态设置为rejected。如果写入成功,函数会将Promise对象的状态设置为resolved。 */ };

单文件上传处理「FORM-DATA」

app.post("/upload_single", async (req, res) => { try { let { files } = await multiparty_upload(req, true); let file = (files.file && files.file[0]) || {}; res.send({ code: 0, codeText: "upload success", originalFilename: file.originalFilename, servicePath: file.path.replace(__dirname, HOSTNAME), }); } catch (err) { res.send({ code: 1, codeText: err, }); } });

这段代码实现了一个名为/upload_single的路由,用于处理单个文件上传的请求。当客户端向服务器发送POST请求时,服务器会调用这个路由来处理请求。

这个路由使用了multiparty模块来解析请求中的文件数据。multiparty是一个Node.js模块,用于处理multipart/form-data类型的数据, 这种数据通常用于文件上传。在这个路由中,我们使用multiparty_upload函数来解析请求中的文件数据。

multiparty_upload函数是一个异步函数,它接受两个参数:req和true。req是一个包含请求信息的对象,true表示使用流的方式上传文件。这个函数返回一个Promise对象,用于异步处理文件上传的过程。

在这个路由中,我们使用了async/await语法来处理异步操作。我们使用await关键字来等待multiparty_upload函数返回的Promise对象,然后将解析结果赋值给files变量。 files变量是一个包含上传文件信息的对象,其中file属性是一个数组,包含了上传的文件信息。

接着,我们从files对象中获取上传的文件信息,并将这些信息发送给客户端。我们使用响应对象res的send方法来发送响应,响应中包含了上传成功的信息和访问资源路径。

如果解析过程中出现错误,multiparty_upload函数会抛出一个错误,我们使用try/catch语句来捕获这个错误,并将错误信息发送给客户端。

总之,这段代码实现了一个简单的文件上传功能,使用了multiparty模块来解析文件数据,使用了async/await语法来处理异步操作,使用了try/catch语句来处理错误。

app.post("/upload_single_name", async (req, res) => { try { let { fields, files } = await multiparty_upload(req); let file = (files.file && files.file[0]) || {}, filename = (fields.filename && fields.filename[0]) || "", path = `${uploadDir}/${filename}`, isExists = false; // 检测是否存在 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, true); } catch (err) { res.send({ code: 1, codeText: err, }); } });

这段代码实现了一个名为/upload_single_name的路由,用于处理单个文件上传的请求,并指定了上传文件的文件名。当客户端向服务器发送POST请求时,服务器会调用这个路由来处理请求。

这个路由与之前的/upload_single路由相似,但是增加了一个filename字段,用于指定上传文件的文件名。在这个路由中,我们首先使用await multiparty_upload(req)来解析请求中的文件数据,然后从解析结果中获取上传的文件信息和指定的文件名。

接着,我们使用指定的文件名和上传目录的路径来构造上传文件的完整路径,并使用await exists(path)来检测该文件是否已经存在。如果文件已经存在,我们向客户端发送一个包含文件已经存在的信息的响应,并返回。如果文件不存在,我们调用writeFile函数来将上传的文件写入到指定的路径中,并向客户端发送一个包含上传成功的信息和访问资源路径的响应。

如果解析过程中出现错误,multiparty_upload函数会抛出一个错误,我们使用try/catch语句来捕获这个错误,并将错误信息发送给客户端。

实现了一个带有文件名的文件上传功能,使用了multiparty模块来解析文件数据,使用了async/await语法来处理异步操作,使用了try/catch语句来处理错误。

4.文件上传方案之BASE64及服务器端处理 4.1 实现效果:

iShot_2023-05-15_18.18.25.gif

image.png

4.2 html文件 单一文件上传「BASE64」,只适合图片 上传图片 只能上传jpg/png格式图片,且大小不能超过2mb 4.3 客户端js (function () { let upload = document.querySelector("#upload2"), upload_inp = upload.querySelector(".upload_inp"), upload_button_select = upload.querySelector(".upload_button.select"); // 验证是否处于可操作性状态 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); }; }); }; 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, },//作用是将编码后的 BASE64 字符串和文件名传递给服务器,供服务器进行解析和处理。在服务器上,这两个参数将用于组成一个完整的上传请求。 { 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"); } }); upload_button_select.addEventListener("click", function () { if (checkIsDisable(this)) return; upload_inp.click(); }); })();

const changeBASE64 = (file) => {}

这段代码定义了一个名为 changeBASE64 的 JavaScript 函数,该函数接受一个参数 file(表示文件对象)。该函数的作用是将传入的文件对象转换为 BASE64 编码,并返回一个 Promise 对象。当文件读取并转换成功时,则通过 resolve 回调返回一个包含了文件内容的字符串形式的 BASE64 编码。

下面是每一步的详细解释:

const changeBASE64 = (file) => { 这行代码定义了一个名为 changeBASE64 的函数,并接受一个参数 file。该函数使用 ES6 的箭头函数语法声明。

return new Promise((resolve) => { 该行代码返回一个 Promise 对象,用于可以异步获取转换后的 BASE64 编码。resolve 参数是 Promise 中的回调函数,将在异步操作成功结束时被调用。

let fileReader = new FileReader(); 该行代码声明一个新的 FileReader 对象,用来读取文件。

fileReader.readAsDataURL(file); 该行代码使用 FileReader 的 readAsDataURL 方法,把文件内容读入内存,并将其转换为 Base64 格式。

fileReader.onload = (ev) => {resolve(ev.target.result);}; 该行代码监听 onload 事件,该事件在文件读取完成后被触发。在 onload 回调函数中,调用 resolve 函数,以传递读取到的文件数据(BASE64 编码)。

该段代码的作用是将传入的文件对象转换为 BASE64 编码,并使用 Promise 异步返回 BASE64 编码字符串。

upload_inp.addEventListener("change", async function () {}

这段代码监听了一个文件上传框(upload_inp)的 change 事件,当文件发生改变时(即用户选择了一个文件),则会开始执行以下步骤:

获取文件和初始化必要变量

首先,函数会通过 upload_inp.files[0] 获取用户选中的文件,然后将这个文件存储到变量 file 中,接着定义两个额外的变量:BASE64 和 data,并将它们初始化为 undefined。

检查文件大小

然后,代码会检查用户选中的文件是否为空。如果为空,则直接退出函数。如果文件存在,代码会检查这个文件的大小是否超过了 2MB,如果超过则通过 alert 函数警告用户,并退出函数。

添加“加载中”状态

如果用户选中的文件大小满足要求,则接下来会添加一个类名为 "loading" 的 class 到一个元素(upload_button_select)上,以显示“正在上传的状态”。

转换成 BASE64 编码

接下来,代码调用另一个异步函数 changeBASE64,该函数将文件转换为 BASE64 编码。函数使用 await 关键字等待该异步操作完成,并将结果存储在 BASE64 变量中。

发送 HTTP POST 请求

在获取到 BASE64 编码后,代码会向服务器发送一个 HTTP POST 请求,传递编码后的文件内容、文件名等参数。请求的代码使用了 Axios 库,该库暴露了一个名为 instance 的请求实例。请求的 URL 是 "/upload_single_base64",请求头部的 Content-Type 是 "application/x-www-form-urlencoded"。请求信息包括了文件的编码和文件名等参数。整个 HTTP 请求过程使用了 try...catch 语句捕获 HTTP 错误,如果成功则展示“上传成功”的提示窗口,否则展示“上传失败”的提示窗口。

移除“加载中”状态

不论 HTTP 请求成功与否,最终都会执行 finally 代码块中的代码,也就是通过 classList.remove 方法从 upload_button_select 元素上移除 "loading" 类,以结束“正在上传”的状态。

综上所述,该段代码的作用是实现了一个文件上传功能,其具体步骤包括检查文件大小、转换文件为 BASE64 编码、向服务器发送 HTTP POST 请求、显示上传结果的提示窗口等。

upload_button_select.addEventListener("click", function () {}

这段代码添加了一个事件监听器,当用户点击上传按钮(upload_button_select)时,会触发该监听器,执行以下步骤:

checkIsDisable 函数的调用

该函数的作用是检查上传按钮是否包含 "disable" 或 "loading" 类名,如果包含其中之一,则会立即返回(即不执行下面的上传操作)。

触发上传框的点击事件

如果上传按钮没有 "disable" 或 "loading" 类名,则会使用 click 方法模拟触发 upload_inp(上传框)的点击事件,这样可以弹出文件选择器,用户便可选择要上传的文件。

该段代码的作用是实现一个按钮和上传框的联动,当用户点击上传按钮时,如果未处于“禁用”或“加载中”状态,则触发上传框的点击事件,弹出文件选择器以便用户选择要上传的文件。

4.4 服务器端

后端接口

image.png

单文件上传处理「BASE64」

app.post("/upload_single_base64", async (req, res) => { let file = req.body.file, filename = req.body.filename, spark = new SparkMD5.ArrayBuffer(), suffix = /\.([0-9a-zA-Z]+)$/.exec(filename)[1], isExists = false, path; file = decodeURIComponent(file); file = file.replace(/^data:image\/\w+;base64,/, ""); file = Buffer.from(file, "base64"); spark.append(file); 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); });

这段代码是一个服务器端路由处理程序,用于接收客户端的 POST 请求,并处理 BASE64 编码的文件上传。以下是对代码每一步的详细解释:

使用 Express 框架的 app.post 方法创建一个 POST 路由,路由路径为 "/upload_single_base64"。 在异步函数中,获取请求主体中的 file 和 filename 字段的值,并声明变量 spark、suffix、isExists 和 path。 使用 SparkMD5.ArrayBuffer 创建一个 SparkMD5 对象,用于计算文件内容的 MD5 值。 使用正则表达式 /\.[0-9a-zA-Z]+$/ 匹配文件名中的后缀,并将匹配结果赋值给 suffix 变量。 将 isExists 变量初始化为 false,用于判断文件是否已存在。 对 file 进行解码,使用 decodeURIComponent 函数解码 BASE64 编码的文件数据。 使用正则表达式 /^data:image\/\w+;base64,/ 替换文件数据的开头部分,将其移除,只留下文件的 BASE64 编码内容。 使用 Buffer.from 方法将 BASE64 编码的文件内容转换为 Buffer 对象。 使用 spark.append 方法将文件内容追加到 SparkMD5 对象中,计算文件的 MD5 值。 构建文件的存储路径 path,由 ${uploadDir}/${spark.end()}.${suffix} 组成,其中 ${uploadDir} 是上传目录的路径,${spark.end()} 是文件内容的 MD5 值,${suffix} 是文件的后缀。 使用 await delay() 延迟一段时间。是为了模拟上传过程中的延迟,您可能需要查看其他代码以确定 delay 函数的具体实现。 调用自定义函数 exists 检测文件是否已经存在。 如果文件已存在,发送一个 JSON 响应,包含字段 code、codeText、originalFilename 和 servicePath。code 设置为0表示成功,codeText 是附带的文本说明,originalFilename 是原始文件名,servicePath 是服务端文件的访问路径,将 __dirname 替换为 HOSTNAME。 如果文件不存在,则调用自定义函数 writeFile 处理文件写入操作,并传入相应的参数。

这段代码处理客户端的文件上传请求。它首先解析请求中的 BASE64 编码文件数据,计算文件的 MD5 值,并构建文件的存储路径。然后,它检测文件是否已经存在,如果存在则返回相应的信息,如果不存在则执行文件写入操作。最终,它发送相应的响应数据给客户端。

5.文件上传缩略图及HASH名字的编译处理

iShot_2023-05-16_11.49.26.gif

5.1html代码 单一文件上传「缩略图处理」 选择文件 上传到服务器 5.2客户端js代码

image.png

// 验证是否处于可操作性状态 const checkIsDisable = (element) => { let classList = element.classList; return classList.contains("disable") || classList.contains("loading"); };

这段代码定义了一个名为 checkIsDisable 的 JavaScript 函数,该函数接受一个参数 element(表示 DOM 元素)。该函数的作用是检查该元素是否处于某些状态中,例如“禁用”或“加载中”。

下面是每一步的详细解释:

const checkIsDisable = (element) => { 这行代码定义了一个名为 checkIsDisable 的函数,并接受一个参数 element。该函数使用 ES6 的箭头函数语法声明。

let classList = element.classList; 该行代码将元素的 class 列表存储在 classList 变量中。classList 是元素的属性之一,它是一个只读属性,返回一个包含元素类名的 DOMTokenList 对象。

return classList.contains("disable") || classList.contains("loading"); 该行代码检查元素是否包含类名 "disable" 或 "loading"。如果是其中之一则返回 true,否则返回 false。

综上所述,checkIsDisable 函数的作用是传入一个 DOM 元素,检查该元素是否处于“禁用”或“加载中”等状态中,如果是其中之一则返回 true,否则返回 false。

// 把选择的文件读取成为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}`, }); }; }); };

这两个函数分别是将文件对象转换为 BASE64 编码和将文件对象转换为 ArrayBuffer,并计算文件的 MD5 哈希值的函数。

changeBASE64 函数的实现步骤如下:

return new Promise((resolve) => { ... }); 该行代码返回一个 Promise 对象,用来异步返回处理后的结果。

let fileReader = new FileReader(); 创建一个 FileReader 对象,用来读取文件对象的数据。

fileReader.readAsDataURL(file); 将文件对象读取为 BASE64 编码。

fileReader.onload = (ev) => { resolve(ev.target.result); }; 当文件读取完成后,将读取的结果作为 Promise 的返回值返回。

changeBuffer 函数的实现步骤如下:

return new Promise((resolve) => { ... }); 该行代码返回一个 Promise 对象,用来异步返回处理后的结果。

let fileReader = new FileReader(); 创建一个 FileReader 对象,用来读取文件对象的数据。

fileReader.readAsArrayBuffer(file); 将文件对象读取为 ArrayBuffer。

fileReader.onload = (ev) => { ... }; 当文件读取完成后,执行回调函数。

let buffer = ev.target.result, spark = new SparkMD5.ArrayBuffer(), HASH, suffix; 将读取的 ArrayBuffer 赋值给 buffer 变量,并创建一个 SparkMD5.ArrayBuffer 对象用于计算文件的 MD5 哈希值。同时定义 HASH、suffix 变量,用于存储文件的哈希值和后缀名。

spark.append(buffer); HASH = spark.end(); 将读取的 ArrayBuffer 数据添加到 SparkMD5 对象中,并计算文件的 MD5 哈希值。

suffix = /\.([a-zA-Z0-9]+)$/.exec(file.name)[1]; 使用正则表达式从文件名中提取出文件的后缀名,并将其赋值给 suffix 变量。

resolve({ buffer, HASH, suffix, filename: HASH.{HASH}.HASH.{suffix}, }); 将处理后的结果作为 Promise 的返回值返回,其中包括文件的 ArrayBuffer 数据、哈希值、后缀名以及新的文件名(由哈希值和后缀名组成)。

// 把文件上传到服务器 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; }); });

这部分代码包含了文件上传到服务器的逻辑。以下是对代码每一步的详细解释:

changeDisable 函数:

该函数接受一个布尔类型的参数 flag,用于指定是否禁用上传按钮。 如果 flag 为 true,则给上传按钮添加 "disable" 类和 "loading" 类,用于禁用按钮并显示加载状态。 如果 flag 为 false,则移除上传按钮的 "disable" 类和 "loading" 类,恢复按钮的可用状态。

在上传按钮 (upload_button_upload) 上添加 "click" 事件监听器,并定义一个异步函数作为事件处理程序。

使用 checkIsDisable 函数检查上传按钮是否被禁用,如果被禁用,则返回并不执行后续操作。

检查 _file 变量是否存在,如果不存在则弹出警告提示用户选择要上传的文件,并返回。

调用 changeDisable(true) 禁用上传按钮,并显示加载状态。

调用 changeBuffer(_file) 函数获取文件的相关信息,其中包括文件名。

创建一个 FormData 对象 formData。

使用 formData.append 方法将文件对象 _file 和文件名 filename 添加到表单数据中。

使用 instance.post 方法发送 POST 请求到 "/upload_single_name" 路径,并传递表单数据 formData。

使用 Promise 的链式调用处理请求的响应数据。

如果响应的 data.code 为 0,表示文件上传成功,弹出成功提示,并返回。

如果 data.code 不为 0,则使用 Promise.reject 将 data.codeText 作为拒绝的理由抛出。

在链式调用的 catch 方法中捕获拒绝的理由,并弹出文件上传失败的提示。

在 finally 方法中,无论请求成功还是失败,都会执行以下操作:

调用 changeDisable(false) 恢复上传按钮的可用状态。 隐藏上传进度相关的元素。 将上传进度元素 (upload_abbre) 的图片 (upload_abbre_img) 的 src 设置为空字符串。 将 _file 变量设置为 null,清空已选择的文件。

这段代码实现了文件上传的功能。在点击上传按钮后,它首先禁用按钮并显示加载状态,然后获取文件的信息,构建表单数据并发送 POST 请求到服务器。根据服务器返回的响应结果,弹出相应的提示信息。最后,它恢复上传按钮的状态,清空选择的文件,并进行一些界面上的调整。

// 文件预览,就是把文件对象转换为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(); });

这段代码实现了文件上传的预览功能,即在用户选择文件后,将文件对象转换为 BASE64 编码,并将编码后的数据赋值给图片的 src 属性,从而实现文件预览的效果。

具体实现步骤如下:

upload_inp.addEventListener("change", async function () { ... }); 该行代码为文件上传按钮添加了一个 change 事件监听器,当用户选择文件后,该事件监听器会被触发。

let file = upload_inp.files[0], BASE64; 该行代码获取用户选择的文件对象,并定义一个变量 BASE64 用来存储文件对象转换后的 BASE64 编码。

if (!file) return; 如果用户没有选择文件,则直接返回,不进行后续的操作。

_file = file; 将选择的文件对象保存在一个全局变量 _file 中,以便后续操作使用。

upload_button_select.classList.add("disable"); 将上传按钮的样式设置为禁用状态,防止用户重复上传。

BASE64 = await changeBASE64(file); 调用 changeBASE64 函数将文件对象转换为 BASE64 编码,并使用 await 等待转换完成后将结果赋值给 BASE64 变量。

upload_abbre.style.display = "block"; 将文件预览区域的样式设置为显示状态。

upload_abbre_img.src = BASE64; 将文件预览区域中的图片的 src 属性设置为 BASE64 编码,从而实现文件预览的效果。

upload_button_select.classList.remove("disable"); 将上传按钮的样式设置为可用状态,以便用户进行下一次上传操作。

upload_button_select.addEventListener("click", function () { ... }); 该行代码为上传按钮添加了一个 click 事件监听器,当用户点击上传按钮时,该事件监听器会被触发。

if (checkIsDisable(this)) return; 如果上传按钮处于禁用状态,则直接返回,不进行后续的操作。

upload_inp.click(); 当用户点击上传按钮时,触发上传按钮的点击事件,从而弹出文件选择对话框,让用户选择要上传的文件。

5.3 服务器代码 app.post("/upload_single_name", async (req, res) => { try { let { fields, files } = await multiparty_upload(req); let file = (files.file && files.file[0]) || {}, filename = (fields.filename && fields.filename[0]) || "", path = `${uploadDir}/${filename}`, isExists = false; // 检测是否存在 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, true); } catch (err) { res.send({ code: 1, codeText: err, }); } });

这段代码实现了一个名为/upload_single_name的路由,用于处理单个文件上传的请求,并指定了上传文件的文件名。当客户端向服务器发送POST请求时,服务器会调用这个路由来处理请求。

这个路由与之前的/upload_single路由相似,但是增加了一个filename字段,用于指定上传文件的文件名。在这个路由中,我们首先使用await multiparty_upload(req)来解析请求中的文件数据,然后从解析结果中获取上传的文件信息和指定的文件名。

接着,我们使用指定的文件名和上传目录的路径来构造上传文件的完整路径,并使用await exists(path)来检测该文件是否已经存在。如果文件已经存在,我们向客户端发送一个包含文件已经存在的信息的响应,并返回。如果文件不存在,我们调用writeFile函数来将上传的文件写入到指定的路径中,并向客户端发送一个包含上传成功的信息和访问资源路径的响应。

如果解析过程中出现错误,multiparty_upload函数会抛出一个错误,我们使用try/catch语句来捕获这个错误,并将错误信息发送给客户端。

总之,这段代码实现了一个带有文件名的文件上传功能,使用了multiparty模块来解析文件数据,使用了async/await语法来处理异步操作,使用了try/catch语句来处理错误。

6.文件上传的进度管控处理

iShot_2023-05-16_12.22.50.gif

6.1 html 单一文件上传「进度管控」 上传文件 6.2 客户端js /* 进度管控 */ (function () { let upload = document.querySelector("#upload4"), 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"); }; upload_inp.addEventListener("change", async function () { let file = upload_inp.files[0], data; if (!file) return; upload_button_select.classList.add("loading"); 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("很遗憾,文件上传失败,请您稍后再试~~"); } finally { upload_button_select.classList.remove("loading"); upload_progress.style.display = "none"; upload_progress_value.style.width = `0%`; } }); upload_button_select.addEventListener("click", function () { if (checkIsDisable(this)) return; upload_inp.click(); }); })();

这段代码是一个事件处理程序,用于在文件选择框 (upload_inp) 的值发生变化时执行操作。下面是对代码每一步的详细解释:

在文件选择框上添加 "change" 事件监听器,并定义一个异步函数作为事件处理程序。

通过 upload_inp.files[0] 获取选择的文件对象,并将其赋值给变量 file。

如果 file 不存在(即用户没有选择文件),则直接返回,不执行后续操作。

给选择按钮添加 "loading" 类,以显示加载状态。

在 try 块中执行文件上传操作。

创建一个 FormData 对象,并将文件对象和文件名添加到 formData 中。

使用 instance.post 方法发送 POST 请求,将 formData 作为请求体发送到 "/upload_single" 路径。

在请求的配置项中,通过 onUploadProgress 属性指定一个回调函数,用于在文件上传过程中更新上传进度。

细谈 `onUploadProgress`: `onUploadProgress` 是一个回调函数,用于在文件上传过程中更新上传进度。 它是 Axios 库中的一个配置项,用于监听 XMLHttpRequest 对象的上传进度事件。 onUploadProgress: function (progressEvent) { 处理上传进度}在上述代码中, `progressEvent` 是包含上传进度信息的事件对象。它提供了以下属性: - `loaded`:已上传的字节数。 - `total`:文件的总字节数。 ```js axios.post('/upload', formData, { onUploadProgress: function (progressEvent) { let percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total); console.log(`上传进度:${percentCompleted}%`); } }) .then(function (response) { console.log('上传成功'); }) .catch(function (error) { console.log('上传失败'); }); ``` 在上述示例中,通过 `onUploadProgress` 配置项监听上传进度事件,并计算出上传的百分比。 然后,可以将该百分比显示在页面上或进行其他自定义操作。 注意,这里使用的是 Axios 库进行请求,但其他支持配置 `onUploadProgress` 的网络请求库也可以类似地使用。 请注意,`onUploadProgress` 回调函数只在上传过程中触发,并且不同的浏览器可能在触发频率和精确度上有所差异。因此,实际使用时需要根据需求和浏览器的行为进行适当的处理。 你可以根据这些信息来更新上传进度条、显示上传百分比或执行其他与上传进度相关的操作。

在回调函数中,通过事件对象 ev 获取已上传的字节数 loaded 和文件总字节数 total,计算出上传进度的百分比,并更新上传进度条的显示。

如果上传成功(即响应的 data.code 等于 0),则将上传进度设置为 100%(确保进度条显示完整),等待 300 毫秒(使用 delay 函数)后显示上传成功的提示信息。

如果上传失败,则抛出异常,并在 catch 块中处理异常,显示上传失败的提示信息。

无论上传成功还是失败,最后都执行 finally 块中的代码。

移除选择按钮的 "loading" 类,恢复按钮的状态。

隐藏上传进度条。

将上传进度条的宽度重置为 0%。

这段代码实现了文件上传的功能,并在上传过程中更新上传进度条的显示。如果上传成功,会显示上传成功的提示信息;如果上传失败,会显示上传失败的提示信息。最终,无论上传成功还是失败,都会重置按钮状态和上传进度条的显示。

7.多文件上传和进度管控处理

iShot_2023-05-16_12.53.38.gif

多文件上传 选择文件 上传到服务器

image.png

// 验证是否处于可操作性状态 const checkIsDisable = (element) => { let classList = element.classList; return classList.contains("disable") || classList.contains("loading"); }; // 把文件上传到服务器 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 (_files.length === 0) { alert("请您先选择要上传的文件~~"); return; } changeDisable(true); // 循环发送请求 let upload_list_arr = Array.from(upload_list.querySelectorAll("li")); _files = _files.map((item) => { let fm = new FormData(), curLi = upload_list_arr.find((liBox) => liBox.getAttribute("key") === item.key), curSpan = curLi ? curLi.querySelector("span:nth-last-child(1)") : null; 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"; }); });

这段代码实现了将多个文件上传到服务器的操作。下面是对代码每一步的详细解释:

定义了一个名为 changeDisable 的函数,用于在上传过程中禁用或启用按钮样式。

给上传按钮 (upload_button_upload) 添加 "click" 事件监听器,并定义一个函数作为事件处理程序。

检查上传按钮是否被禁用,如果是,则返回并不执行后续操作。

检查 _files 数组中是否有文件项,如果没有,则弹出提示框提示用户先选择要上传的文件,然后返回并不执行后续操作。

调用 changeDisable 函数并传入 true,禁用上传按钮。

使用 Array.from 方法将上传列表中的每个列表项转换为数组,存储在 upload_list_arr 变量中。

重构 _files 数组中的每个文件项,将其转换为一个 Promise 对象,该对象代表了文件的上传请求过程。

在每个上传请求的配置对象中,使用 onUploadProgress 回调函数来监视上传进度。该函数会获取当前上传进度并更新对应文件项的进度显示。

如果上传请求的响应数据中的 code 属性为 0,表示文件上传成功,将更新对应文件项的进度显示为 100%。

如果上传请求的响应数据中的 code 属性不为 0,表示文件上传失败,将返回一个被拒绝的 Promise 对象。

使用 Promise.all 方法等待所有文件上传请求的处理结果。

如果所有文件上传请求都成功完成,即所有 Promise 对象都被解析,则弹出提示框提示用户所有文件上传成功。

如果有任何一个文件上传请求失败,即存在被拒绝的 Promise 对象,则弹出提示框提示用户上传过程中出现问题。

无论上传成功还是失败,最后都调用 changeDisable 函数并传入 false,恢复上传按钮的样式。

将 _files 数组重置为空数组,清空上传列表的内容。

最后隐藏上传列表。

这段代码实现了多文件上传的功能,并显示了每个文件的上传进度。在所有文件上传请求完成后,会根据成功或失败的情况弹出相应的提示框,并对上传按钮和上传列表进行样式的处理。

// 基于事件委托实现移除的操作 upload_list.addEventListener("click", function (ev) { let target = ev.target, curLi = null, key; if (target.tagName === "EM") { curLi = target.parentNode.parentNode; if (!curLi) return; upload_list.removeChild(curLi); key = curLi.getAttribute("key"); _files = _files.filter((item) => item.key !== key); if (_files.length === 0) { upload_list.style.display = "none"; } } });

这段代码基于事件委托实现了移除文件操作。下面是对代码每一步的详细解释:

给上传列表 (upload_list) 添加 "click" 事件监听器,并定义一个函数作为事件处理程序。

获取事件的目标元素 (target),即用户点击的元素。

定义变量 curLi 并初始化为 null,用于存储当前点击的列表项。

定义变量 key,用于存储要移除的文件的唯一标识符。

如果目标元素的标签名 (tagName) 是 "EM",表示用户点击的是移除按钮。

获取当前点击的列表项 (curLi),通过访问父节点 (parentNode) 和祖父节点 (parentNode) 来获取正确的列表项元素。

如果当前列表项 (curLi) 不存在,则直接返回,不执行后续操作。

从上传列表 (upload_list) 中移除当前列表项 (curLi)。

获取当前列表项的唯一标识符 (key),通过访问自定义属性 "key" 来获取。

使用 filter 方法从 _files 数组中过滤掉具有相同唯一标识符的文件对象。

如果经过过滤后的 _files 数组长度为 0,表示已经移除了所有文件项,则隐藏上传列表。

这段代码实现了基于事件委托的移除文件操作。当用户点击移除按钮时,会从上传列表中删除对应的文件项,并更新 _files 数组,以确保与列表项保持同步。如果所有文件项都被移除,上传列表将被隐藏。

// 获取唯一值 const createRandom = () => { let ran = Math.random() * new Date(); return ran.toString(16).replace(".", ""); }; upload_inp.addEventListener("change", async function () { _files = Array.from(upload_inp.files); if (_files.length === 0) return; // 我们重构集合的数据结构「给每一项设置一个位置值,作为自定义属性存储到元素上,后期点击删除按钮的时候,我们基于这个自定义属性获取唯一值,再到集合中根据这个唯一值,删除集合中这一项」 _files = _files.map((file) => { return { file, filename: file.name, key: createRandom(), }; }); // 绑定数据 let str = ``; _files.forEach((item, index) => { str += ` 文件${index + 1}:${item.filename} 移除 `; }); upload_list.innerHTML = str; upload_list.style.display = "block"; }); upload_button_select.addEventListener("click", function () { if (checkIsDisable(this)) return; upload_inp.click(); });

这段代码实现了选择多个文件并展示在上传列表中的功能。下面是对代码每一步的详细解释:

定义了一个函数 createRandom,用于生成唯一值。它使用了当前时间戳与随机数的组合,并将结果转换为 16 进制字符串,去除小数点。

给上传文件输入框 (upload_inp) 添加 "change" 事件监听器,并定义一个异步函数作为事件处理程序。

使用 Array.from 方法将上传文件输入框中的文件转换为数组,并存储在 _files 变量中。

如果 _files 数组的长度为 0,则直接返回,不执行后续操作。

使用 map 方法遍历 _files 数组,为每个文件创建一个对象,包含 file、filename 和 key 三个属性。其中,file 属性存储文件对象,filename 属性存储文件名,key 属性存储通过 createRandom 函数生成的唯一值。

将处理后的文件对象数组重新赋值给 _files 变量。

构建上传列表的内容字符串 str。

使用 forEach 方法遍历 _files 数组,为每个文件创建一个列表项 (li),其中设置 key 自定义属性为文件的唯一值,显示文件名和移除按钮。

将列表项的字符串拼接到 str 中。

将拼接好的列表项字符串设置为上传列表 (upload_list) 的 HTML 内容。

显示上传列表。

给选择按钮 (upload_button_select) 添加 "click" 事件监听器,并定义一个函数作为事件处理程序。

使用 checkIsDisable 函数检查选择按钮是否被禁用,如果被禁用,则直接返回,不执行后续操作。

触发上传文件输入框的点击事件,弹出文件选择对话框,让用户选择文件。

这段代码实现了选择多个文件后,将文件信息展示在上传列表中,并为每个文件生成唯一的标识符。用户可以通过点击移除按钮来删除对应的文件项。

8.拖拽上传的实现方案

iShot_2023-05-16_16.13.01.gif

拖拽上传 将文件拖到此处,或点击上传 正在上传中,请稍等... 客户端js代码

image.png

// 实现文件上传 const uploadFile = async (file) => { if (isRun) return; isRun = true; upload_mark.style.display = "block"; try { let fm = new FormData(), data; fm.append("file", file); fm.append("filename", file.name); data = await instance.post("/upload_single", fm); if (+data.code === 0) { alert(`恭喜您,文件上传成功,您可以基于 ${data.servicePath} 访问该文件~~`); return; } throw data.codeText; } catch (err) { alert(`很遗憾,文件上传失败,请您稍后再试~~`); } finally { upload_mark.style.display = "none"; isRun = false; } };

这段代码定义了一个名为 uploadFile 的异步函数,用于实现文件上传操作。

函数接受一个 file 参数,表示要上传的文件对象。在函数内部,首先检查 isRun 变量,如果其值为 true,表示上传操作正在进行中,则直接返回,避免重复上传。然后将 upload_mark 元素的显示样式设置为 block,以显示上传进度标识。

接下来,通过 FormData 创建一个新的表单数据对象 fm,并使用 fm.append 方法将文件对象和文件名添加到表单数据中。

然后使用 instance.post 方法发送 POST 请求,将表单数据作为请求体发送到服务器的 /upload_single 路径。等待请求返回的数据 data。

如果返回的数据中的 code 属性为 0,表示文件上传成功,弹出提示框显示上传成功的信息,并结束函数执行。

如果返回的数据中的 code 属性不为 0,表示文件上传失败,抛出一个包含错误信息的异常。

无论文件上传成功还是失败,最后都会执行 finally 代码块中的逻辑,将 upload_mark 元素的显示样式设置为 none,以隐藏上传进度标识,并将 isRun 变量的值设为 false,表示上传操作已结束。

该函数用于封装文件上传的逻辑,通过 POST 请求将文件对象和文件名发送到服务器,并根据返回的结果显示相应的提示信息。

image.png

这段代码是用于实现拖拽文件上传的功能。我将逐步解释每一步的功能和细节。

首先,我们给上传区域(可能是一个DOM元素)绑定了几个事件,包括dragenter、dragleave、dragover和drop。

upload.addEventListener("dragover", function (ev) { ev.preventDefault(); });

这段代码绑定了dragover事件,并阻止了默认的拖拽行为。当文件拖拽到上传区域时,浏览器默认会阻止文件的打开行为,所以我们需要阻止默认行为以允许文件拖拽进入。

upload.addEventListener("drop", function (ev) { ev.preventDefault(); let file = ev.dataTransfer.files[0]; if (!file) return; uploadFile(file); });

这段代码绑定了drop事件,并阻止了默认的拖拽行为。当文件被释放到上传区域时,触发了drop事件。我们阻止了默认行为以防止浏览器打开文件。然后,我们使用ev.dataTransfer.files来获取拖拽的文件列表,这里我们只取第一个文件。如果没有拖拽文件,则直接返回。接下来,我们调用uploadFile函数,将拖拽的文件作为参数进行上传操作。

这样,当用户将文件拖拽到上传区域时,就会触发相应的事件,并调用uploadFile函数来处理文件上传操作。

这里是关于拖拽操作的几个事件的用法解释: 1. `dragenter`事件:当拖动的元素进入目标区域时触发。 可以用来给目标区域添加一些视觉效果或状态的改变。例如,改变目标区域的背景色或边框样式来表示进入了拖拽状态。 2. `dragleave`事件:当拖动的元素离开目标区域时触发。 可以用来还原目标区域的视觉效果或状态。例如,恢复目标区域的默认背景色或边框样式。 3. `dragover`事件:在拖动的元素在目标区域内移动时持续触发。 默认情况下,浏览器会阻止对拖放操作的处理,因此需要通过调用`ev.preventDefault()`来阻止浏览器的默认行为。这样可以允许在目标区域内放置拖动的元素。 4. `drop`事件:当拖动的元素在目标区域内释放时触发。 通常在该事件的处理函数中获取拖放的数据,并进行相应的操作。例如,获取拖放的文件并进行文件上传操作。 这些事件通常结合使用,以实现拖拽文件到指定区域进行上传或其他操作的功能。`dragover`事件和`drop`事件被使用来实现文件拖拽上传的功能。在`dragover`事件中,调用`ev.preventDefault()`阻止浏览器的默认行为,允许在目标区域内放置拖动的元素。在`drop`事件中,获取拖放的文件并调用`uploadFile`函数进行文件上传操作。 需要注意的是,在使用拖拽事件时,要确保在相应的事件处理函数中阻止浏览器的默认行为,以确保拖拽操作正常工作。

手动实现上传

upload_inp.addEventListener("change", function () { let file = upload_inp.files[0]; if (!file) return; uploadFile(file); });

这段代码绑定了change事件,当用户选择文件后,会触发该事件。我们通过upload_inp.files[0]获取用户选择的文件,这里我们只取第一个文件。如果没有选择文件,则直接返回。然后,调用uploadFile函数,将选择的文件作为参数进行上传操作。

upload_submit.addEventListener("click", function () { upload_inp.click(); });

这段代码绑定了点击事件,当用户点击上传按钮时,会触发该事件。在事件处理函数中,我们模拟点击了隐藏的文件选择按钮upload_inp,通过调用upload_inp.click()来触发文件选择对话框,让用户手动选择文件。

通过以上代码,用户既可以通过手动选择文件按钮进行文件上传,也可以通过拖拽文件到上传区域来实现文件上传操作。无论是手动选择还是拖拽,最终都会调用uploadFile函数来处理文件上传操作。

9.大文件的切片上传和断点续传

image.png

9.1 html代码 大文件上传 上传图片 9.2 客户端js代码

image.png

upload_button_select.addEventListener("click", function () { if (checkIsDisable(this)) return; upload_inp.click(); }); /* 这段代码是一个事件监听器,用于处理点击"选择文件"按钮的操作。让我解释它的功能:\ 1. 监听`click`事件:当"选择文件"按钮被点击时触发。 2. 检查按钮状态:通过调用`checkIsDisable`函数检查按钮是否处于禁用状态(包含"disable"或"loading"类)。 3. 触发文件选择:如果按钮没有被禁用,调用`upload_inp.click()`方法触发文件选择框的点击事件,从而弹出文件选择框供用户选择文件。 */ 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); // 获取已经上传的切片信息 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(); }); }); });

该段代码为上传文件添加了切片上传的支持,并在上传完成后,将切片合并为完整的文件。

let { HASH, suffix } = await changeBuffer(file); 调用 changeBuffer 函数,获取文件的哈希值和后缀名。

data = await instance.get("/upload_already", { params: { HASH, }, }); 使用 axios 发送 GET 请求,获取已经上传的文件切片信息。

let max = 1024 * 100, count = Math.ceil(file.size / max), index = 0, chunks = []; 定义 max 变量为切片大小,count 变量为切片数量,index 变量为当前切片索引,chunks 变量为存储切片的数组。

if (count > 100) { max = file.size / 100; count = 100; } 如果切片数量超过 100,将切片大小设置为文件大小除以 100,并将切片数量设置为 100。

while (index < count) { ... } 使用循环将文件切片,并将每个切片对象添加到 chunks 数组中。

const clear = () => { ... }; const complate = async () => { ... }; 定义 clear 和 complate 函数,分别用于清除上传进度条和合并切片。

关于切片的使用 `File.slice()` 方法用于将指定文件切片成多个部分,以便分段上传或下载。该方法的语法如下: ``` file.slice(start, end, contentType); ``` 其中,`start` 和 `end` 参数分别表示切片的起始位置和结束位置,`contentType` 参数可选,表示切片的 MIME 类型。如果省略 `contentType` 参数,则默认使用原文件的 MIME 类型。 `start` 和 `end` 参数可以是负数,表示从文件末尾开始计算的偏移量。例如,`start` 参数为 `-100` 表示从文件末尾往前数 100 个字节的位置开始切片。 使用 `File.slice()` 方法切片文件时,需要注意以下几点: - `File.slice()` 方法返回的是一个新的文件对象,而不是原文件对象本身。 - 切片的起始位置和结束位置必须是文件的有效位置,否则会抛出异常。 - 切片的结束位置不能超过文件的总大小,否则会自动调整为文件的总大小。 - 切片的大小一般应该根据网络环境和服务器的限制进行调整,一般建议将文件切片为 1MB 到 10MB 的大小。 例如,以下代码将一个文件切片为 1MB 大小的多个部分: ``` const MAX_CHUNK_SIZE = 1024 * 1024; // 1MB function sliceFile(file) { let chunks = []; let start = 0; let end = MAX_CHUNK_SIZE; while (start < file.size) { chunks.push(file.slice(start, end)); start = end; end = Math.min(start + MAX_CHUNK_SIZE, file.size); } return chunks; } ``` 该函数将文件切片为多个部分,并将每个切片对象存储在 `chunks` 数组中,以便上传或下载时使用。

const complate = async () => {}

这段代码定义了一个名为complate的异步函数,用于处理切片上传完成的操作。这段代码负责管理切片上传的进度条,并在所有切片上传完成后发起切片合并请求,并根据合并结果进行处理。:

管控进度条:每次调用complate函数时,index增加1,根据已上传切片的数量和总切片数量计算进度条的宽度,更新upload_progress_value元素的宽度样式,以展示上传进度的变化。 判断是否所有切片都上传成功:如果index小于总切片数量count,说明还有切片未完成上传,函数提前返回,等待下一次调用。 更新进度条到100%:如果所有切片都上传成功,将进度条的宽度样式设置为100%。 发起切片合并请求:使用instance.post方法向服务器发起切片合并的请求。请求的URL为"/upload_merge",请求体包括HASH和count两个参数。 处理切片合并结果:根据服务器返回的数据进行判断。如果返回的code为0,表示切片合并成功,弹出提示框告知用户文件上传成功,并调用clear函数清理状态。如果返回的code不为0,表示切片合并失败,弹出提示框告知用户切片合并失败,并调用clear函数清理状态。

chunks.forEach((chunk) => {}

这段代码实现了将每个切片文件上传到服务器的过程。如果某个切片文件已经上传过,将跳过上传步骤。在每个切片上传完成后,会根据切片上传结果进行处理,成功则调用complate函数,失败则弹出提示框并调用clear函数清理状态。

遍历切片数组:使用forEach方法遍历chunks数组,其中每个元素是一个切片对象。 检查是否已经上传:检查already数组中是否包含当前切片的文件名(chunk.filename)。如果已经上传过,则直接调用complate函数进行处理,表示该切片已完成上传。 创建FormData对象:创建一个新的FormData对象fm,用于存储要上传的切片文件和相关信息。 添加切片文件和文件名:使用fm.append方法将切片文件(chunk.file)和文件名(chunk.filename)添加到FormData对象中。 发起切片上传请求:使用instance.post方法向服务器发起切片上传的请求。请求的URL为"/upload_chunk",请求体为fm。 处理切片上传结果:在then方法中,对服务器返回的数据进行判断。如果返回的code为0,表示切片上传成功,调用complate函数进行处理。如果返回的code不为0,表示切片上传失败,通过Promise.reject方法返回一个带有错误信息(data.codeText)的拒绝态的Promise,进入catch方法进行错误处理。 错误处理:在catch方法中,弹出提示框告知用户当前切片上传失败,并调用clear函数清理状态。 9.3切片上传接口

image.png

image.png

合并上传的切片文件

const merge = function merge(HASH, count) { return new Promise(async (resolve, reject) => { let path = `${uploadDir}/${HASH}`, fileList = [], suffix, isExists; isExists = await exists(path); if (!isExists) { reject("HASH path is not found!"); return; } fileList = fs.readdirSync(path); if (fileList.length < count) { reject("the slice has not been uploaded!"); return; } fileList .sort((a, b) => { let reg = /_(\d+)/; return reg.exec(a)[1] - reg.exec(b)[1]; }) .forEach((item) => { !suffix ? (suffix = /\.([0-9a-zA-Z]+)$/.exec(item)[1]) : null; fs.appendFileSync(`${uploadDir}/${HASH}.${suffix}`, fs.readFileSync(`${path}/${item}`)); fs.unlinkSync(`${path}/${item}`); }); fs.rmdirSync(path); resolve({ path: `${uploadDir}/${HASH}.${suffix}`, filename: `${HASH}.${suffix}`, }); }); }; 创建合并函数:使用函数表达式创建名为merge的函数,该函数接受两个参数:HASH和count,分别表示文件的哈希值和切片数量。 返回一个Promise对象:使用new Promise创建一个新的Promise对象,用于进行合并操作,并通过resolve和reject函数来处理异步结果。 构建路径和文件列表:根据给定的哈希值HASH构建路径path,用于存储切片文件的目录。定义一个空数组fileList来存储切片文件的文件名列表。定义变量suffix来存储文件的后缀名。 检查路径是否存在:使用exists函数(未给出代码)来检查路径path是否存在,返回一个布尔值isExists。如果路径不存在,通过reject函数返回错误信息"HASH path is not found!"。 读取切片文件列表:使用fs.readdirSync同步方法读取路径path下的所有文件名,并将文件名存储在fileList数组中。 检查切片文件数量:如果切片文件数量fileList.length小于count,说明还有切片文件未上传完成,通过reject函数返回错误信息"the slice has not been uploaded!"。 对文件列表排序:使用sort方法对fileList数组进行排序,排序规则是根据文件名中的数字进行升序排序。使用正则表达式提取文件名中的数字,并进行比较排序。 合并切片文件:使用forEach方法遍历排序后的fileList数组,对每个切片文件进行合并操作。 获取文件后缀名:如果suffix为空,则使用正则表达式从文件名中提取出后缀名,并将其赋值给suffix变量。 合并文件内容:使用fs.appendFileSync方法将切片文件的内容追加到最终合并的文件中。其中,第一个参数为合并后的文件路径,第二个参数为读取切片文件内容的路径。 删除切片文件:使用fs.unlinkSync方法删除已合并的切片文件。 删除切片目录:使用fs.rmdirSync方法删除切片文件的存储目录path。 解析合并结果:通过resolve函数返回一个对象,包含合并后的文件路径path和文件名filename,其中文件名由哈希值和后缀名组成。 完成合并操作。

这段代码实现了将上传的切片文件进行合并的功能。它首先检查切片文件是否已全部上传完成,然后按照文件名中的数字顺序进行排序,并将切片文件的内容追加到最终的合并文件中。合并完成后,删除切片文件及其存储目录,并返回合并后的文件路径和文件名。

app.post("/upload_chunk", async (req, res) => { try { let { fields, files } = await multiparty_upload(req); let file = (files.file && files.file[0]) || {}, filename = (fields.filename && fields.filename[0]) || "", path = "", isExists = false; // 创建存放切片的临时目录 let [, HASH] = /^([^_]+)_(\d+)/.exec(filename); path = `${uploadDir}/${HASH}`; !fs.existsSync(path) ? fs.mkdirSync(path) : null; // 把切片存储到临时目录中 path = `${uploadDir}/${HASH}/${filename}`; 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, true); } catch (err) { res.send({ code: 1, codeText: err, }); } }); 定义路由处理器:使用app.post方法定义一个POST请求的路由处理器,路径为"/upload_chunk"。 异步处理请求:使用async关键字定义一个异步函数,该函数接收req和res对象作为参数,分别表示请求和响应对象。 解析请求数据:使用multiparty_upload函数(未给出代码)解析请求中的表单字段和文件。 提取文件信息:从files对象中获取名为"file"的文件信息,并赋值给file变量。从fields对象中获取名为"filename"的字段信息,并赋值给filename变量。 定义变量:定义变量path用于存储切片的路径,初始化为一个空字符串。定义变量isExists用于判断文件是否已存在,初始化为false。 创建临时目录:根据切片文件的名称提取哈希值HASH,然后构建临时目录的路径path,存放在${uploadDir}/${HASH}。 如果临时目录不存在,则使用fs.mkdirSync同步方法创建目录。 存储切片文件:将切片文件存储到临时目录中。 构建切片文件的完整路径path,存放在${uploadDir}/${HASH}/${filename}。 使用exists函数(未给出代码)判断文件是否已存在,返回一个布尔值isExists。 如果文件已存在,通过响应对象res发送一个包含已存在的文件信息的响应,包括code为0,codeText为"file is exists",originalFilename为切片文件的原始文件名,servicePath为文件的服务路径(将绝对路径转换为相对路径)。 如果文件不存在,调用writeFile函数(未给出代码)将切片文件写入到临时目录中,并进行响应。

总体来说,这段代码处理了上传切片文件的请求。它从请求中解析出文件信息,并根据切片文件的哈希值创建临时目录。然后将切片文件存储到临时目录中,如果文件已存在,则返回文件已存在的信息,如果文件不存在,则将文件写入临时目录并进行响应。

app.post("/upload_merge", async (req, res) => { let { HASH, count } = req.body; try { let { filename, path } = await merge(HASH, count); res.send({ code: 0, codeText: "merge success", originalFilename: filename, servicePath: path.replace(__dirname, HOSTNAME), }); } catch (err) { res.send({ code: 1, codeText: err, }); } });

这段代码是用于处理合并切片的路由处理器。让我逐步解释每一步的功能:

定义路由处理器:使用app.post方法定义一个POST请求的路由处理器,路径为"/upload_merge"。 异步处理请求:使用async关键字定义一个异步函数,该函数接收req和res对象作为参数,分别表示请求和响应对象。 提取请求数据:从req.body对象中提取HASH和count,分别表示哈希值和切片数量。 合并切片:调用merge函数传入HASH和count进行切片合并。 merge函数返回一个Promise对象,在异步函数中使用await等待合并结果。 解构赋值将merge函数返回的filename和path分别赋值给对应的变量。 发送响应:使用响应对象res发送一个包含合并成功信息的响应,包括code为0,codeText为"merge success",originalFilename为合并后的文件名,servicePath为合并后文件的服务路径(将绝对路径转换为相对路径)。 异常处理:如果合并过程中发生错误,使用响应对象res发送一个包含错误信息的响应,包括code为1,codeText为错误信息。

总体来说,这段代码处理了合并切片文件的请求。它从请求中获取哈希值和切片数量,调用merge函数进行切片合并,并将合并结果通过响应对象进行返回。如果合并过程中出现错误,则发送包含错误信息的响应。

app.get("/upload_already", async (req, res) => { let { HASH } = req.query; let path = `${uploadDir}/${HASH}`, fileList = []; try { fileList = fs.readdirSync(path); fileList = fileList.sort((a, b) => { let reg = /_(\d+)/; return reg.exec(a)[1] - reg.exec(b)[1]; }); res.send({ code: 0, codeText: "", fileList: fileList, }); } catch (err) { res.send({ code: 0, codeText: "", fileList: fileList, }); } }); 定义路由处理器:使用app.get方法定义一个GET请求的路由处理器,路径为"/upload_already"。 异步处理请求:使用async关键字定义一个异步函数,该函数接收req和res对象作为参数,分别表示请求和响应对象。 提取请求参数:从req.query对象中提取HASH,表示哈希值。 获取切片信息:通过指定哈希值构造切片存储路径path,并使用fs.readdirSync同步读取该路径下的文件列表,将结果赋值给fileList数组。 对文件列表排序:使用正则表达式提取每个文件名中的切片序号,并通过排序函数将文件列表按照切片序号进行升序排序。 发送响应:使用响应对象res发送一个包含已上传切片信息的响应,包括code为0,codeText为空字符串,fileList为已上传切片的文件列表。 异常处理:如果获取切片信息的过程中发生错误,使用响应对象res发送一个包含错误信息的响应,包括code为0,codeText为空字符串,fileList为空数组。

这段代码处理了获取已上传切片信息的请求。它从请求中获取哈希值,通过读取指定路径下的文件列表,并对文件列表按照切片序号进行排序,最后将切片信息通过响应对象进行返回。如果获取切片信息的过程中出现错误,则发送一个包含错误信息的响应。



【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭