JavaScript進階之前端文件上傳和下載示例詳解
文件下載
1.通過a標簽點擊直接下載
<a href="https:xxx.xlsx" rel="external nofollow" download="test">下載文件</a>
download屬性標識文件需要下載且下載名稱為test
如果有 Content-Disposition 響應(yīng)頭,則不需要設(shè)置download屬性就能下載,文件名在響應(yīng)頭里面由后端控制
此方法有同源和請求headers鑒權(quán)的問題
2.open或location.href
window.open('xxx.zip');
location.href = 'xxx.zip';
需要注意 url 長度和編碼問題
不能直接下載瀏覽器默認預(yù)覽的文件,如txt、圖片
3.Blob和Base64
function downloadFile(res, Filename) {
// res為接口返回數(shù)據(jù),在請求接口的時候可進行鑒權(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標簽并釋放代碼一致,可從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對象
}
注意 請求發(fā)送的時候注明 responseType = "blob",如無設(shè)置則需要 new Blob的時候傳入第二個參數(shù),如
new Blob([res], { type: xhr.getResponseHeader("Content-Type") });
此方法可以解決請求headers鑒權(quán)和下載瀏覽器默認直接預(yù)覽的文件,并得知下載進度
文件上傳
文件上傳思路

File文件
- MDN描述

上傳單個文件-客戶端
<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">上傳進度:<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;
}
};
// 上傳進度
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 庫處理 multipart/form-data

const app = express();
// 上傳成功后返回URL地址
const resourceUrl = `http://127.0.0.1:${port}/`;
// 存儲文件目錄
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是否允許多個值(相關(guān)類型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) {
// 最后一個切片以實際結(jié)束大小為準。
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è)置默認切片大小為5M
const DefaultChunkSize = 5 * 1024 * 1024;
const start = async function (bigFile) {
// 生成多個切片
const fileList = handleFileChunk(bigFile, DefaultChunkSize);
// 獲取整個大文件的內(nèi)容hash,方便實現(xiàn)秒傳
// const containerHash = await getFileHash(fileList);
const containerHash = await getFileHash2(bigFile);
// 給每個切片添加輔助內(nèi)容信息
const chunksInfo = fileList.map(({ file }, index) => ({
// 整個文件hash
fileHash: containerHash,
// 當(dāng)前切片的hash
hash: containerHash + "-" + index,
// 當(dāng)前是第幾個切片
index,
// 文件個數(shù)
fileCount: fileList.length,
// 切片內(nèi)容
chunk: file,
// 文件總體大小
totalSize: bigFile.size,
// 單個文件大小
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();
// 獲取全部內(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();
// 獲取全部內(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 }) => {
//生成每個切片上傳的信息
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,
};
大文件上傳-服務(wù)端

...
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");
//設(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;
//總共切片個數(shù)
const [fileCount] = fields.fileCount;
//當(dāng)前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);
// 大文件存在直接返回,根據(jù)內(nèi)容hash存儲,可以實現(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:切片存放的臨時目錄
* splitSize:每個切片的大小
* fileCount:文件總個數(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ù)量不對,拋出錯誤
if (filterPath.length !== fileCount) {
console.log("合并錯誤");
return;
}
// 根據(jù)切片下標進行排序,方便合并
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 切片存儲目錄
* @param {any} totalCount 大文件包含切片個數(shù)
* @param {any} hash 大文件hash
* @returns
*/
export function checkFileIsMerge(pathName, totalCount, hash) {
var dirs = [];
//同步讀取切片存儲目錄
const readDir = fse.readdirSync(pathName);
//判斷目錄下切片數(shù)量 小于 總切片數(shù),不能合并
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);
//數(shù)量一直,可以合并
if (dirs.length === totalCount) {
return true;
}
return false;
}
這里的大文件上傳有幾處問題,我沒有解決,留給各位思考啦
- 內(nèi)容hash計算速度如何提升(serviceworker)
- 文件上傳進度
- 斷點續(xù)傳
以上就是JavaScript進階之前端文件上傳和下載示例詳解的詳細內(nèi)容,更多關(guān)于JavaScript前端文件上傳下載的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript在IE中“意外地調(diào)用了方法或?qū)傩栽L問”
FF是正常的,IE報“意外地調(diào)用了方法或?qū)傩栽L問”。2008-11-11
JS解決Date對象在IOS中的“大坑” 以及時間格式兼容問題
這篇文章主要介紹了JS解決Date對象在IOS中的“大坑” 以及時間格式兼容問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-10-10
JS實現(xiàn)網(wǎng)頁上隨滾動條滾動的層效果代碼
這篇文章主要介紹了JS實現(xiàn)網(wǎng)頁上隨滾動條滾動的層效果代碼,涉及JavaScript頁面元素屬性的獲取、運算及設(shè)置等操作技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-11-11
Javascript保存網(wǎng)頁為圖片借助于html2canvas庫實現(xiàn)
借助于html2canvas庫,把網(wǎng)頁保存為Canvas畫布,再把生成的canvas保存成圖片,下面的示例,大家可以看看2014-09-09

