JavaScript进阶之前端文件上传和下载示例详解

作者:云牧 时间:2024-06-12 11:24:46 

文件下载

1.通过a标签点击直接下载

<a href="https:xxx.xlsx" rel="external nofollow"  download="test">下载文件</a>

download属性标识文件需要下载且下载名称为test

如果有 Content-Disposition 响应头,则不需要设置download属性就能下载,文件名在响应头里面由后端控制

此方法有同源和请求headers鉴权的问题

2.open或location.href

window.open('xxx.zip');
location.href = 'xxx.zip';

需要注意 url 长度和编码问题

不能直接下载浏览器默认预览的文件,如txt、图片

3.Blob和Base64

function downloadFile(res, Filename) {
 // res为接口返回数据,在请求接口的时候可进行鉴权
 if (!res) return;
 // IE及IE内核浏览器
 if ("msSaveOrOpenBlob" in navigator) {
   navigator.msSaveOrOpenBlob(res, name);
   return;
 }
 const url = URL.createObjectURL(new Blob([res]));
 //  const fileReader = new FileReader();  使用 Base64 编码生成
 // fileReader.readAsDataURL(res);
 // fileReader.onload = function() { ...此处逻辑和下面创建a标签并释放代码一致,可从fileReader.result获取href值...}
 const a = document.createElement("a");
 a.style.display = "none";
 a.href = url;
 a.download = Filename;
 document.body.appendChild(a);
 a.click();
 document.body.removeChild(a);
 URL.revokeObjectURL(url); // 释放blob对象
}

注意 请求发送的时候注明 responseType = "blob",如无设置则需要 new Blob的时候传入第二个参数,如

new Blob([res], { type: xhr.getResponseHeader("Content-Type") });

此方法可以解决请求headers鉴权和下载浏览器默认直接预览的文件,并得知下载进度

文件上传

文件上传思路

JavaScript进阶之前端文件上传和下载示例详解

File文件

  • MDN描述

JavaScript进阶之前端文件上传和下载示例详解

上传单个文件-客户端

<input id="uploadFile" type="file" accept="image/*" />
  • type属性file:用户选择文件

  • accept属性:规定选择文件的类型

JavaScript进阶之前端文件上传和下载示例详解

<body>
   <input id="uploadFile" type="file" accept="image/*" />
   <button type="button" id="uploadBtn" onClick="startUpload()">开始上传</button>
   <div class="progress">上传进度:<span id="progressValue">0</span></div>
   <div id="uploadResult" class="result"></div>
   <script>
     const uploadFileEle = document.getElementById("uploadFile");
     const progressValueEle = document.getElementById("progressValue");
     const uploadResultEle = document.getElementById("uploadResult");
     try {
       function startUpload() {
         if (!uploadFileEle.files.length) return;
         // 获取文件
         const file = uploadFileEle.files[0];
         // 创建上传数据
         const formData = new FormData();
         formData.append("file", file);
         // 上传文件
         upload(formData);
       }
       function upload(data) {
         const xhr = new XMLHttpRequest();
         xhr.onreadystatechange = function () {
           if (xhr.readyState === 4 && xhr.status === 200) {
             const result = JSON.parse(xhr.responseText);
             console.log("result:", result);
             uploadResultEle.innerText = xhr.responseText;
           }
         };
         // 上传进度
         xhr.upload.onprogress = function (event) {
           if (event.lengthComputable) {
             progressValueEle.innerText = Math.ceil((event.loaded * 100) / event.total) + "%";
           }
         };
         xhr.open("POST", "http://127.0.0.1:3000/upload", true);
         xhr.send(data);
       }
     } catch (e) {
       console.log("error:", e);
     }
   </script>
</body>

上传文件-服务端

  • 客户端使用form-data传递,服务端使用相同方式接受解析

  • 使用 multer 库处理 multipart/form-data

JavaScript进阶之前端文件上传和下载示例详解

