剛剛實現一個需求:處理一個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等屬性。
第一次在掘金(大佬雲集的地方)上寫博客,只是把從一次需求中拓展學習的東西整理了一下,有問題歡迎你們指出,謝謝~ ^ ^