整理文件相關知識點

本文涉及內容

  • Excel處理插件
  • 認識文件(base64編碼/類型化數組)
  • 文件上傳下載
  • Node Stream流初探
  • 踩坑記錄

從一個Excel表格開始

剛剛實現一個需求:處理一個excel文件,能夠取出數據,也能夠將數據保存成excel文件下載。html

主要調研了兩個插件:前端

  • js-xlsx 處理Excel的插件,能夠在多種平臺使用(包括react),僅在前端工程裏就能夠實現分析和保存Excel的需求;
  • exceljs 基於NodeJS的Excel處理插件,在Node端使用,須要前端上傳文件到Node端,在Node端生成文件經過瀏覽器下載。

項目的狀況是前端採用antd組件庫,後端採用Node。兩個插件都探了下路,代碼以下:java

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();
複製代碼

上述代碼有兩個注意點:node

  • 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,另外返回數據也能夠用流的方式:react

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

認識文件

首先,咱們對文件作一個簡單梳理:git

  • 全部文件都是以二進制形式保存的;
  • 與數據類型類似,文件也有文件類型的概念(之後綴名形式體現),文件類型可粗略分爲兩類,一類是文本文件(.txt / .java / .html),另外一類是二進制文件(.zip / .pdf / .mp3 / .xlsx)。
  • 基本上,文本文件裏的每一個二進制字節都是某個可打印字符的一部分,均可以用最基本的文本編輯器進行查看和編輯,如notepad/vi二進制文件中,每一個字節能夠表示字符、顏色、字體、聲音大小等等,若是用基本的文本編輯器打開,通常都是滿屏亂碼,須要專門的應用程序進行查看和編輯。
  • 文本文件的編碼(咱們通常不知道一個文本文件用什麼編碼的,UTF-8可使用BOM頭)

摘自:文件概述 / 計算機程序的思惟邏輯github

在學習文件傳輸的過程當中總有一些名詞似懂非懂,base6四、ascii、arraybuffer、Uint8Array...json

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等屬性。

結語

第一次在掘金(大佬雲集的地方)上寫博客,只是把從一次需求中拓展學習的東西整理了一下,有問題歡迎你們指出,謝謝~ ^ ^

相關文章
相關標籤/搜索