整理文件相关知识点

2,380 阅读1分钟

本文涉及内容

  • Excel处理插件
  • 认识文件(base64编码/类型化数组)
  • 文件上传下载
  • Node Stream流初探
  • 踩坑记录

从一个Excel表格开始

刚刚实现一个需求:处理一个excel文件,可以取出数据,也可以将数据保存成excel文件下载。

主要调研了两个插件:

  • js-xlsx 处理Excel的插件,可以在多种平台使用(包括react),仅在前端工程里就可以实现分析和保存Excel的需求;
  • exceljs 基于NodeJS的Excel处理插件,在Node端使用,需要前端上传文件到Node端,在Node端生成文件通过浏览器下载。

项目的情况是前端采用antd组件库,后端采用Node。两个插件都探了下路,代码如下:

Excel处理插件

js-xlsx

// 前端读取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();

上述代码有两个注意点:

  • antd的Upload组件哪怕不手动写上传也会向服务器发送一个请求,最后改成html的input标签;
  • XLSX的read方法可以接受多种数据格式,如上面转换成Uint8Array(后面展开说明)

exceljs

// 前端上传
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,另外返回数据也可以用流的方式:

await workbook.commit().then(() => {
    cont stream = workbook.stream;
    this.ctx.attachment('write.xlsx');
    ...
});

认识文件

首先,我们对文件做一个简单梳理:

  • 所有文件都是以二进制形式保存的;
  • 与数据类型相似,文件也有文件类型的概念(以后缀名形式体现),文件类型可粗略分为两类,一类是文本文件(.txt / .java / .html),另一类是二进制文件(.zip / .pdf / .mp3 / .xlsx)。
  • 基本上,文本文件里的每个二进制字节都是某个可打印字符的一部分,都可以用最基本的文本编辑器进行查看和编辑,如notepad/vi二进制文件中,每个字节可以表示字符、颜色、字体、声音大小等等,如果用基本的文本编辑器打开,一般都是满屏乱码,需要专门的应用程序进行查看和编辑。
  • 文本文件的编码(我们一般不知道一个文本文件用什么编码的,UTF-8可以使用BOM头)

摘自:文件概述 / 计算机程序的思维逻辑

在学习文件传输的过程中总有一些名词似懂非懂,base64、ascii、arraybuffer、Uint8Array...

Base64编码

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方法读文件的结果一样。所以下载文件不能使用发请求的方式,不会启动下载行为。

Node Stream流

在做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的概念区别开,知乎上一个热帖中这样区分:

  • cache 是为了弥补高速设备和低速设备的鸿沟而引入的中间层,最终起到加快访问速度的作用。
  • buffer 的主要目的进行流量整形,把突发的大数量较小规模的 I/O 整理成平稳的小数量较大规模的 I/O,以减少响应次数

查阅资料的时候看到一个例子,我们在看视频的会看到缓冲条,一会儿增加一块,视频的下载不是下一点就交互到播放部分,这样会影响播放的流畅,这时需要缓冲区的概念,缓冲了较多数据后一起写入。(网上看到的,描述可能不准确,有了解的朋友欢迎讨论,本人对视频很感兴趣,因为爱看剧~)

列举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实现文件流转存服务

踩坑

上传文件400报错

后端采用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 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是JavaScript原生格式的数据。File 接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。

antd的Upload组件对上传的文件进行了一次封装,获取文件需要info.file.originFileObj得到。 封装添加了percent、status、response等属性。

结语

第一次在掘金(大佬云集的地方)上写博客,只是把从一次需求中拓展学习的东西整理了一下,有问题欢迎大家指出,谢谢~ ^ ^