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

Mike Huang
麥克的半路出家筆記
12 min readAug 13, 2019

--

在開發網路應用程式時,上傳檔案是個常見的功能。最近學到透過 Multer 套件,讓客戶端能上傳文件到伺服器端。本篇將紀錄使用 Multer 處理大頭貼上傳,及應用另一個套件 Sharp 加工圖片的筆記。

Multer

Multer 是由推出 Express 的同一間公司所提供的第三方套件,專門用來處理文件上傳到伺服器。👉 npm 👉GitHub

專案介紹

這次預期的功能,是透過建立三條路由,讓使用者可以上傳、刪除、讀取大頭照 Profile Picture:

  • 使用者需要經過驗證,才能上傳和刪除大頭照 — 透過 auth Middleware 驗證後,含有用戶資訊的物件,將被儲存在 req.user上供後續使用
  • 借助 Mongoose ODM,將使用者上傳的照片儲存在 MongoDB 資料庫當中

初始化專案

安裝 Multer

$ npm i multer

在 Node.js 載入 Multer

const multer = require(‘multer’)

設定 Multer

const upload = multer(opts)

透過 multer(opts) 設定 Multer ,並將回傳的實例(instance)存在 upload上,供後續使用— multer() 接受一個 opts 物件,它帶有許多可以用來設定的「選項」:

限制上傳檔案的大小:limits

limit 選項帶有一個物件,可以用來限制以下各屬性的大小

Source: https://github.com/expressjs/multer#readme

為避免使用者上傳的檔案過大,在專案中限制上傳的檔案不得超過 1MB:

  • fileSize:限制上傳檔案的大小
  • fileSize 接受的單位為 bytes:1MB = 1000000 bytes

限制接受的上傳格式:fileFilter

fileFilter 選項帶有一個函式,用來篩選符合條件的上傳檔案,其接受三個參數: request 物件、帶有上傳檔案資訊的file 物件、篩選完成後呼叫的cb 函式。

👉 file 會依照不同的設定,帶有以下特定的屬性資訊:

src: https://www.npmjs.com/package/multer#file-information

👉 cb() 是一個當篩選完成時被呼叫 Callback 函式,其接受兩個參數:(1)錯誤訊息 (2)說明是否接受該檔案的 Boolean 值

  1. 若接受該檔案:呼叫時帶入 true
cb(null, true)

2. 若不接受該檔案:呼叫時帶入 false

cb(null, false)

3. 輸出錯誤訊息

cb(new Error('請上傳正確的檔案格式'))

在專案中,由於要請使用者上傳的是封面照,因此透過 fileFilter() 函式篩選時,只接受三種檔案格式:jpg、png、jpeg — 若篩選失敗,丟出錯誤訊息:

  • originalname:獲得檔案的原始名稱(名稱+檔案格式)
  • .match:確認檔案格式是篩選時所設定的三種檔案格式之一
  • cb(new Error()):檔案不符合格式時,觸發 cb() 函式,帶入錯誤訊息
  • cb(null, true):檔案符合格式時,觸發 cb() 函式,帶入 true

在路由中接收上傳檔案

透過 multer(opts) 設定所回傳的實例(instance)上,帶有不同種方法可以用來處理檔案的接收,常見的幾個方法:

.single(fieldname)

接收來自名為 fieldname 欄位的「單一」上傳檔案,並將檔案資訊存放在 req.file 上。例如:

// 接收名為 avatar 欄位的單一檔案
upload.single(‘avatar’)

.array(fieldname[, maxCount])

接收來自名為 fieldname 欄位的「多個」上傳檔案,且能透過第二個參數 maxCount 限制接收的最高數量,最終將包含所有檔案資訊的陣列存放在 req.files 上。例如:

// 接收至多 12 個名為 avatar 欄位的檔案
upload.array('avatars', 12)

.fields(fields)