const app = express();
// 上传成功后返回URL地址
const resourceUrl = `http://127.0.0.1:${port}/`;
// 存储文件目录
const uploadDIr = path.join(__dirname, "/upload");
// destination 设置资源保存路径,filename 设置资源名称
const storage = multer.diskStorage({
 destination: async function (_req, _file, cb) {
   cb(null, uploadDIr);
 },
 filename: function (_req, file, cb) {
   // 设置文件名
   cb(null, `${file.originalname}`);
 },
});
const multerUpload = multer({ storage });
//设置静态访问目录
app.use(express.static(uploadDIr));
app.post("/upload", multerUpload.any(), function (req, res, _next) {
 // req.file 是 `avatar` 文件的信息
 let urls = [];
 //获取所有已上传的文件
 const files = req.files;
 if (files && files.length > 0) {
   //遍历生成url 集合返回给客户端
   urls = files.map((item, _key) => {
     return resourceUrl + item.originalname;
   });
 }
 return res.json({
   REV: true,
   DATA: {
     url: urls,
   },
   MSG: "成功",
 });
});

多文件上传-客户端

  • input属性:multiple是否允许多个值(相关类型emailfile )

<body>
   <input id="uploadFile" type="file" accept="image/*" multiple />
   <button id="uploadBtn" onClick="startUpload()">开始上传</button>
   <div class="progress">上传进度:<span id="progressValue">0</span></div>
   <div id="uploadResult" class="result"></div>
   <script>
     const uploadFileEle = document.getElementById("uploadFile");
     const progressValueEle = document.getElementById("progressValue");
     const uploadResultEle = document.getElementById("uploadResult");
     try {
       function startUpload() {
         if (!uploadFileEle.files.length) return;
         //获取文件
         const files = uploadFileEle.files;
         const formData = this.getUploadData(files);
         this.upload(formData);
       }
       //添加多个文件
       function getUploadData(files) {
         const formData = new FormData();
         for (let i = 0; i < files.length; i++) {
           const file = files[i];
           formData.append(file.name, file);
         }
         return formData;
       }
       function upload(data) {
         const xhr = new XMLHttpRequest();
         xhr.onreadystatechange = function () {
           if (xhr.readyState === 4 && xhr.status === 200) {
             const result = JSON.parse(xhr.responseText);
             console.log("result:", result);
             uploadResultEle.innerText = xhr.responseText;
           }
         };
         xhr.upload.addEventListener(
           "progress",
           function (event) {
             if (event.lengthComputable) {
               progressValueEle.innerText = Math.ceil((event.loaded * 100) / event.total) + "%";
             }
           },
           false
         );
         xhr.open("POST", "http://127.0.0.1:3000/upload", true);
         xhr.send(data);
       }
     } catch (e) {
       console.log("error:", e);
     }
   </script>
 </body>

大文件上传-客户端

JavaScript进阶之前端文件上传和下载示例详解

<body>
   <input id="uploadFile" type="file" />
   <button type="button" id="uploadBtn" onClick="startUpload()">开始上传</button>
   <div class="progress">上传进度:<span id="progressValue">0</span></div>
   <div id="uploadResult" class="result"></div>
   <script src="./fileUtils.js"></script>
   <script src="./spark-md5.min.js"></script>
   <script src="./index.js"></script>
   <script>
     const uploadFileEle = document.getElementById("uploadFile");
     const progressValueEle = document.getElementById("progressValue");
     const uploadResultEle = document.getElementById("uploadResult");
     try {
       function startUpload() {
         if (!uploadFileEle.files.length) return;
         //获取文件
         const file = uploadFileEle.files[0];
         window.upload.start(file);
       }
     } catch (e) {
       console.log("error:", e);
     }
   </script>
 </body>

fileUtils

