目录
- 文件下载
- 1.通过a标签点击直接下载
- 2.open或location.href
- 3.Blob和Base64
- 文件上传
- 文件上传思路
- File文件
- 上传单个文件-客户端
- 上传文件-服务端
- 多文件上传-客户端
- 大文件上传-客户端
- 大文件上传-服务端
文件下载
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)
- 文件上传进度
- 断点续传