刚刚实现一个需求:处理一个excel文件,能够取出数据,也能够将数据保存成excel文件下载。html
主要调研了两个插件:前端
项目的状况是前端采用antd组件库,后端采用Node。两个插件都探了下路,代码以下:java
// 前端读取Excel
onChange = info => {
if (info.file.status === 'done') {
console.log(`${info.file.name} file uploaded successfully`);
const reader = new FileReader();
reader.onload = e => {
const fileData = new Uint8Array(e.target.result);
const workbook = XLSX.read(fileData, { type: 'array' });
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
const data = XLSX.utils.sheet_to_json(firstSheet, { header: 1 });
};
reader.readAsArrayBuffer(info.file.originFileObj);
}
};
render() {
return (
<Upload onChange={this.onChange}> <Button>excel导入</Button> </Upload>
);
}
// 前端生成Excel
const data: [
[ "id", "name", "value" ],
[ 1, "sheetjs", 7262 ],
[ 2, "js-xlsx", 6969 ]
];
const worksheet = XLSX.utils.aoa_to_sheet(data);
const newWorkbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(newWorkbook, worksheet, "SheetJS");
const ff = XLSX.writeFile(newWorkbook, 'ff.xlsx');
const a = document.createElement('a');
// a.href = ff;
a.download = ff;
a.click();
复制代码
上述代码有两个注意点:node
// 前端上传
const formData = new FormData();
formData.append('file', info.file.originFileObj);
//Node端处理Excel
const { files } = ctx.request;
if (files) {
const element = files[0];
if (element.field === 'file' && element.filepath) {
const workbook = new Excel.Workbook();
await workbook.xlsx.readFile(element.filepath).then(() => {
workbook.eachSheet(function(worksheet) {
if (worksheet.columns) {
for (let i = 1; i <= worksheet.columns.length; i += 1) {
worksheet.getColumn(i).eachCell((cell, index) => {
// 设置表头数据
if (index === 1) { ……
//Node端返回Excel文件
await workbook.xlsx.writeFile('write.xlsx').then(async () => {
this.ctx.attachment('write.xlsx');
this.ctx.type = '.xlsx';
// this.ctx.set('Content-Type', 'application/octet-stream');
this.ctx.body = fs.readFileSync('write.xlsx');
}, function(err) {
console.log(err);
}
);
复制代码
上面的代码采用的写文件并读取的方式返回Excel,另外返回数据也能够用流的方式:react
await workbook.commit().then(() => {
cont stream = workbook.stream;
this.ctx.attachment('write.xlsx');
...
});
复制代码
首先,咱们对文件作一个简单梳理:git
摘自:文件概述 / 计算机程序的思惟逻辑github
在学习文件传输的过程当中总有一些名词似懂非懂,base6四、ascii、arraybuffer、Uint8Array...json
Base编码用于文件传输,有些网络传输渠道不支持全部字节,Base64能够传输ASCII码的控制字符等,把不可打印字符用可打印字符表示。因此,Base64是一种基于64个可打印字符来表示二进制数据的表示方法。后端
咱们能够将图片转换成Base64码,并设置成图片的src,便可实现图片的展现,可用于图片预览(无须上传图片,也可使用blob地址,这个之后再研究)数组
其转换方法:
window.btoa('helloworld');
window.atob('aGVsbG93b3JsZA==')
复制代码
关于Base64更详细的介绍请移步:Base64原理
类型化数组的主要用途是处理二进制数据,使开发者能够经过类型化数组操做内存,加强JS处理二进制数据的能力。JS将类型化数组的实现拆分红缓冲和视图两部分,缓冲(ArrayBuffer —— 固定长度的二进制缓冲区)和视图(将二进制数据转换成实际有类型的数据并操做,如Unit8Array、Unit16Array、Float32Array)。
var buffer = new ArrayBuffer(8);
var unit8View = new Unit8Array(buffer);
unit8View[0] = 1
复制代码
关于类型化数组更详细的介绍请移步:JavaScript类型化数组(二进制数组)
<script>
function onFileChange(e){
const file = e.target.files[0];
const formData = new FormData();
formData.append('file', file);
fetch('http://localhost:7001/send', {
method: 'POST',
body: formData
}).then(res => res.json()).then(res => {
console.log(res);
});
}
</script>
<div>
<input type="file" onchange="onFileChange(event)" />
</div>
复制代码
文件通常使用FormData发送。formData.append能够添加多个传输项。
⚠️注意:fetch的header中添加'Content-Type':'multipart/form-data'会报错,缘由就是添加后在header中不能生成随机分隔符boundary,边界用于分割不一样data,相似get请求name=John&age=16。
后端body发送一个文件,并设置响应头为Content-disposition:attachment,filename='xxx.xlsx'便可。 须要注意的是前端,下载是一个浏览器行为,须要使用a标签点击/window.location.href/表单的方式实现,我在作项目时使用发请求的方式,结果返回的是一段乱码。
通过分析,乱码是fetch解析了文件并把数据返回的结果,其乱码与FileReader使用readAsBinaryString方法读文件的结果同样。因此下载文件不能使用发请求的方式,不会启动下载行为。
在作Exceljs的下载Excel文件时,若是先写入文件再读取传输,会产生临时文件,而文件又有避免重名等问题,因而使用了Exceljs的流的方式传输。
Stream是Node的核心模块之一,分为可读流和可写流,直接使用fs.createReadStream和fs.createWriteStream生成。
对于流的理论网上有不少解读(对我来讲有点晦涩),个人理解是流是一种机制,经过缓冲区实现数据的有序放入和取出,而且设置了highWaterMark对一次写入/读出缓冲区作了控制。
可读流有两种读的方式,一种是触发data事件(不断进行,不论你操不操做数据,都不断读入缓冲区),一种是触发readable事件(在回调中咱们能够rs.read(1)读取缓冲区的数据) ——可读流这里有个疑问:每次放入缓冲区的字符数是highWaterMark,可是若是读的字符数超过这个值,则下一次放入的字符数也超过了highWaterMark,这里没有在官网上找到说明,望你们解答~
一个可读流的例子:
const rs = fs.createReadStream(filepath,{
encoding: 'utf8',
highWaterMark: 3
});
// pause模式
rs.on('readable', ()=> {
rs.read(6);
setTimeout(()=>{
console.log('缓冲区: ');
console.log(rs._readableState.buffer);
},2000)
})
复制代码
可写流使用wr.write('待写入内容'),write函数会返回一个bool值,表示缓冲区是否满了,若是满了则返回false,因此可放入while循环做为是否继续写入的判断依据。当缓冲区排空,也就是缓冲区中的数据真正被写入文件时,会触发drain事件,能够在该事件中继续write。
⚠️若是缓冲区满了继续写会不会丢数据呢?答案是不会,数据会被写入内存,可是官方不建议这样作。 参考:stackoverflow.com/questions/3…
一个可写流的例子:
const ws = fs.createWriteStream(filepath, {
encoding: 'utf8',
highWaterMark:3
});
let i = 9;
let flag = true;
function write(){
while(i>0 && flag){
// while(i>0){
flag = ws.write(''+i);
console.log(flag);
i--;
}
}
write();
ws.on('drain', () => {
flag = true;
console.log('drain');
write();
})
复制代码
在讲ArrayBuffer的时候咱们已经接触了缓冲的概念,计算机领域有不少地方用到了缓冲buffer,可是须要将buffer和cache的概念区别开,知乎上一个热帖中这样区分:
查阅资料的时候看到一个例子,咱们在看视频的会看到缓冲条,一下子增长一块,视频的下载不是下一点就交互到播放部分,这样会影响播放的流畅,这时须要缓冲区的概念,缓冲了较多数据后一块儿写入。(网上看到的,描述可能不许确,有了解的朋友欢迎讨论,本人对视频很感兴趣,由于爱看剧~)
列举node操做缓存的几个方法:
// 新建
this._cache = Buffer.alloc(0);
// 将buf加到_cache
this._cache = Buffer.concat([this._cache, buf], cacheLength + bufLength);
// 拷贝到固定长度的Buffer中
this._cache.copy(newBuf, 0, i * this.cutSize, (i+1) * this.cutSize);
// 只保留最后一个分片
this._cache = this._cache.slice(cutCount * this.cutSize);
复制代码
上述方法是在视频分片上传的代码中提取的,代码源自使用Node.js实现文件流转存服务
后端采用egg.js框架,egg使用egg-multipart处理上传的文件,对于文件类型和大小有限制,默认不容许xlsx类型的文件,须要在fileExtends中配置(还能够配置临时文件清除时间等):
module.exports = {
multipart: {
mode: 'file',
fileSize: '100mb',
// tmpdir: `${appInfo.baseDir}/cache/tmp`,
cleanSchedule: {
// cron style see https://github.com/eggjs/egg-schedule#cron-style-scheduling
cron: '0 0 7 * * *',
},
fileExtensions: [
'.xlsx',
],
},
};
复制代码
Blob 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不必定是JavaScript原生格式的数据。File 接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。
antd的Upload组件对上传的文件进行了一次封装,获取文件须要info.file.originFileObj获得。 封装添加了percent、status、response等属性。
第一次在掘金(大佬云集的地方)上写博客,只是把从一次需求中拓展学习的东西整理了一下,有问题欢迎你们指出,谢谢~ ^ ^