// 文件分片
function handleFileChunk(file, chunkSize) {
   const fileChunkList = [];
   // 索引值
   let curIndex = 0;
   while (curIndex < file.size) {
       // 最后一个切片以实际结束大小为准。
       const endIndex = curIndex + chunkSize < file.size ? curIndex + chunkSize : file.size;
       // 截取当前切片大小
       const curFileChunkFile = file.slice(curIndex, endIndex);
       // 更新当前索引
       curIndex += chunkSize;
       fileChunkList.push({ file: curFileChunkFile });
   }
   return fileChunkList;
}
//设置默认切片大小为5M
const DefaultChunkSize = 5 * 1024 * 1024;
const start = async function (bigFile) {
 // 生成多个切片
 const fileList = handleFileChunk(bigFile, DefaultChunkSize);
 // 获取整个大文件的内容hash,方便实现秒传
 // const containerHash = await getFileHash(fileList);
 const containerHash = await getFileHash2(bigFile);
 // 给每个切片添加辅助内容信息
 const chunksInfo = fileList.map(({ file }, index) => ({
   // 整个文件hash
   fileHash: containerHash,
   // 当前切片的hash
   hash: containerHash + "-" + index,
   // 当前是第几个切片
   index,
   // 文件个数
   fileCount: fileList.length,
   // 切片内容
   chunk: file,
   // 文件总体大小
   totalSize: bigFile.size,
   // 单个文件大小
   size: file.size,
 }));
 //上传所有文件
 uploadChunks(chunksInfo, bigFile.name);
};
/**
*
* 获取全部文件内容hash
* @param {any} fileList
*/
async function getFileHash(fileList) {
 console.time("filehash");
 const spark = new SparkMD5.ArrayBuffer();
 // 获取全部内容
 const result = fileList.map((item, key) => {
   return getFileContent(item.file);
 });
 try {
   const contentList = await Promise.all(result);
   for (let i = 0; i < contentList.length; i++) {
     spark.append(contentList[i]);
   }
   // 生成指纹
   const res = spark.end();
   console.timeEnd("filehash");
   return res;
 } catch (e) {
   console.log(e);
 }
}
/**
*
* 获取全部文件内容hash
* @param {any} fileList
*/
async function getFileHash2(fileList) {
 console.time("filehash");
 const spark = new SparkMD5.ArrayBuffer();
 // 获取全部内容
 const content = await getFileContent(fileList);
 try {
   spark.append(content);
   // 生成指纹
   const result = spark.end();
   console.timeEnd("filehash");
   return result;
 } catch (e) {
   console.log(e);
 }
}
/**
*
* 获取文件内容
* @param {any} file
*/
function getFileContent(file) {
 return new Promise((resolve, reject) => {
   const fileReader = new FileReader();
   // 读取文件内容
   fileReader.readAsArrayBuffer(file);
   fileReader.onload = (e) => {
     // 返回读取到的文件内容
     resolve(e.target.result);
   };
   fileReader.onerror = (e) => {
     reject(fileReader.error);
     fileReader.abort();
   };
 });
}
/**
*
* 上传所有的分片
* @param {any} chunks
* @param {any} fileName
*/
async function uploadChunks(chunks, fileName) {
 const requestList = chunks
   .map(({ chunk, hash, fileHash, index, fileCount, size, totalSize }) => {
     //生成每个切片上传的信息
     const formData = new FormData();
     formData.append("hash", hash);
     formData.append("index", index);
     formData.append("fileCount", fileCount);
     formData.append("size", size);
     formData.append("splitSize", DefaultChunkSize);
     formData.append("fileName", fileName);
     formData.append("fileHash", fileHash);
     formData.append("chunk", chunk);
     formData.append("totalSize", totalSize);
     return { formData, index };
   })
   .map(async ({ formData, index }) =>
     singleRequest({
       url: "http://127.0.0.1:3000/uploadBigFile",
       data: formData,
     })
   );
 //全部上传
 await Promise.all(requestList);
}
/**
* 单个文件上传
*/
function singleRequest({ url, method = "post", data, headers = {} }) {
 return new Promise((resolve) => {
   const xhr = new XMLHttpRequest();
   xhr.open(method, url);
   Object.keys(headers).forEach((key) => xhr.setRequestHeader(key, headers[key]));
   xhr.send(data);
   xhr.onload = (e) => {
     resolve({
       data: e.target.response,
     });
   };
 });
}
window.upload = {
 start: start,
};

