JavaScript進(jìn)階之前端文件上傳和下載示例詳解
文件下載
1.通過a標(biāo)簽點(diǎn)擊直接下載
<a href="https:xxx.xlsx" rel="external nofollow" download="test">下載文件</a>
download
屬性標(biāo)識(shí)文件需要下載且下載名稱為test
如果有 Content-Disposition
響應(yīng)頭,則不需要設(shè)置download
屬性就能下載,文件名在響應(yīng)頭里面由后端控制
此方法有同源和請(qǐng)求headers
鑒權(quán)的問題
2.open或location.href
window.open('xxx.zip'); location.href = 'xxx.zip';
需要注意 url 長(zhǎng)度和編碼問題
不能直接下載瀏覽器默認(rèn)預(yù)覽的文件,如txt
、圖片
3.Blob和Base64
function downloadFile(res, Filename) { // res為接口返回?cái)?shù)據(jù),在請(qǐng)求接口的時(shí)候可進(jìn)行鑒權(quán) if (!res) return; // IE及IE內(nèi)核瀏覽器 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() { ...此處邏輯和下面創(chuàng)建a標(biāo)簽并釋放代碼一致,可從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對(duì)象 }
注意 請(qǐng)求發(fā)送的時(shí)候注明 responseType = "blob"
,如無設(shè)置則需要 new Blob的時(shí)候傳入第二個(gè)參數(shù),如
new Blob([res], { type: xhr.getResponseHeader("Content-Type") });
此方法可以解決請(qǐng)求headers
鑒權(quán)和下載瀏覽器默認(rèn)直接預(yù)覽的文件,并得知下載進(jìn)度
文件上傳
文件上傳思路
File文件
- MDN描述
上傳單個(gè)文件-客戶端
<input id="uploadFile" type="file" accept="image/*" />
type
屬性file
:用戶選擇文件accept
屬性:規(guī)定選擇文件的類型
<body> <input id="uploadFile" type="file" accept="image/*" /> <button type="button" id="uploadBtn" onClick="startUpload()">開始上傳</button> <div class="progress">上傳進(jìn)度:<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]; // 創(chuàng)建上傳數(shù)據(jù) 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; } }; // 上傳進(jìn)度 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>
上傳文件-服務(wù)端
- 客戶端使用form-data傳遞,服務(wù)端使用相同方式接受解析
- 使用 multer 庫(kù)處理 multipart/form-data
const app = express(); // 上傳成功后返回URL地址 const resourceUrl = `http://127.0.0.1:${port}/`; // 存儲(chǔ)文件目錄 const uploadDIr = path.join(__dirname, "/upload"); // destination 設(shè)置資源保存路徑,filename 設(shè)置資源名稱 const storage = multer.diskStorage({ destination: async function (_req, _file, cb) { cb(null, uploadDIr); }, filename: function (_req, file, cb) { // 設(shè)置文件名 cb(null, `${file.originalname}`); }, }); const multerUpload = multer({ storage }); //設(shè)置靜態(tài)訪問目錄 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
是否允許多個(gè)值(相關(guān)類型email
、file
)
<body> <input id="uploadFile" type="file" accept="image/*" multiple /> <button id="uploadBtn" onClick="startUpload()">開始上傳</button> <div class="progress">上傳進(jìn)度:<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); } //添加多個(gè)文件 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">上傳進(jìn)度:<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) { // 最后一個(gè)切片以實(shí)際結(jié)束大小為準(zhǔn)。 const endIndex = curIndex + chunkSize < file.size ? curIndex + chunkSize : file.size; // 截取當(dāng)前切片大小 const curFileChunkFile = file.slice(curIndex, endIndex); // 更新當(dāng)前索引 curIndex += chunkSize; fileChunkList.push({ file: curFileChunkFile }); } return fileChunkList; }
//設(shè)置默認(rèn)切片大小為5M const DefaultChunkSize = 5 * 1024 * 1024; const start = async function (bigFile) { // 生成多個(gè)切片 const fileList = handleFileChunk(bigFile, DefaultChunkSize); // 獲取整個(gè)大文件的內(nèi)容hash,方便實(shí)現(xiàn)秒傳 // const containerHash = await getFileHash(fileList); const containerHash = await getFileHash2(bigFile); // 給每個(gè)切片添加輔助內(nèi)容信息 const chunksInfo = fileList.map(({ file }, index) => ({ // 整個(gè)文件hash fileHash: containerHash, // 當(dāng)前切片的hash hash: containerHash + "-" + index, // 當(dāng)前是第幾個(gè)切片 index, // 文件個(gè)數(shù) fileCount: fileList.length, // 切片內(nèi)容 chunk: file, // 文件總體大小 totalSize: bigFile.size, // 單個(gè)文件大小 size: file.size, })); //上傳所有文件 uploadChunks(chunksInfo, bigFile.name); }; /** * * 獲取全部文件內(nèi)容hash * @param {any} fileList */ async function getFileHash(fileList) { console.time("filehash"); const spark = new SparkMD5.ArrayBuffer(); // 獲取全部?jī)?nèi)容 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); } } /** * * 獲取全部文件內(nèi)容hash * @param {any} fileList */ async function getFileHash2(fileList) { console.time("filehash"); const spark = new SparkMD5.ArrayBuffer(); // 獲取全部?jī)?nèi)容 const content = await getFileContent(fileList); try { spark.append(content); // 生成指紋 const result = spark.end(); console.timeEnd("filehash"); return result; } catch (e) { console.log(e); } } /** * * 獲取文件內(nèi)容 * @param {any} file */ function getFileContent(file) { return new Promise((resolve, reject) => { const fileReader = new FileReader(); // 讀取文件內(nèi)容 fileReader.readAsArrayBuffer(file); fileReader.onload = (e) => { // 返回讀取到的文件內(nèi)容 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 }) => { //生成每個(gè)切片上傳的信息 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); } /** * 單個(gè)文件上傳 */ 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, };
大文件上傳-服務(wù)端
... import { checkFileIsMerge, chunkMerge } from "./upload"; const multiparty = require("multiparty"); const fse = require("fs-extra"); // 上傳成功后返回URL地址 const resourceUrl = `http://127.0.0.1:${port}/`; // 存儲(chǔ)文件目錄 const uploadDIr = path.join(__dirname, "/upload"); //設(shè)置靜態(tài)訪問目錄 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: "上傳文件失敗", }); } //取出文件內(nèi)容 const [chunk] = files.chunk; //當(dāng)前chunk 文件hash const [hash] = fields.hash; //大文件的hash const [fileHash] = fields.fileHash; //大文件的名稱 const [fileName] = fields.fileName; //切片索引 const [index] = fields.index; //總共切片個(gè)數(shù) const [fileCount] = fields.fileCount; //當(dāng)前chunk 的大小 // const [size] = fields.size; const [splitSize] = fields.splitSize; //整個(gè)文件大小 const [totalSize] = fields.totalSize; const saveFileName = `${fileHash}${extractExt(fileName)}`; //獲取整個(gè)文件存儲(chǔ)路徑 const filePath = path.resolve(uploadDIr, saveFileName); const chunkDir = path.resolve(uploadDIr, fileHash); // 大文件存在直接返回,根據(jù)內(nèi)容hash存儲(chǔ),可以實(shí)現(xiàn)后續(xù)秒傳 if (fse.existsSync(filePath)) { return res.json({ code: 1000, data: { url: `${resourceUrl}${saveFileName}` }, msg: "上傳文件已存在", }); } // 切片目錄不存在,創(chuàng)建切片目錄 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:切片存放的臨時(shí)目錄 * splitSize:每個(gè)切片的大小 * fileCount:文件總個(gè)數(shù) * 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); }); //數(shù)量不對(duì),拋出錯(cuò)誤 if (filterPath.length !== fileCount) { console.log("合并錯(cuò)誤"); return; } // 根據(jù)切片下標(biāo)進(jìn)行排序,方便合并 filterPath.sort((a, b) => a.split("-")[1] - b.split("-")[1]); await Promise.all( chunkPaths.map((chunkPath, index) => { //并發(fā)寫入,需要知道開始和結(jié)束位置 let end = (index + 1) * splitSize; if (index === fileCount - 1) { end = totalSize + 1; } return pipeStream( path.resolve(chunkDir, chunkPath), // 指定位置創(chuàng)建可寫流 fse.createWriteStream(filePath, { start: index * splitSize, end: end, }) ); }) ); //刪除所有切片 // fse.rmdirSync(chunkDir); // 合并后刪除保存切片的目錄 return filePath; } /** * * 檢查切片是否可以合并 * @export * @param {any} pathName 切片存儲(chǔ)目錄 * @param {any} totalCount 大文件包含切片個(gè)數(shù) * @param {any} hash 大文件hash * @returns */ export function checkFileIsMerge(pathName, totalCount, hash) { var dirs = []; //同步讀取切片存儲(chǔ)目錄 const readDir = fse.readdirSync(pathName); //判斷目錄下切片數(shù)量 小于 總切片數(shù),不能合并 if (readDir && readDir.length < totalCount) return false; //獲取目錄下所有真正屬于該文件的切片,以大文件hash為準(zhǔn) (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); //數(shù)量一直,可以合并 if (dirs.length === totalCount) { return true; } return false; }
這里的大文件上傳有幾處問題,我沒有解決,留給各位思考啦
- 內(nèi)容hash計(jì)算速度如何提升(serviceworker)
- 文件上傳進(jìn)度
- 斷點(diǎn)續(xù)傳
以上就是JavaScript進(jìn)階之前端文件上傳和下載示例詳解的詳細(xì)內(nèi)容,更多關(guān)于JavaScript前端文件上傳下載的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript在IE中“意外地調(diào)用了方法或?qū)傩栽L問”
FF是正常的,IE報(bào)“意外地調(diào)用了方法或?qū)傩栽L問”。2008-11-11JS解決Date對(duì)象在IOS中的“大坑” 以及時(shí)間格式兼容問題
這篇文章主要介紹了JS解決Date對(duì)象在IOS中的“大坑” 以及時(shí)間格式兼容問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10JS實(shí)現(xiàn)網(wǎng)頁(yè)上隨滾動(dòng)條滾動(dòng)的層效果代碼
這篇文章主要介紹了JS實(shí)現(xiàn)網(wǎng)頁(yè)上隨滾動(dòng)條滾動(dòng)的層效果代碼,涉及JavaScript頁(yè)面元素屬性的獲取、運(yùn)算及設(shè)置等操作技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11JavaScript對(duì)象合并實(shí)現(xiàn)步驟介紹
這篇文章主要介紹了JavaScript對(duì)象合并實(shí)現(xiàn)步驟,為什么要合并?這是我在重構(gòu)代碼的時(shí)候的一個(gè)需求。簡(jiǎn)單來說,我會(huì)有若干個(gè)對(duì)象需要合并為一個(gè)對(duì)象。而這些對(duì)象為兩層,如果直接展開或者賦值會(huì)涉及到深拷貝2023-01-01Javascript保存網(wǎng)頁(yè)為圖片借助于html2canvas庫(kù)實(shí)現(xiàn)
借助于html2canvas庫(kù),把網(wǎng)頁(yè)保存為Canvas畫布,再把生成的canvas保存成圖片,下面的示例,大家可以看看2014-09-09JavaScript中實(shí)現(xiàn)塊作用域的方法
在Javascript中由于沒有作用域的概念,所以很容易發(fā)生標(biāo)識(shí)符名稱的沖突,尤其是在比較大的項(xiàng)目中,這類情況更容易發(fā)生2010-04-04