前言 雖然實作過檔案之間的傳送或編寫/覆蓋新檔案,但發現自己對底層實作的原理完全沒有概念,加上這次在研究 GCS 的實作,因此有了這篇文章的誕生。學習程式之後察覺,每個部分都可以越挖越深,越看越細。
Stream - 流 什麼是 Stream 流? I/O 的傳輸=>資料輸入輸出,有資料從一端流向另一端的感覺。HTTP 請求、檔案傳輸都會使用到 Stream 的概念。 有四種不同的流:
Readable streams 創造讀取的流
Writable streams 創造寫入的流
Duplex streams 可讀可寫流
Transform streams 基於 Duplex,在讀寫過程可以調整數據的流 在實作上我們常用的 fs 套件就是 base on 原生的 stream 再包裝,所以在 fs 裡面也可以看到很多 stream 的使用方法。
Buffer - 緩衝區 二進制緩衝區,和 String 可以互相做轉換。Stream 的傳輸會將數據儲存到內部的緩衝區(buffer)中,buffer 容量滿時再流向另一端,這麼做可以減少 stream 直接對磁碟頻繁的操作。而緩衝器的容量大小也可以藉由highWaterMark
來做設定。
附上 fs 操作檔案的實作
1 2 3 4 5 const fs = require ('fs' );const read = fs.readFileSync('./test1.txt' ); const readstream = fs.createReadStream('./test1.txt' );
如果使用createreadstream
做傳輸的話,會使用 pipe()來做搭配,資料在流動的時候需要有一個管道讓它進行傳輸 而有趣的是,read & write 的用法是有對應的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const readable = fs.createReadStream('test1.txt' );const writeable = fs.createWriteStream('test2.txt' );readable .on('error' , (err ) => { }) .on('data' , (chunk ) => { console .log(chunk); }) .on('end' , () => { console .log('there will be no more data.' ); }); writeable .on('pipe' , (stream ) => { console .log(stream); }) .on('finish' , () => { console .log('數據寫入新檔案完成會觸發' ); }); readable.pipe(writeable);
以上的執行順序會是
writeable 打開 pipe 管道,開始接收 readable 的 stream
readable 的 data 事件觸發,開始將數據轉換成 buffer
readable 的 end 事件觸發,數據已全數轉換完畢
writeable 的 finish 事件觸發,數據全數完成寫入新檔案
還有其他的事件
可以到官方文件 看看
因為使用方法很多,這裡我是想自己記錄另外一些使用方法,自行將檔案轉成 buffer 再透過可寫流寫入檔案, 也就是使用上面的 readFileSync 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const fs = require ('fs' );const readbuffer = fs.readFileSync('./test1.txt' );const writeable = fs.createWriteStream();writeable.write(readbuffer, () => { console .log('write successfully' ); }); const fs = require ('fs' );const readbuffer = fs.readFileSync('./test1.txt' );const writeable = fs.createWriteStream();writeable .on('finish' , () => { console .log('完成' ); }) .end(readbuffer, () => { console .log('最後一個想寫入的東西,寫入完成' ); });
Google Storage 實作檔案傳輸 google storage,google 提供的雲端空間,用來存放檔案或圖片,和 AWS 的 S3 是相同功能,實務上用來將網頁上傳的檔案跟圖片存放到這邊來,成功之後就可以返回一個 url 提供我們下載自己上傳的檔案&呈現在自己的網頁上。
必須先 npm @google-cloud/storage
才能連接 google storage 空間
必須先到 GCP 取得憑證,利用憑證才能進入 google storage 空間
提供 GCS bucket name => 也就是你在 google storage 中開創的空間名稱完成以上才會開始寫程式 *這邊沒有寫太多 GCS 說明,主要注重在傳輸檔案的部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const { keyFilename } = require ('./config' ); const { Storage } = require ('@google-cloud/storage' );const storage = new Storage({ keyFilename }); let bucket = storage.bucket('your_bucket_name' ); const uploadFromBuffer = (buffer, destination ) => new Promise ((resolve, reject ) => { const file = bucket.file(destination); const ws = file.createWriteStream(); ws.on('error' , (err ) => { reject(err); }) .on('finish' , async () => { resolve(await file.publicUrl()); }) .end(buffer); });
會看到@google-cloud/storage
的使用也有createReadStream
, createWriteStream
的方法,表示他們也有使用 stream 再包裝~
前後端上傳檔案實作 以上是單純上傳到 google storage 的程式碼,這部分通常都會寫在後端,現在想實作從前端串接後端 API 再上傳至 GCS 上,前端到後端 API 的這段我也研究了一些時間,原因是因為跟 postman 的不熟悉?以及傳送檔案這樣的實物和傳送 JSON 感覺上不同而導致在操作上會有很多疑惑,許多不確定經過一試再試的精神才總算完成了!!
之前也有實作過傳送檔案的方法,當時使用的方式是 google 搜尋後比較常見的方法,是使用 FormData 在前端抓取到資料,而他的實作順序是
使用者上傳檔案,前端接收到 data 訊息
打後端 API 傳送 formData,檔案被傳送到專案的資料夾內,檔案相關資料用 Multer 接收
後端 API 用 stream 傳送資料到 GCS 上 完成前到後的流程,如果想看相關教學可以看這裡
2. 前端直接傳送 buffer 到後端 用 buffer 傳到後端是這次新的學習,這邊紀錄一下。 後端的 Buffer 在前端是概括了 ArrayBuffer、TypeBuffer、viewData, ArrayBuffer 是一大塊內存,只能唯讀,若想對 ArrayBuffer 暫存器做操作,要使用 TypeBuffer 的種類才可以,而 TypeBuffer 種類有 Uint8array, Uint16array…等等,而此次前端的資料流就使用了 Uint8array 來傳送到 Server 端。
前端使用 file API - FileReader 操作,將檔案讀取成 ArrayBuffer 格式
fetch 給 server 時 body 的 data 要將 ArrayBuffer 轉成 Uint8array 才可傳送
server 接收到 Uint8array 後再轉成 Buffer 才可傳至 gcs
upload.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <meta http-equiv ="X-UA-Compatible" content ="IE=edge" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <title > Document</title > </head > <body > <input type ="file" accept ="" id ="file-uploader" /> <input type ="button" value ="案 下去" id ="btn" /> <script > const fileUploader = document .getElementById('file-uploader' ); const btn = document .getElementById('btn' ); let file; let filename; fileUploader.addEventListener('change' , handleFileUpload); btn.addEventListener('click' , upload); async function upload ( ) { try { const beforeUploadCheck = await beforeUpload(file); if (!beforeUploadCheck.isValid) throw beforeUploadCheck.errorMessages; const arrayBuffer = await getArrayBuffer(file); await uploadFileAJAX(arrayBuffer, filename); console .log('File Uploaded Success' ); } catch (err) { console .log(err); } } async function handleFileUpload (e ) { file = e.target.files[0 ]; filename = e.target.files[0 ].name; console .log(file); console .log(filename); if (!file) return ; } function beforeUpload (fileobj ) { return new Promise ((resolve ) => { const validFileTypes = ['image/jpeg' , 'image/png' , 'xlsx' ]; const isValidFileType = validFileTypes.includes(fileobj.type); let errorMessages = []; if (!isValidFileType) errorMessages.push('You can only upload JPG or PNG file!' ); const isValidFileSize = fileobj.size / 1024 / 1024 < 2 ; if (!isValidFileSize) errorMessages.push('Image must smaller than 2MB!' ); resolve({ isValid : isValidFileType && isValidFileSize, errorMessages : errorMessages.join('\n' ), }); }); } function getArrayBuffer (fileobj ) { return new Promise ((resolve, reject ) => { const reader = new FileReader(); reader.addEventListener('load' , () => { console .log(reader); resolve(reader.result); }); reader.addEventListener('error' , () => { reject('error occurred in getArrayBuffer' ); }); reader.readAsArrayBuffer(fileobj); }); } function uploadFileAJAX (arrayBuffer, filename ) { return fetch('/upload/buffer' , { headers : { 'content-type' : 'application/json' , }, method : 'POST' , body : JSON .stringify({ uint8array : new Uint8Array (arrayBuffer), filename : `test/${filename} ` , }), }) .then((res ) => { return res.json(); }) .then((data ) => console .log(data)) .catch((err ) => console .log(err)); } </script > </body > </html >
express 接收部分
1 2 3 4 5 6 7 8 app.post('/upload/buffer' , async (req, res) => { const _uint8array = req.body.uint8array; const _filename = req.body.filename; const uint8array = Uint8Array .from(Object .values(_uint8array)); const buffer = Buffer.from(uint8array); const g_url = await GCS.uploadFromBuffer(buffer, _filename); return res.json({ status : 'OK' , data : g_url }); });
參考這篇的範例程式碼
心得: 邊整理資料邊整理思緒,想了一遍流程後思路也更清晰一些,但寫文章關於架構的整理,想著要怎麼寫才能清楚一點紀錄整個過程還是挺花時間的就是。以上就是近日來了解的部分,若說要研究肯定還可以再看更多細節 QQ,只能多看看文件,相信會跟它們熟一點~以上
資料來源:认识 node 核心模块–从 Buffer、Stream 到 fs 瞭解 Node.js 中的 Stream Node.js 流 Streams and Buffers in Node.js 官方文件 官方文件中文 google storage SDK 文件 Node.js 大檔案上傳 [WebAPIs] 檔案上傳 Input File, File Upload, and FileList nodejs 处理文件上传(express)
Author:
Chi Lin
Permalink:
https://chiderlin.github.io/2021/12/07/buffer-stream/
License:
Copyright (c) 2019 CC-BY-NC-4.0 LICENSE
Slogan:
Do you believe in DESTINY ?