本系列文已出版成書「NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式」,感謝 iT 邦幫忙與博碩文化的協助。如果對 NestJS 有興趣、覺得這個系列文對你有幫助的話,歡迎前往購書,你的支持是我最大的寫作動力!
檔案上傳(File Upload) 是一項很基本的功能,到處都可以看見它的蹤影,如:某某社群網站的上傳大頭貼、某某影音網站上傳影片等。
Nest 針對檔案上傳功能封裝了一套名為 multer 的套件,它會處理格式為 multipart/form-data 的資料,在 Express 的應用程式上經常可以看到它的身影,是非常知名的套件。
雖然 Nest 將其包裝成內建模組,但還是建議各位安裝 multer 的型別定義檔,透過 npm 來進行安裝:
$ npm install @types/multer -D
接收單一檔案的方式很簡單,只要在特定路由下使用 FileInterceptor 並透過參數裝飾器 @UploadedFile 來取得檔案。其中,FileInterceptor 有兩個參數可以帶入,分別是:
fieldName:檔案在表單上對應的名稱。options:對應到 MulterOption,詳細內容可以參考 multer 官方文檔。這邊以 app.controller.ts 為例來實作單一檔案上傳:
import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller()
export class AppController {
@Post('/single')
@UseInterceptors(FileInterceptor('file'))
uploadSingleFile(@UploadedFile() file: Express.Multer.File) {
return file;
}
}
透過 Postman 進行測試,將一個檔案名稱為 nestjs_logo.svg 的圖片上傳,會收到該圖片的相關訊息:
如果同一個欄位名稱有一個以上的檔案,要使用 FilesInterceptor 並透過參數裝飾器 @UploadedFiles 來取得一個包含多個 Express.Multer.File 型別的陣列。
注意:這裡是使用複數 Files 而不是單一檔案上傳所使用的
FileInterceptor與@UploadedFile。
FilesInterceptor 有三個參數可以帶入,分別是:
fieldName:檔案在表單上對應的名稱。maxCount:配置可接受檔案數量的上限,可以選擇性填入。options:對應到 MulterOption。同樣以 app.controller.ts 為例來實作單一欄位多檔上傳:
import { Controller, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
@Controller()
export class AppController {
@Post('/multiple')
@UseInterceptors(FilesInterceptor('files'))
uploadMultipleFiles(@UploadedFiles() files: Express.Multer.File[]) {
return files.map(({ fieldname, originalname }) => ({ fieldname, originalname }));
}
}
透過 Postman 進行測試,將檔案名稱為 nestjs_logo.svg 與 nodejs_logo.png 的圖片上傳,會收到它們的欄位名稱與檔案名稱:
假如表單有多個欄位並且有一個以上的欄位包含檔案,要使用 FileFieldsInterceptor 並透過 @UploadedFiles 裝飾器來取得一個以欄位名稱作為 key 的物件,其值為 Express.Multer.File 型別的陣列。其中,FileFieldsInterceptor 有兩個參數可以帶入:
uploadedFields:一個包含多個物件的陣列,物件需要擁有 name 屬性來指定欄位的名稱,亦可以給定 maxCount 來指定該欄位可接受的檔案數量上限。options:對應到 MulterOption。同樣以 app.controller.ts 為例來實作多欄位多檔案上傳:
import { Controller, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
import { FileFieldsInterceptor } from '@nestjs/platform-express';
@Controller()
export class AppController {
@Post('/multiple')
@UseInterceptors(FileFieldsInterceptor([
{ name: 'first' },
{ name: 'second' }
]))
uploadMultipleFiles(@UploadedFiles() files: { [x: string]: Express.Multer.File[] }) {
const { first, second } = files;
const list = [...first, ...second];
return list.map(({ fieldname, originalname }) => ({ fieldname, originalname }));
}
}
透過 Postman 進行測試,將檔案名稱為 nestjs_logo.svg 與 nodejs_logo.png 的圖片上傳,會收到它們的欄位名稱與檔案名稱:
假如表單有多個欄位並且有一個以上的欄位包含檔案,但不需要依照欄位名稱做分類的話,可以直接使用 AnyFilesInterceptor 並透過 @UploadedFiles 裝飾器來取得一個包含多個 Express.Multer.File 型別的陣列。其中,AnyFilesInterceptor 可以帶入一個參數,即 options。
同樣以 app.controller.ts 為例來實作不分欄位多檔上傳:
import { Controller, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
import { AnyFilesInterceptor } from '@nestjs/platform-express';
@Controller()
export class AppController {
@Post('/multiple')
@UseInterceptors(AnyFilesInterceptor())
uploadMultipleFiles(@UploadedFiles() files: Express.Multer.File[]) {
return files.map(({ fieldname, originalname }) => ({ fieldname, originalname }));
}
}
透過 Postman 進行測試,將檔案名稱為 nestjs_logo.svg 與 nodejs_logo.png 的圖片上傳,會收到它們的欄位名稱與檔案名稱:
上面每個功能都可以指定 MulterOption 的配置,假如有個配置是多數上傳檔案都會用到的,那每次都要個別配置實在太麻煩了,所以 Nest 有提供一個預設值的方法,大幅減少這種重複的操作,那該如何使用呢?只要導入 MulterModule 並調用 register 方法即可,該方法可接受之參數正是 MulterOption。
這裡以 app.module.ts 為例,假如我們希望把上傳的檔案存到名為 upload 的資料夾裡,那就在 register 裡面給定 dest 屬性,並指定其值為 ./upload:
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
MulterModule.register({
dest: './upload'
})
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
我們沿用「預設 multer 設置」與「不分欄位之多個檔案上傳」的範例進行測試,透過 Postman 上傳 nestjs_logo.svg 與 nodejs_logo.png,會在專案目錄下看到 upload 資料夾,裡面含有以下內容:
奇怪,怎麼跟預期的不一樣?是哪裡出錯了嗎?其實這是因為 multer 預設情況下會隨機命名避免檔名衝突,這裡可以做個小實驗,將這兩個檔案的檔案名稱與副檔名改成原始的樣子,就可以看到他們的原貌了:
但這並不是解決問題的好方法,我們會希望能夠自動化去處理這件事情,那該怎麼做呢?這時候可以用 multer 提供的 diskStorage 來輔助我們去處理檔案名稱的問題。
diskStorage 是一個函式,我們可以透過指定 destination 來配置檔案的存放位置、指定 filename 去處理檔案名稱,這兩個屬性的值皆為 函式,透過函式去處理的彈性比較大,畢竟給特定值並不適用在每個場景。
我們透過撰寫一個 Helper Class 來實作這兩個函式,在 src 資料夾下新增 core/helpers 資料夾,並添加 multer.helper.ts,由於這兩個函式有特定的參數,故我們的方法也需要遵循這些參數來設計,其包含了 Request、Express/Multer.File 以及 (error: Error | null, destination: string) => void 的 Callback 函式,透過該 Callback 將處理好的結果返回給 multer:
import { Request } from 'express';
import { join } from 'path';
export class MulterHelper {
public static destination(
request: Request,
file: Express.Multer.File,
callback: (error: Error | null, destination: string) => void
): void {
callback(null, join(__dirname, '../../../upload/'));
}
public static filenameHandler(
request: Request,
file: Express.Multer.File,
callback: (error: Error | null, destination: string) => void
): void {
const { originalname } = file;
const timestamp = new Date().toISOString();
callback(null, `${timestamp}-${originalname}`);
}
}
接著,我們就來將這兩個函式實裝上去,修改 app.module.ts 的內容,將 register 物件參數中的 dest 換成 storage,並配置 destination 與 filename:
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { MulterHelper } from './core/helpers/multer.helper';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
MulterModule.register({
storage: diskStorage({
destination: MulterHelper.destination,
filename: MulterHelper.filenameHandler
})
})
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
最後,透過 Postman 進行測試,將 nestjs_logo.svg 與 nodejs_logo.png 上傳,會在專案目錄下的 upload 資料夾看到這兩個檔案:
multer 將檔案上傳功能簡化成套用 Middleware 即可使用,Nest 更進一步進行包裝,使其可以很輕易地在 Nest 中使用,讓它的使用方式更符合 Nest 的設計原則,是非常好用且強大的套件。這裡附上今天的懶人包:
multipart/form-data 的資料。FileInterceptor 並透過 @UploadedFile 來取得檔案資料。FilesInterceptor 並透過 @UploadedFiles 來取得檔案資料。FileFieldsInterceptor 並透過 @UploadedFiles 來取得檔案資料。AnyFilesInterceptor 並透過 @UploadedFiles 來取得檔案資料。MulterModule.register() 來配置 multer 預設值。storage 屬性與 diskStorage 來實作檔案儲存。