文件流
file-saver
支持各种文件数据类型的文件下载
安装
npm i file-saver -S
使用
import { saveAs } from 'file-saver';
// saveAs方法 文件数据/File对象/资源链接 下载全文件名 自动提供Unicode文本编码提示
// saveAs(Blob/File/Url, optional DOMString filename, optional Object { autoBom: true }
saveAs(blob, 'temp.jpg');
Excel
导出文件
Blob
服务端响应为文件的Blob类型数据
// 导出Blob为Excel文件
export const blob2Excel = (data, res, name) => {
if (!data) return;
// fileName默认为headers中文件信息
const fileName = extractFilenameFromContentDisposition(res.headers?.['content-disposition']);
// iconv-lite解决中文名称乱码
// const iconv = require('iconv-lite');
// iconv.skipDecodeWarning = true; // 忽略警告
// fileName = name || iconv.decode(fileName, 'gbk');
// 或 window.decodeURIComponent解码
fileName = name || window.decodeURIComponent(fileName) + '.xlsx';
// 转为Blob对象
const blob = new Blob([data], {
type: '' // application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8
});
// 下载至本地
// a.click模拟下载
// saveAs: import { saveAs } from 'file-saver'
saveAs(blob, fileName);
};
function extractFilenameFromContentDisposition(contentDisposition) {
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
const matches = filenameRegex.exec(contentDisposition);
if (matches !== null && matches[1]) {
return matches[1].replace(/(utf-8)?['"]/g, '');
}
return 'download';
}
注:Blob响应数据的请求头应声明Content-Type: blob
以被正确被解析
JSON转Excel
纯前端的数组/json对象转为excel
const data = [{name: 'xxx', ...} ...];
const sheetHeader = new Map([
['name', '名称'],
['id', '身份Id'],
['url', '链接']
]);
const fileName = 'info';
const sheetName = '工作表1';
json2Excel(data, sheetHeader, fileName, sheetName);
import XLSX from "xlsx";
// Array 表头 文件名 工作表名
function json2Excel(data, sheetHeader, fileName, sheetName){
const sheetHeaderKeys = Array.from(sheetHeader.keys());
const sheetHeaderTitles = Array.from(sheetHeader.values());
const sheetBody = data.map((row) => sheetHeaderKeys.map((key) => row[key]));
const sheetData = [sheetHeaderTitles].concat(sheetBody);
// 1. 创建sheet(工作表对象)
sheetName = sheetName || 'sheet1';
const sheet = XLSX.utils.aoa_to_sheet(sheetData);
// 2. 创建workbook(工作簿对象)
const workbook = {
SheetNames: [sheetName],
Sheets: { [sheetName]: sheet }
};
// 3. 生成xlsx文件数据
// bookSST: 是否生成Shared String Table; 如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
const wopts = {
bookType: 'xlsx', // 要生成的文件类型
bookSST: false,
type: 'binary'
};
const wbout = XLSX.write(workbook, wopts);
// 4. 导出为excel文件
saveAs(new Blob([s2ab(wbout)], { type: '' }), `${fileName}.xlsx`);
}
function s2ab(s) {
var buf = new ArrayBuffer(s.length);
var view = new Uint8Array(buf);
for (var i=0; i!=s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
return buf;
}
Excel静态资源
Excel文件本身作为静态资源存在
downloadTemplate() {
window.open('/static/template.xlsx', '_self');
}
读取文件
Excel转JSON
const arr = XLSX.readExcel(file.raw).then((sheetData) =>
sheetData.map((row) => ({
name: row['姓名'],
id: row['id'],
url: row['链接']
}));
).catch(e => console.log(e));
import XLSX from 'xlsx';
export const readExcel = function (blob_file) {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onload = () => {
// 解析文件为二进制
const buffer = reader.result;
const bytes = new Uint8Array(buffer);
const length = bytes.byteLength;
let binary = '';
for (let i = 0; i < length; i++) {
binary += String.fromCharCode(bytes[i]);
}
// 读取数据并转为json
const wb = XLSX.read(binary, { type: 'binary' });
const sheetData = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
resolve(sheetData);
};
reader.onerror = (err) => {reject(err) };
reader.readAsArrayBuffer(blob_file);
});
};
JSZIP
将文件或文件目录转换为压缩包文件
安装
npm i jszip -S
使用
import JSZip from 'jszip';
var zip = new JSZip();
// file:向压缩目录添加文件
zip.file("Hello.txt", "Hello World\n");
// folder:向压缩目录新建目录
var img = zip.folder("images");
img.file("smile.gif", imgData, {base64: true}); // 支持指定文件数据类型,默认为blob
// 生成压缩文件
zip.generateAsync({type:"blob"}).then(function(content) {
saveAs(content, "example.zip");
});
大文件下载/上传
问题
大文件上传/下载时,占用浏览器大量内存,导致内存溢出 大文件上传/下载时,花费大量时间,导致请求超时 且一旦上面的原因导致失败,由于需重新开始,传输成本高
方法
流式传输: 将数据分成数据块在管道(pipe)上流动实现传输,这一过程不需要将文件读取到内存中;解决内存占用过大问题;
分片传输: 与流式传输有共通之处,人为数据切片,支持断点续传,一定程度缓解内存问题,且有效防止大文件时间超出问题;
场景
从第三方服务下载大文件,并返回给前端下载
- 从第三方服务直接下载到服务器的磁盘中,然后以流转给前端
- 从第三方服务流式下载到服务器的磁盘中,然后以流转给前端
- 第三方服务分片传递到(需第三方服务支持)服务器,然后将分片传递给前端
注:
服务端文件流常通过磁盘存储,因为直接在内存存储文件,造成内存开销过大;
流式下载在浏览器不适用,因为浏览器必须通过file将整个文件读取到内存才能后续下载,所以浏览器下载永远会占用内存;(但可使用ReadableStream实现浏览器流式上传)
下载方案
分片下载 前端计算分片发起若分片下载请求,后端根据分片信息从磁盘下读取文件某段文件流,返回前端。最后由前端合并成完整的文件。 这种方式只能减少网络传输的压力,同时支持断点续传,下载进度。
前端代码:
const fileUrl = 'http://example.com/file.txt';
const chunkSize = 1024 * 1024; // 1MB
async function downloadFile() {
const response = await axios.head(fileUrl);
const totalSize = response.headers['content-length'];
const totalChunks = Math.ceil(totalSize / chunkSize);
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const startByte = chunkIndex * chunkSize;
const endByte = (chunkIndex + 1) * chunkSize - 1;
const rangeHeader = `bytes=${startByte}-${endByte}`;
const chunkResponse = await axios.get(fileUrl, {
headers: { Range: rangeHeader },
responseType: 'blob',
});
const chunkData = chunkResponse.data;
// 处理下载的分片数据,例如保存到本地或进行其他操作
// ...
fileChunks.push(chunkData);
// 显示下载进度
const progress = ((chunkIndex + 1) / totalChunks) * 100;
console.log(`下载进度: ${progress.toFixed(2)}%`);
}
const finalBlob = new Blob(fileChunks);
console.log('文件下载完成');
}
downloadFile();
Node代码:
const Koa = require('koa');
const fs = require('fs');
const path = require('path');
const app = new Koa();
app.use(async (ctx) => {
const filePath = 'path/to/file.mp4'; // 文件路径
const stat = fs.statSync(filePath);
const fileSize = stat.size;
const range = ctx.headers.range;
if (range) {
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
const chunkSize = end - start + 1;
ctx.status = 206;
ctx.set({
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunkSize,
'Content-Type': 'video/mp4',
});
const fileStream = fs.createReadStream(filePath, { start, end });
ctx.body = fileStream;
} else {
ctx.status = 200;
ctx.set({
'Content-Length': fileSize,
'Content-Type': 'video/mp4',
});
const fileStream = fs.createReadStream(filePath);
ctx.body = fileStream;
}
});
流式下载 使用流式传输的方式将文件内容逐步发送给前端。仅减少网络传输的压力,但实现简单。
前端 axios
设置响应类型为 blob
即可
后端:
const Koa = require('koa');
const fs = require('fs');
const path = require('path');
const app = new Koa();
app.use(async (ctx) => {
const filePath = 'path/to/file.mp4'; // 文件路径
ctx.response.set('Content-Type', 'video/mp4');
ctx.response.attachment('file.mp4');
const fileStream = fs.createReadStream(filePath);
ctx.body = fileStream;
});
此时响应变成了 transfer-encoding:chunked
transfer-encoding:chunked
,浏览器会分块
注: 服务器向第三方服务下载文件流,详见 KID-1912博客 | koa2笔记 |
断点续传 不从传输的性能上优化,而是从传输的可靠性上优化。 不仅适用于大文件,还能实现文件的暂停/继续,错误重试等。 即前端在传输时记录当前已下载位置,因某原因中断时向后端发起续传请求, 后端接收到续传请求,从磁盘读取指定位置后的文件流,返回给前端。
前端代码:
const fileUrl = 'http://example.com/file.mp4'; // 文件的 URL
const fileName = 'file.mp4'; // 下载后保存的文件名
function downloadFile() {
axios({
method: 'get',
url: fileUrl,
responseType: 'blob',
headers: {
Range: 'bytes=0-', // 设置断点续传的起始位置
},
})
.then((response) => {
// 对分片字节拼接处理
})
.catch((error) => {
console.error('文件下载失败:', error);
});
}
downloadFile();
Node代码:
const Koa = require('koa');
const fs = require('fs');
const path = require('path');
const app = new Koa();
app.use(async (ctx) => {
const filePath = 'path/to/file.mp4'; // 文件路径
const fileSize = fs.statSync(filePath).size;
const range = ctx.headers.range;
const start = Number(range.replace(/\D/g, ''));
const end = fileSize - 1;
const chunkSize = end - start + 1;
ctx.set('Content-Type', 'video/mp4');
ctx.set('Content-Length', chunkSize);
ctx.set('Content-Range', `bytes ${start}-${end}/${fileSize}`);
ctx.set('Accept-Ranges', 'bytes');
ctx.status = 206;
const fileStream = fs.createReadStream(filePath, { start, end });
ctx.body = fileStream;
});
CDN 加速 使用内容分发网络(CDN)可以提高大文件的下载速度和并发性, 将文件缓存到离用户较近的节点,减少数据传输的延迟。 通过配置合适的 CDN 加速策略,可以提升大文件下载的性能和用户体验。
上传方案
分片上传
见 [JS二进制 - KID-1912’s博客 | KID-1912’s Blog](https://kid-1912.github.io/2022/06/22/Js%E4%BA%8C%E8%BF%9B%E5%88%B6/) File段落 |
断点上传 与分片上传同理,只是前端上传中断时,接着上次位置继续上传
前端代码:
async function uploadFile(url, file, chunkSize = 5 * 1024 * 1024) {
const fileSize = file.size;
let startByte = 0;
while (startByte < fileSize) {
const chunk = file.slice(startByte, startByte + chunkSize);
const formData = new FormData();
formData.append('file', chunk, 'file.ext');
formData.append('startByte', startByte);
formData.append('totalSize', fileSize);
const response = await axios.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
startByte += chunkSize;
console.log(`Uploaded ${startByte} bytes`);
}
console.log('Upload complete');
}
const fileUrl = 'http://your-server/upload';
const fileInput = document.getElementById('file-input');
const file = fileInput.files[0];
uploadFile(fileUrl, file);
Node代码:
const Koa = require('koa');
const fs = require('fs');
const koaBody = require('koa-body');
const app = new Koa();
app.use(koaBody());
app.post('/upload', async (ctx) => {
const { file, startByte, totalSize } = ctx.request.body;
const filePath = '/path/to/save/file.ext';
const buffer = Buffer.from(file, 'base64');
if (startByte === 0) {
fs.writeFileSync(filePath, buffer);
} else {
fs.appendFileSync(filePath, buffer);
}
const uploadedBytes = startByte + buffer.length;
const progress = Math.floor((uploadedBytes / totalSize) * 100);
console.log(`Received ${uploadedBytes} bytes (${progress}% completed)`);
ctx.status = 200;
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
并发上传
并发式的分片上传,需额外逻辑处理