接收來自不同欄位的上傳檔案,其中透過 fields 這個陣列來設定 — fields 包含了一個個「欄位名稱+限制數量」的物件 — 最終將包含所有檔案資訊的陣列以物件的形式,存放在 req.files 上。例如:

// 接收名為 avatar 和 gallery 欄位的檔案,分別接受最多 1 個和 8 個檔案
upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }])

嵌入接收方法

在專案中,只接收一個來自名為 avatar 欄位,且檔案格式為 jpg/jpeg/png 的圖片,因此將使用 upload.single(‘avatar’) 作為 Middleware ,帶入路由中,來處理檔案接收,運作流程大致如下:

  1. 來自客戶端的請求透過 POST 方法 /users/me/avatar 路徑進來
  2. Multer 將到請求中,尋找來自名為 avatar 欄位的上傳檔案
  3. Multer 確認該檔案符合「檔案大小」及「檔案格式」的篩選條件後,將檔案資訊儲存在 req.file 上,供後續使用

將照片存至資料庫

新增欄位

要儲存用戶大頭貼到資料庫以前,需要先修改userSchema:新增一個用來存放大頭貼的avatar欄位,其欄位類型為Buffer — 可儲存二進位的照片資料

儲存資料

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

  • req.user 是在透過 auth Middleware 驗證使用者後,從資料庫獲取的用戶物件:存入大頭貼前,該物件上 avatar 屬性是空值
  • req.file 是該檔案經 Multer 驗證和處理後,存放檔案資訊的地方:在此將存有二進位照片資料的 buffer 存放在該用戶的 avatar 欄位中
  • req.user.save():請 Mongoose 將包含大頭貼的用戶資料存至資料庫中
❗️File 物件屬性:
前面有提到 File 上的屬性會因為 Multer 設定的不同而有所差異。若設定 multer() 時,opt 物件沒有透過 dest / storage 屬性指定儲存方式,帶有照片的 Buffer 將被存在 req.file 上,供後續使用;相反的,req.file 將不會帶有 buffer 屬性資訊

測試功能

透過 Postman 上傳一張封面照到此路徑測試:

  • 傳輸的資料類型設定為:form-data
  • KEY 左側設定為正確的欄位名稱:avatar
  • KEY 右側設定為正確的類型:file
  • VALUE 選擇要上傳的檔案
  • 透過 Send 發送請求,並確認 Status code 為 200 OK:代表成功
❗️設定表單傳輸類型👉 透過表單上傳文件,需設定 <form> 的 enctype 屬性為: multipart/form-data
👉 enctype 屬性:設定提交表單到伺服器前,應對表單資料如何進行編譯
👉 multipart/form-data: 讓 Server 知道表單不僅包含純文字,也包含二進位資料
👉 若無提交檔案,可用 enctype 屬性預設:application/x-www-form-urlencoded
👉 Multer 會在 request 中尋找帶有此表單類型的資料
src: https://www.npmjs.com/package/multer#usage

確認結果

在 Robo 3T 當中,可以確認該使用者 Document 的 avatar 欄位已經存有大頭貼的二進位資料了:

將照片從資料庫刪除

透過 DELETE 方法向 /users/me/avatar 路徑發出請求,刪除使用者大頭貼:

  • 使用者需要經過驗證,才能刪除大頭照 — 透過 auth Middleware 驗證後,已經獲得 req.user能直接使用 — 將使用者avatar欄位設定為 null
  • 請 Mongoose 將更新後的使用者資料儲存到 MongoDB 資料庫中
  • 若過程中發生錯誤,Status Code 設定為 500 (伺服器發生錯誤)回傳

在瀏覽器中讀取大頭照

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

  • 透過使用者 id 到資料庫尋找該名使用者資料
  • 若無找到該名使用者,或該名使用者無大頭貼時,丟出錯誤
  • 回傳大頭貼時,需要設定 Header 當中的 Content-type ,定義回傳的資料格式為 png 的圖片檔
  • catch 接收到錯誤,設定 Status Code 為 404 (伺服器找不到該資源)回傳