大文件上传-服务端

JavaScript进阶之前端文件上传和下载示例详解

...
import { checkFileIsMerge, chunkMerge } from "./upload";
const multiparty = require("multiparty");
const fse = require("fs-extra");
// 上传成功后返回URL地址
const resourceUrl = `http://127.0.0.1:${port}/`;
// 存储文件目录
const uploadDIr = path.join(__dirname, "/upload");
//设置静态访问目录
app.use(express.static(uploadDIr));
const extractExt = (filename) => filename.slice(filename.lastIndexOf("."), filename.length); // 提取后缀名
app.post("/uploadBigFile", function (req, res, _next) {
 const multipart = new multiparty.Form();
 multipart.parse(req, async (err, fields, files) => {
   if (err) {
     console.error(err);
     return res.json({
       code: 5000,
       data: null,
       msg: "上传文件失败",
     });
   }
   //取出文件内容
   const [chunk] = files.chunk;
   //当前chunk 文件hash
   const [hash] = fields.hash;
   //大文件的hash
   const [fileHash] = fields.fileHash;
   //大文件的名称
   const [fileName] = fields.fileName;
   //切片索引
   const [index] = fields.index;
   //总共切片个数
   const [fileCount] = fields.fileCount;
   //当前chunk 的大小
   // const [size] = fields.size;
   const [splitSize] = fields.splitSize;
   //整个文件大小
   const [totalSize] = fields.totalSize;
   const saveFileName = `${fileHash}${extractExt(fileName)}`;
   //获取整个文件存储路径
   const filePath = path.resolve(uploadDIr, saveFileName);
   const chunkDir = path.resolve(uploadDIr, fileHash);
   // 大文件存在直接返回,根据内容hash存储,可以实现后续秒传
   if (fse.existsSync(filePath)) {
     return res.json({
       code: 1000,
       data: { url: `${resourceUrl}${saveFileName}` },
       msg: "上传文件已存在",
     });
   }
   // 切片目录不存在,创建切片目录
   if (!fse.existsSync(chunkDir)) {
     await fse.mkdirs(chunkDir);
   }
   const chunkFile = path.resolve(chunkDir, hash);
   if (!fse.existsSync(chunkFile)) {
     await fse.move(chunk.path, path.resolve(chunkDir, hash));
   }
   const isMerge = checkFileIsMerge(chunkDir, Number(fileCount), fileHash);
   if (isMerge) {
     //合并
     await chunkMerge({
       filePath: filePath,
       fileHash: fileHash,
       chunkDir: chunkDir,
       splitSize: Number(splitSize),
       fileCount: Number(fileCount),
       totalSize: Number(totalSize),
     });
     return res.json({
       code: 1000,
       data: { url: `${resourceUrl}${saveFileName}` },
       msg: "文件上传成功",
     });
   } else {
     return res.json({
       code: 200,
       data: { url: `${resourceUrl}${filePath}` },
       msg: "文件上传成功",
     });
   }
 });
});

upload.ts

const fse = require("fs-extra");
const path = require("path");
/**
* 读流,写流
* @param path
* @param writeStream
* @returns
*/
const pipeStream = (path, writeStream) =>
   new Promise((resolve) => {
       const readStream = fse.createReadStream(path);
       readStream.on("end", () => {
           // fse.unlinkSync(path);
           resolve(null);
       });
       readStream.pipe(writeStream);
   });
