JavaScript进阶之前端文件上传和下载示例详解
作者:云牧
这篇文章主要为大家介绍了JavaScript进阶之前端文件上传和下载示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
文件下载
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
鉴权和下载浏览器默认直接预览的文件,并得知下载进度
文件上传
文件上传思路
File文件
- MDN描述
上传单个文件-客户端
<input id="uploadFile" type="file" accept="image/*" />
type
属性file
:用户选择文件accept
属性:规定选择文件的类型
<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
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
是否允许多个值(相关类型email
、file
)
<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>
大文件上传-客户端
<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, };
大文件上传-服务端
... 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)
- 文件上传进度
- 断点续传
以上就是JavaScript进阶之前端文件上传和下载示例详解的详细内容,更多关于JavaScript前端文件上传下载的资料请关注脚本之家其它相关文章!