測試功能

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

錯誤處理

前面已經透過許多設定,請 Multer 先行協助處理上傳的檔案,且遇到上傳失敗時,丟出錯誤訊息。然而,目前一旦發生上傳失敗的狀況 — 例如上傳一個超過 1MB 的檔案 — 伺服器回傳的會是 HTML 格式的錯誤內容,其中當然包含從 Multer 丟出的錯誤訊息:

修改錯誤回傳

透過預設的錯誤處理,將伺服器將回傳 HTML 格式的內容,然而在專案中,想要透過修改錯誤處理,回傳 JSON 格式的錯誤訊息,解決的方式:「在路由的最後,撰寫一個接收和處理錯誤訊息的 Middleware」詳細介紹可以參考 👉Express 官方文件

處理錯誤訊息的 Middleware 範例如下,其帶有四個參數 — 除了 reqresnext 三個參數外,也必須帶有 err 參數來接收錯誤訊息:

嵌入錯誤處理

在路由的最後,加入錯誤處理的 Middleware: Status Code 設定為 400,並回傳 JSON 格式的錯誤訊息:

測試功能

透過 Postman 上傳一張超過 1MB 的圖片到這條路徑,並確認回傳的 Status Code 是 400,且 Body 是一個 JSON 格式的錯誤訊息:

Sharp

Sharp 是一個第三方套件,用來處理圖像,例如:轉換圖片的尺寸和格式等。 Sharp 支援的格式有:JPEG, PNG, WebP, TIFF, GIF and SVG 。

專案介紹

透過本篇上半部的專案,成功讓使用者可以上傳、刪除、讀取大頭照 Profile Picture。接下來將透過 Sharp 模組,優化「負責上傳的路由」 — 將使用者上傳的圖片,先行轉換成統一的尺寸和檔案,再存至資料庫當中。

安裝 Sharp

$ npm i sharp

在 Node.js 載入 Sharp

const sharp = require('sharp')

Sharp 實例

在使用 Sharp 時,首先需要先將圖片(input)帶入 sharp() 函式中使用:

  • input 參數:可以接受含有圖片資訊的 Buffer 或圖片路徑的字串
  • sharp() 回傳Sharp 實例 — 透過此實例上的許多函式方法,可以為圖片進行一連串的編輯、修改、加工

resize() 方法

resize()Sharp 實例上的方法,其帶有三個參數:

  • width:預期修正後的寬
  • height:預期修正後的高
  • options:帶有其他設定選項的物件。詳細選項可以參考👉官方文件

在專案中,透過使用 resize() 方法,並帶入 options 物件,設定預期產出的寬和高皆為 50 px:

png() 方法

Sharp 實例上有許多轉換圖片格式的方法 — png() 函式能將圖片轉成 png 格式。其帶有一個options參數,詳細選項可以參考👉官方文件

toBuffer() 方法

為方便將修改後的圖片存取到資料庫當中,最後透過 toBuffer() 方法,將 50x50 png 格式的圖片,轉成 Buffer— 此方法回傳的是一個 Promise:

將圖片存至資料庫

編輯後的圖片將被存放在 buffer 上。透過新增使用者 avatar 欄位的資訊後,就能請 Mongoose 協助將使用者資料存至資料庫當中:

測試功能

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

結語

本篇實作中,建立了三條路由來製作大頭貼的上傳、刪除、讀取功能,並借助 Multer 第三方套件的功能,來處理使用者上傳的檔案。且為了使大頭貼在儲存至資料庫以前,能有一致的圖片尺寸與格式,額外使用了 Sharp 套件,為大頭貼進行加工。

在實作的過程中,會發現 Multer 和 Sharp 提供的功能真的很多 — 例如 Multer 還提供其他儲存檔案的方式;Sharp 也提供多種圖片加工的方式。迫不及待有更多的機會可以去挖掘和嘗試!

--

--

Mike Huang
麥克的半路出家筆記

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