/**
*
* 合并所有切片
* @export
* @param {any} {
*     filePath:文件路径包含后缀名
*     fileHash:文件hash
*     chunkDir:切片存放的临时目录
*     splitSize:每个切片的大小
*     fileCount:文件总个数
*     totalSize:文件总大小
* }
* @returns
*/
export async function chunkMerge({
   filePath,
   fileHash,
   chunkDir,
   splitSize,
   fileCount,
   totalSize,
}) {
   const chunkPaths = await fse.readdir(chunkDir);
   //帅选合适的切片
   const filterPath = chunkPaths.filter((item) => {
       return item.includes(fileHash);
   });
   //数量不对,抛出错误
   if (filterPath.length !== fileCount) {
       console.log("合并错误");
       return;
   }
   // 根据切片下标进行排序,方便合并
   filterPath.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
   await Promise.all(
       chunkPaths.map((chunkPath, index) => {
           //并发写入,需要知道开始和结束位置
           let end = (index + 1) * splitSize;
           if (index === fileCount - 1) {
               end = totalSize + 1;
           }
           return pipeStream(
               path.resolve(chunkDir, chunkPath),
               // 指定位置创建可写流
               fse.createWriteStream(filePath, {
                   start: index * splitSize,
                   end: end,
               })
           );
       })
   );
   //删除所有切片
   // fse.rmdirSync(chunkDir); // 合并后删除保存切片的目录
   return filePath;
}

/**
*
* 检查切片是否可以合并
* @export
* @param {any} pathName 切片存储目录
* @param {any} totalCount 大文件包含切片个数
* @param {any} hash 大文件hash
* @returns
*/
export function checkFileIsMerge(pathName, totalCount, hash) {
   var dirs = [];
   //同步读取切片存储目录
   const readDir = fse.readdirSync(pathName);
   //判断目录下切片数量 小于 总切片数,不能合并
   if (readDir && readDir.length < totalCount) return false;
   //获取目录下所有真正属于该文件的切片,以大文件hash为准
   (function iterator(i) {
       if (i == readDir.length) {
           return;
       }
       const curFile = fse.statSync(path.join(pathName, readDir[i]));
       //提出目录和文件名不包含大文件hash的文件
       if (curFile.isFile() && readDir[i].includes(hash + "")) {
           dirs.push(readDir[i]);
       }
       iterator(i + 1);
   })(0);
   //数量一直,可以合并
   if (dirs.length === totalCount) {
       return true;
   }
   return false;
}

这里的大文件上传有几处问题,我没有解决,留给各位思考啦

  • 内容hash计算速度如何提升(serviceworker)

  • 文件上传进度

  • 断点续传

来源:https://juejin.cn/post/7148832320277987342

标签:JavaScript,前端,文件上传,文件下载
0
投稿

猜你喜欢

  • 使用Python编写一个简单的tic-tac-toe游戏的教程

    2021-01-26 04:27:43
  • 基于JS实现移动端向左滑动出现删除按钮功能

    2024-04-17 09:51:07
  • Python开发之快速搭建自动回复微信公众号功能

    2022-06-27 03:08:04
  • linux环境下安装python虚拟环境及注意事项

    2023-12-09 08:02:47
  • 如何正确的解决 MySQL中忽略用户的现象

    2008-11-27 16:00:00
  • JavaScript如何获取一个元素的样式信息

    2023-08-28 12:16:17
  • Python如何配置环境变量详解

    2021-11-20 08:18:21
  • python web框架的总结

    2022-01-13 12:22:24
  • Golang中Interface接口的三个特性

    2024-04-23 09:35:46
  • JS实现动态添加外部js、css到head标签的方法

    2024-05-02 16:29:45
  • python应用文件读取与登录注册功能

    2023-04-17 17:04:03
  • 使用Pycharm(Python工具)新建项目及创建Python文件的教程

    2022-06-07 11:43:52
  • 使用Pytorch+PyG实现MLP的详细过程

    2023-05-03 17:48:14
  • z-index在IE中的迷惑

    2007-05-11 16:50:00
  • php+mysqli使用面向对象方式更新数据库实例

    2023-06-23 00:30:23
  • 使用canal监控mysql数据库实现elasticsearch索引实时更新问题

    2024-01-20 22:48:39
  • Python 模板引擎的注入问题分析

    2021-05-26 17:05:30
  • Python version 2.7 required, which was not found in the registry

    2021-06-02 14:57:13
  • MySQL查询倒数第二条记录实现方法

    2024-01-26 07:15:50
  • vue3中使用ref和emit来减少props的使用示例详解

    2024-04-27 16:02:02
  • asp之家 网络编程 m.aspxhome.com