[筆記] 使用 Multer 實作大頭貼上傳(Part 2)

Mike Huang
麥克的半路出家筆記
7 min readAug 14, 2019

專案介紹

上一篇「使用 Multer 實作大頭貼上傳(Part 1)」介紹了 Multer,讓客戶端可以上傳大頭照到伺服器端,經過 Sharp 處理圖片尺寸和規格後,將圖片以 Buffer 存至 MongoDB 資料庫中。一旦使用者上傳大頭貼後,也可透過另外兩條路由,刪除和在瀏覽器中讀取大頭貼。

本篇將延續同樣功能的三條路由,嘗試使用不同的檔案儲存和讀取方式,來處理上傳的大頭照:

  1. 透過 Multer 的DiskStorage將檔案存至指定的資料夾(磁碟中)
  2. 存至 MongoDB 的是相對簡單和較不佔空間的「檔案路徑」字串
  3. 讀取大頭照時,從資料庫取得圖片路徑,透過 fs.createReadStream() 以串流形式讀取檔案、透過 pipe() 方法回傳

初始化專案

本篇是延續上一篇的專案做功能上的變更及優化,因此已經安裝且載入好 Multer 模組,並完成初步的設定:👉詳情請見上一篇

設定 Multer

const upload = multer(opts)

上一篇有提到,能透過 multer(opts) 設定和初始化 Multer ,其將回傳的實例(instance)存在 upload上,供後續使用— multer() 接受一個 opts 物件,它帶有許多可以用來設定的「選項」 ,上一篇中所使用的兩個設定:

  • 限制上傳檔案的大小:limits
  • 限制接受的上傳格式:fileFilter

指定存取位置:storage

本次實作,欲將上傳的檔案存至指定的目錄,因此將額外使用 storage 屬性,透過它可以設定檔案的存取位置,storage 有分兩種:

  • DiskStorage:是一個 storage engine,透過它可以將檔案存至磁碟中。使用時,可以使用destinationfilename屬性設定存取位置和檔案命名
❗️檔案命名: filename
👉若無設定檔案命名的方式,Multer 將隨機給予檔案一個名稱(不包含檔案格式)
👉命名時須自行加上檔案格式,Multer 將不會自動補上
  • MemoryStorage:是一個 storage engine,它將上傳的檔案以Buffer物件存在記憶體中,它不帶有任何可用的選項 option

設定 DiskStorage

本次實作將使用 DiskStorage 把上傳的大頭貼,依照特定的檔案命名規則,存取至指定的目錄。首先需要使用multer上的 diskStorage() 方法開始設定

  1. destination:指定檔案儲存位置
  • 帶有一個接收三個參數的函式:reqfilecbCallback 函式
  • cb() 會在設定完成後被執行,並接受兩個參數:錯誤訊息(成功設定時帶入 null)及存取檔案的資料夾(設定存取到 images 資料夾)

2. filename:指定檔案命名方式

  • 帶有一個接收三個參數的函式:reqfilecbCallback 函式
  • cb() 會在設定完成後被執行,並接受兩個參數:錯誤訊息(成功設定時帶入 null)及檔案名稱(設定為日期+原始檔案名稱)

將檔案路徑存至資料庫

新增欄位

要儲存用戶大頭貼檔案路徑到資料庫以前,需要先修改userSchema:新增一個用來存放路徑的avatar欄位,其欄位類型為String

儲存資料

userSchema 中新增好存放大頭貼連結的欄位後,就能在路由中,撰寫程式碼,將圖檔連結存到資料庫中:

  • req.user 是在透過 auth Middleware 驗證使用者後,從資料庫獲取的用戶物件:存入大頭貼前,該物件上 avatar 屬性是空值
  • req.file.path:帶有該檔案被儲存的路徑資訊,將其存在該用戶的 avatar 欄位
  • req.user.save():請 Mongoose 將包含大頭貼的用戶資料存至資料庫中

測試功能

透過 Postman 上傳一張封面照到此路徑測試:在 Robo 3T 當中,可以確認該使用者 Document 的 avatar 欄位已經存有大頭貼存取的路徑資訊:

在 Visual Studio Code 當中也能看到一個 images 資料夾被創立,且資料夾中存有一張依照命名方式設定的大頭貼圖檔:

在瀏覽器中讀取大頭照

在瀏覽器透過 GET 方法向 /users/:id/avatar 路徑發出請求,並帶入使用者 id,以獲取該名使用者的大頭貼,在瀏覽器中呈現:

  • 透過使用者 id 到資料庫尋找該名使用者資料
  • 若無找到該名使用者,或該名使用者無大頭貼時,丟出錯誤
  • 透過 Node.js 提供的 path 核心模組join()方法串接和取得該圖片的絕對位置,存在 imagePath 中(__dirname:取得目前 js 檔案的絕對路徑)
  • 回傳大頭貼時,需要設定 Header 當中的 Content-type ,定義回傳的資料格式為 jpg 的圖片檔
  • catch 接收到錯誤,設定 Status Code 為 404 (伺服器找不到該資源)回傳

1. 透過緩衝形式回傳:Buffer

第一種方式是透過緩衝形式回傳大頭貼:使用 Node.js 檔案系統(fs)的 readFile() 方法讀取該檔案 — 檔案將先被讀取、暫存至緩衝區(記憶體中),當檔案讀取完成後,才回傳給使用者。

使用緩衝形式傳送的缺點:

  • 等待時間:若傳輸的檔案非常大(例如高畫質影片),在真正回傳給使用者前,就需要先等待很久的讀取時間,用戶體驗就會降低。
  • 記憶體容量:若伺服器一次接收多個客戶端請求,針對每個請求都需要將檔案的讀取先暫存在緩衝區中,那伺服器的記憶體容量就會不堪負荷

2. 透過串流形式回傳:Stream

第二種方式是透過串流形式回傳大頭貼:使用 Node.js 檔案系統(fs)的 createReadStream() 方法以串流形式讀取檔案 — 可以想成把檔案切成很多小塊(chunk)讀取,當一小區塊被讀取完成,就透過 pipe() 方法回傳。

使用串流形式傳送的優點:

檔案在還沒有完整被讀取完畢前,就開始輸出,這能解決使用緩衝所面臨的時間等待和記憶體空間限制問題。在生活中常見的一個例子是觀看串流影片:用戶不需要等待影片讀取完才能觀看,而是一邊看,影片會一邊被讀取和傳送,省下了等待時間。

測試功能

在瀏覽器中,透過 GET 方法向 /users/:id/avatar 路徑發出請求,並帶入使用者id,能正確讀取大頭照:

結語

透過這次實作,嘗試運用 Multer 套件中不同的方法,來存取使用者上傳的檔案、過程中也才學習到運用串流和緩衝的形式傳輸檔案,以及使用上的優缺點。

最近在實作時,漸漸可以理解在開發上,為完成某個需求,常會有不同的方式能達成。其實未必哪一種方式才是最好或最不好的,更重要的是去深入了解需求和情境,及各種方式的優缺點,以選出當下最合適的方法來開發。

--

--

Mike Huang
麥克的半路出家筆記

熱愛接觸和學習網頁開發相關技術與知識、喜歡分享、旅遊和咖啡的軟體工程師 A software engineer who enjoy learning and sharing web technologies & fancy coffee and travelling