文件上传

单文件与多文件上传

利用 input 元素的 accept 属性限制上传文件的类型、比如使用 image/* 限制只能选择图片文件;

同时,为了防止修改文件后缀绕过限制,需要利用 JS 读取文件中的二进制数据来识别正确的文件类型。

然后把读取的 File 对象封装成 FromData 对象,然后利用 Axios 实例的 post 方法实现文件上传的功能。然后服务端使用 Koa 实现单文件上传的功能;多文件上传利用 input 元素的 multiple 属性。

/** 客户端 */
<input id="uploadFile" type="file" accept="image/*" />
<input id="uploadFile" type="file" accept="image/*" multiple/>const uploadFileEle = document.querySelector("#uploadFile");const request = axios.create({baseURL: "http://localhost:3000/upload",timeout: 60000,
});async function uploadFile() {if (!uploadFileEle.files.length) return;// const files = Array.from(uploadFileEle.files); 多个文件上传const file = uploadFileEle.files[0]; // 获取单个文件// 省略文件的校验过程,比如文件类型、大小校验upload({url: "/single", // multiplefile, // files});
}function upload({ url, file, fieldName = "file" }) {let formData = new FormData();formData.set(fieldName, file);request.post(url, formData, {// 监听上传进度onUploadProgress: function (progressEvent) {const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);console.log(percentCompleted);},});
}function uploadMult({ url, files, fieldName = "file" }) {let formData = new FormData();files.forEach((file) => {formData.append(fieldName, file);});request.post(url, formData, {// 监听上传进度onUploadProgress: function (progressEvent) {const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);console.log(percentCompleted);},});
}/** 服务端 */
const path = require("path");
const Koa = require("koa");
const serve = require("koa-static");
const cors = require("@koa/cors");
const multer = require("@koa/multer");
const Router = require("@koa/router");const app = new Koa();
const router = new Router();
const PORT = 3000;
// 上传后资源的URL地址
const RESOURCE_URL = `http://localhost:${PORT}`;
// 存储上传文件的目录
const UPLOAD_DIR = path.join(__dirname, "/public/upload");const storage = multer.diskStorage({destination: async function (req, file, cb) {// 设置文件的存储目录cb(null, UPLOAD_DIR);},filename: function (req, file, cb) {// 设置文件名cb(null, `${file.originalname}`);},
});const multerUpload = multer({ storage });router.get("/", async (ctx) => {ctx.body = "欢迎使用文件服务";
});router.post("/upload/single",async (ctx, next) => {try {await next();ctx.body = {code: 1,msg: "文件上传成功",url: `${RESOURCE_URL}/${ctx.file.originalname}`,};} catch (error) {ctx.body = {code: 0,msg: "文件上传失败"};}},multerUpload.single("file")
);router.post("/upload/multiple",async (ctx, next) => {try {await next();urls = ctx.files.file.map(file => `${RESOURCE_URL}/${file.originalname}`);ctx.body = {code: 1,msg: "文件上传成功",urls};} catch (error) {ctx.body = {code: 0,msg: "文件上传失败",};}},multerUpload.fields([{name: "file", // 与FormData表单项的fieldName想对应},])
);// 注册中间件
app.use(cors());
app.use(serve(UPLOAD_DIR));
app.use(router.routes()).use(router.allowedMethods());app.listen(PORT, () => {console.log(`app starting at port ${PORT}`);
});

拖拽上传

拖拽事件有:

  • dragenter:当拖拽元素或选中的文本到一个可释放目标时触发;
  • dragover:当元素或选中的文本被拖到一个可释放目标上时触发(每100毫秒触发一次);
  • dragleave:当拖拽元素或选中的文本离开一个可释放目标时触发;
  • drop:当元素或选中的文本在可释放目标上被释放时触发。
<div id="dropArea"><p>拖拽上传文件</p><div id="imagePreview"></div>
</div>const dropAreaEle = document.querySelector("#dropArea");
const imgPreviewEle = document.querySelector("#imagePreview");
const IMAGE_MIME_REGEX = /^image\/(jpe?g|gif|png)$/i;["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {dropAreaEle.addEventListener(eventName, preventDefaults, false);document.body.addEventListener(eventName, preventDefaults, false);
});function preventDefaults(e) {e.preventDefault();e.stopPropagation();
}/** 切换目标区域的高亮状态 */
["dragenter", "dragover"].forEach((eventName) => {dropAreaEle.addEventListener(eventName, highlight, false);
});
["dragleave", "drop"].forEach((eventName) => {dropAreaEle.addEventListener(eventName, unhighlight, false);
});// 添加高亮样式
function highlight(e) {dropAreaEle.classList.add("highlighted");
}// 移除高亮样式
function unhighlight(e) {dropAreaEle.classList.remove("highlighted");
}/** 处理图片预览 */
dropAreaEle.addEventListener("drop", handleDrop, false);function previewImage(file, container) {if (IMAGE_MIME_REGEX.test(file.type)) {const reader = new FileReader();reader.onload = function (e) {let img = document.createElement("img");img.src = e.target.result;container.append(img);};reader.readAsDataURL(file);}
}/** 文件上传 */
function handleDrop(e) {const dt = e.dataTransfer;const files = [...dt.files];files.forEach((file) => {previewImage(file, imgPreviewEle);});// 省略图片预览代码files.forEach((file) => {upload({url: "/single",file,});});
}const request = axios.create({baseURL: "http://localhost:3000/upload",timeout: 60000,
});function upload({ url, file, fieldName = "file" }) {let formData = new FormData();formData.set(fieldName, file);request.post(url, formData, {// 监听上传进度onUploadProgress: function (progressEvent) {const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);console.log(percentCompleted);},});
}

剪贴板复制上传

利用 Clipboard  API 进行系统剪贴板的读写访问,可用于实现剪切、复制和粘贴功能。前端只需要通过 navigator.clipboard 来获取 Clipboard 对象,剪贴板为空或者不包含文本时,navigator.clipboard.readText() 方法会返回一个空字符串。

具体实现逻辑:

  • 监听容器的粘贴事件;
  • 读取并解析剪贴板中的内容;
  • 动态构建 FormData 对象并上传。
<div id="uploadArea"><p>请先复制图片后再执行粘贴操作</p>
</div>const IMAGE_MIME_REGEX = /^image\/(jpe?g|gif|png)$/i;
const uploadAreaEle = document.querySelector("#uploadArea");uploadAreaEle.addEventListener("paste", async (e) => {e.preventDefault();const files = [];if (navigator.clipboard) {let clipboardItems = await navigator.clipboard.read();for (const clipboardItem of clipboardItems) {for (const type of clipboardItem.types) {if (IMAGE_MIME_REGEX.test(type)) {const blob = await clipboardItem.getType(type);insertImage(blob, uploadAreaEle);files.push(blob);}}}} else {const items = e.clipboardData.items;for (let i = 0; i < items.length; i++) {if (IMAGE_MIME_REGEX.test(items[i].type)) {let file = items[i].getAsFile();insertImage(file, uploadAreaEle);files.push(file);}}}if (files.length > 0) {confirm("剪贴板检测到图片文件,是否执行上传操作?") && upload({url: "/multiple",files,});}
});function previewImage(file, container) {const reader = new FileReader();reader.onload = function (e) {let img = document.createElement("img");img.src = e.target.result;container.append(img);};reader.readAsDataURL(file);
}function upload({ url, files, fieldName = "file" }) {let formData = new FormData();files.forEach((file) => {let fileName = +new Date() + "." + IMAGE_MIME_REGEX.exec(file.type)[1];formData.append(fieldName, file, fileName);});request.post(url, formData);
}

比较特殊的是大文件分块上传: 利用 Blob.slice 方法对大文件按照指定的大小进行切割,然后通过多线程进行分块上传,等所有分块都成功上传后,再通知服务端进行分块合并。

大文件并发上传的完整流程如下:

服务器上传

是指文件从一台服务器上传到另外一台服务器,可以借助form-data 这个库实现。具体是通过 fs.createReadStream API 创建可读流,然后调用 FormData 对象的 append 方法添加表单项,最后再调用 submit 方法执行提交操作


const fs = require("fs");
const path = require("path");
const FormData = require("form-data");/** 单文件上传 */
const form1 = new FormData();
form1.append("file", fs.createReadStream(path.join(__dirname, "images/image-1.jpeg")));
form1.submit("http://localhost:3000/upload/single", (error, response) => {if(error) {console.log("单图上传失败");return;}console.log("单图上传成功");
})/** 多文件上传 */
const form2 = new FormData();
form2.append("file", fs.createReadStream(path.join(__dirname, "images/image-2.jpeg")));
form2.append("file", fs.createReadStream(path.join(__dirname, "images/image-3.jpeg")));
form2.submit("http://localhost:3000/upload/multiple", (error, response) => {if(error) {console.log("多图上传失败");return;}console.log("多图上传成功");
});

文件下载

在 JavaScript 中 Blob 类型的对象表示一个不可变、原始数据的类文件对象。 它的数据可以按文本或二进制的格式进行读取,也可以转换成  ReadableStream 用于数据操作。

/*** blobParts:它是一个由 ArrayBuffer,ArrayBufferView,Blob,DOMString 等对象构成的数组。其中,DOMStrings 会被编码为 UTF-8。* options:type —— 代表了将会被放入到 blob 中的数组内容的 MIME 类型,默认值为 ""。*          endings —— 用于指定包含行结束符 \n 的字符串如何被写入。 默认值为 "transparent",代表会保持 blob 中保存的结束符不变。"native",代表行结束符会被更改为适合宿主操作系统文件系统的换行符。*/
new Blob(blobParts, options);

a 标签下载

图片下载的功能是借助 dataUrlToBlob 和 saveFile 这两个函数来实现。它们分别用于实现 Data URLs => Blob 的转换和文件的保存。

function dataUrlToBlob(base64, mimeType) {let bytes = window.atob(base64.split(",")[1]);let ab = new ArrayBuffer(bytes.length);let ia = new Uint8Array(ab);for (let i = 0; i < bytes.length; i++) {ia[i] = bytes.charCodeAt(i);}return new Blob([ab], { type: mimeType });
}// 保存文件
function saveFile(blob, filename) {const a = document.createElement("a");// HTMLAnchorElement.download 属性的作用是表明链接的资源将被下载,而不是显示在浏览器中。a.download = filename;// 创建 Object URL,并把返回的 URL 赋值给 a 元素的 href 属性a.href = URL.createObjectURL(blob);// 调用 a 元素的 click 方法来触发文件的下载操作,a.click();// 调用 URL.revokeObjectURL 方法从内部映射中删除引用,从而允许删除 Blob 来释放内存URL.revokeObjectURL(a.href)
}

showSaveFilePicker API 下载

Window.showSaveFilePicker(options) 该方法会显示允许用户选择保存路径的文件选择器,该方法会返回一个 FileSystemFileHandle 对象,FileSystemFileHandle.createWritable 方法返回FileSystemWritableFileStream对象支持将数据(blob)写入文件中。

async function saveFile(blob, filename) {try {const handle = await window.showSaveFilePicker({suggestedName: filename,types: [{description: "PNG file",accept: {"image/png": [".png"],},},{description: "Jpeg file",accept: {"image/jpeg": [".jpeg"],},},],});const writable = await handle.createWritable();await writable.write(blob);await writable.close();return handle;} catch (err) {console.error(err.name, err.message);}
}

FileSaver 下载

借助 FileSaver.js 提供的 saveAs 方法来保存文件。saveAs 方法支持 3 个参数,第 1 个参数表示它支持 Blob/File/Url 三种类型,第 2 个参数表示文件名(可选),而第 3 个参数表示配置对象(可选)。

 saveAs(imgBlob, "face.png");

Zip 下载

借助 JSZip 可以实现压缩多文件并下载的功能。

const images = ["1.png", "2.png", "3.png"];
const imageUrls = images.map((name) => "../images/" + name);// 从指定的url上下载文件内容
function getFileContent(fileUrl) {return new JSZip.external.Promise(function (resolve, reject) {// 调用jszip-utils库提供的getBinaryContent方法获取文件内容JSZipUtils.getBinaryContent(fileUrl, function (err, data) {if (err) {reject(err);} else {resolve(data);}});});
}async function download() {let zip = new JSZip();Promise.all(imageUrls.map(getFileContent)).then((contents) => {contents.forEach((content, i) => {zip.file(images[i], content);});zip.generateAsync({ type: "blob" }).then(function (blob) {saveAs(blob, "material.zip");});});
}

附件形式下载

服务端场景,通过设置 Content-Disposition 响应头来指示响应的内容以何种形式展示,是以内联(inline)的形式,还是以附件(attachment)的形式下载并保存到本地。

// attachment/file-server.js
const fs = require("fs");
const path = require("path");
const Koa = require("koa");
const Router = require("@koa/router");const app = new Koa();
const router = new Router();
const PORT = 3000;
const STATIC_PATH = path.join(__dirname, "./static/");// http://localhost:3000/file?filename=mouth.png
router.get("/file", async (ctx, next) => {const { filename } = ctx.query;const filePath = STATIC_PATH + filename;const fStats = fs.statSync(filePath);ctx.set({"Content-Type": "application/octet-stream","Content-Disposition": `attachment; filename=${filename}`,"Content-Length": fStats.size,});ctx.body = fs.createReadStream(filePath);
});// 注册中间件
app.use(async (ctx, next) => {try {await next();} catch (error) {// ENOENT(无此文件或目录):通常是由文件操作引起的,这表明在给定的路径上无法找到任何文件或目录ctx.status = error.code === "ENOENT" ? 404 : 500;ctx.body = error.code === "ENOENT" ? "文件不存在" : "服务器开小差";}
});
app.use(router.routes()).use(router.allowedMethods());app.listen(PORT, () => {console.log(`应用已经启动:http://localhost:${PORT}/`);
});

base64 格式(Data URLs)下载

利用 axios 实例的 get 方法发起 HTTP 请求来获取指定的 base64 格式图片。然后先将 base64 字符串转换成 blob 对象,再调用 FileSaver 提供的 saveAs 方法下载保存文件到客户端:

const picSelectEle = document.querySelector("#picSelect");
const imgPreviewEle = document.querySelector("#imgPreview");picSelectEle.addEventListener("change", (event) => {imgPreviewEle.src = "./static/" + picSelectEle.value + ".png";
});const request = axios.create({baseURL: "http://localhost:3000",timeout: 60000,
});async function download() {const response = await request.get("/file", {params: {filename: picSelectEle.value + ".png",},});if (response && response.data && response.data.code === 1) {const fileData = response.data.data;const { name, type, content } = fileData;// 将 base64 字符串(data urls)转换成 blob 对象const imgBlob = base64ToBlob(content, type);saveAs(imgBlob, name);}
}

对图片进行 Base64 编码的操作是定义在 /file 路由对应的路由处理器中,调用 Buffer 对象的 toString 方法对文件内容进行 Base64 编码,最终所下载的图片将以 Base64 格式返回到客户端:

// base64/file-server.js
const fs = require("fs");
const path = require("path");
const mime = require("mime");
const Koa = require("koa");
const cors = require("@koa/cors");
const Router = require("@koa/router");const app = new Koa();
const router = new Router();
const PORT = 3000;
const STATIC_PATH = path.join(__dirname, "./static/");router.get("/file", async (ctx, next) => {const { filename } = ctx.query;const filePath = STATIC_PATH + filename;const fileBuffer = fs.readFileSync(filePath);ctx.body = {code: 1,data: {name: filename,type: mime.getType(filename),content: fileBuffer.toString("base64"),},};
});// 注册中间件
app.use(async (ctx, next) => {try {await next();} catch (error) {ctx.body = {code: 0,msg: "服务器开小差",};}
});
app.use(cors());
app.use(router.routes()).use(router.allowedMethods());app.listen(PORT, () => {console.log(`应用已经启动:http://localhost:${PORT}/`);
});

chunked 分块下载

适用于要传输大量的数据,但是在请求在没有被处理完之前响应的长度是无法获得的场景。要使用分块传输编码,则需要在响应头配置 Transfer-Encoding 字段,并设置它的值为 chunked 或 gzip, chunked

/** 带 chunked 表示数据以一系列分块的形式进行发送 */
Transfer-Encoding: chunked
或
Transfer-Encoding: gzip, chunked

而且响应报文中不能出现与之互斥的字段  Content-Length 。

具体客户端实现逻辑——浏览器端通过 Fetch API 获取,以流的形式进行接收数据,用 ReadableStream.getReader() 创建一个读取器,最后调用 reader.read 方法来读取已返回的分块数据,如果收到的分块非 终止块result.done 的值是 false,则会继续调用 readChunk 方法来读取分块数据。而当接收到 终止块 之后,表示分块数据已传输完成。此时,result.done 属性就会返回 true。从而会自动调用 onChunkedResponseComplete 函数,在该函数内部,我们以解码后的文本作为参数来创建 Blob 对象。之后,继续使用 FileSaver 库提供的 saveAs 方法实现文件下载:

const chunkedUrl = "http://localhost:3000/file?filename=file.txt";function download() {return fetch(chunkedUrl).then(processChunkedResponse).then(onChunkedResponseComplete).catch(onChunkedResponseError);
}function processChunkedResponse(response) {let text = "";let reader = response.body.getReader();let decoder = new TextDecoder();return readChunk();function readChunk() {return reader.read().then(appendChunks);}function appendChunks(result) {let chunk = decoder.decode(result.value || new Uint8Array(), {stream: !result.done,});console.log("已接收到的数据:", chunk);console.log("本次已成功接收", chunk.length, "bytes");text += chunk;console.log("目前为止共接收", text.length, "bytes\n");if (result.done) {return text;} else {return readChunk();}}
}function onChunkedResponseComplete(result) {let blob = new Blob([result], {type: "text/plain;charset=utf-8",});saveAs(blob, "hello.txt");
}function onChunkedResponseError(err) {console.error(err);
}

服务器端利用 fs.createReadStream(filePath) 创建数据的可读流,返回给客户端。

范围下载

在服务端支持 Range 请求首部的前提条件下,在一个HTTP Range 首部中,可以一次性请求多个部分,服务器会以 multipart 文件的形式将其返回。如果服务器返回的是范围响应,需要使用 206 Partial Content 状态码。假如所请求的范围不合法,那么服务器会返回 416 Range Not Satisfiable 状态码,表示客户端错误。服务器允许忽略 Range 首部,从而返回整个文件,状态码用 200 。

Range 的语法:

Range: <unit>=<range-start>-
Range: <unit>=<range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>unit:范围请求所采用的单位,通常是字节(bytes)。
<range-start>:一个整数,表示在特定单位下,范围的起始值。
<range-end>:一个整数,表示在特定单位下,范围的结束值。这个值是可选的,如果不存在,表示此范围一直延伸到文档结束。

借助 xhr 对象设置HTTP 请求头的 range,实现范围下载:

function getBinaryContent(url, start, end, responseType = "arraybuffer") {return new Promise((resolve, reject) => {try {let xhr = new XMLHttpRequest();xhr.open("GET", url, true);xhr.setRequestHeader("range", `bytes=${start}-${end}`);xhr.responseType = responseType;xhr.onload = function () {resolve(xhr.response);};xhr.send();} catch (err) {reject(new Error(err));}});
}

服务端则可以直接借助 koa-range 中间件来实现范围请求的响应。

大文件分块下载

在服务端支持 Range 请求首部的前提条件下,大文件并发下载的完整流程如下:

总结

文件上传与下载的场景比较场景, 其实在处理文件的过程中,可以使用 gzipdeflate 或 br 等压缩算法对文件进行压缩,提高上传与下载的传输效率。

文件上传与下载的场景梳理相关推荐

  1. java spring文件下载_SpringMVC实现文件上传和下载的工具类

    本文主要目的是记录自己基于SpringMVC实现的文件上传和下载的工具类的编写,代码经过测试可以直接运行在以后的项目中. 开发的主要思路是对上传和下载文件进行抽象,把上传和下载的核心功能抽取出来分装成 ...

  2. SpringBoot+MongoDB GridFS文件上传、下载、预览实战

    SpringBoot + MongoDB GridFS 随着web 3.0的兴起,数据的形式不局限于文字,还有语音.视频.图片等.高效存储与检索二进制数据也成为web 3.0必须要考虑的问题.然而这种 ...

  3. 超详细的文件上传和下载(Spring Boot)

    超详细的文件上传和下载 前言Ⅰ:@RequestParam和@RequestPart的区别 @RequestPart @RequestPart这个注解用在multipart/form-data表单提交 ...

  4. 基于MFT文件上传和下载

    1. MFT介绍 Managed File Transfer ("MFT")是一种安全的数据传输软件,是通过网络从一台计算机到另一台计算机的数据传输. 大文件传输(MFT)是一种安 ...

  5. 文件上传,下载,预览,删除(File),分页接口

    文件上传,下载,预览,删除(File) 1.公共参数方法 1.1公共返回类型定义 1.2 分页接口 1.3公共实体类 1.4 公共的 mapper.java/xml(都放在一起) 1.4.1 File ...

  6. PHP网站设计 ---- 网盘(实现用户注册、登录,文件上传、下载、删除、查看等功能)

    PHP网站设计 ---- 网盘(实现用户注册.登录,文件上传.下载.删除.查看等功能) 运行效果 视频演示 项目下载(在xampp/htdocs/下可以直接运行) 完整项目包.zip 功能要求 当用户 ...

  7. Angular 文件上传与下载

    Angular文件上传与下载 文件上传 方式1 使用NG ZORRO中的组件. 文件下载 方式1 直接下载 方式2 通过HTTP请求后端数据的方式进行下载 文件上传 方式1 使用NG ZORRO中的组 ...

  8. SpringBoot下文件上传与下载的实现

    原文:http://blog.csdn.net/colton_null/article/details/76696674 SpringBoot后台如何实现文件上传下载? 最近做的一个项目涉及到文件上传 ...

  9. Python实现阿里云aliyun服务器里的文件上传与下载

    Python实现阿里云服务器里的文件上传与下载 Python实现阿里云服务器里的文件上传与下载 背景: 正文: 预备环境: 构想: 实现: 注意: 结尾 018.4.15 背景: 老实说,因为现实的各 ...

最新文章

  1. 准备战争“软测试”之DB基础知识
  2. (转载)从金岳霖到哥德尔
  3. (王道408考研操作系统)第五章输入/输出(I/O)管理-第一节2:I/O控制器
  4. 个人知识整理(javascript篇初识)
  5. 调用高德API实现数据可视化
  6. 沉浸式体验,文化与科技融合创新的新业态
  7. STM8S自学笔记-001 STM8简介
  8. Java--制作乱字游戏
  9. mysql phpwind_php+mysql及phpwind和wordpress的安装配置
  10. 【Domoticz】玩转Domoticz平台——配合ESPEasy固件,开个头,以后玩起来起来再更新博客
  11. 解决给word中表格设置“跨页断行”后出现大片空白
  12. ueditor编辑器右键粘贴、复制不能用的解决办法
  13. CCleaner解决的三个问题
  14. 工作日志3——模型代码
  15. 宗镜录略讲——南怀瑾老师——系列9
  16. HTML+CSS+JavaScript做一个简约的浏览器主页
  17. 图解算法英文版资源,阅读笔记及代码(Python)
  18. @Validated和@Valid的简单总结
  19. 软件测试Mysql题库_软件测试面试常见数据库考题及答案
  20. IP地址与子网掩码(扫盲)

热门文章

  1. 【人工智能】Embodied AI 技术解释:具身人工智能
  2. 【Adobe Acrobat】裁剪PDF文件中的一小部分并保存成单独页
  3. 计算机高新办公软件应用考试,全国计算机信息高新技术考试办公软件中级操作员考试题库...
  4. 系统试运行报告是谁写的_地表水水质自监测站验收报告编制
  5. Windows windows7 关闭隧道适配器
  6. 如何让文本超出 部分/文本框 显示省略号
  7. 2021年煤矿采煤机(掘进机)操作考试及煤矿采煤机(掘进机)操作模拟试题
  8. python中奇怪的知识又增加了
  9. 电商-分享时短链接生成方案
  10. Oracle 数据库集群常用巡检命令