Angular フロントエンドから Express サーバ + Multer でファイルアップロードを実現する

Angular アプリをフロントエンド、Express サーバをバックエンドに従え、ファイルアップロード機能を実現してみた。

目次

Angular 側の実装

Angular アプリの実装はそこまで複雑ではないのでサクッと。Angular CLI v10 系で構築したアプリにて、次のようなコンポーネントを実装した。

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-upload-file',
  template: `
    <p><input type="file" (change)="handleFileInput($event.target.files)"></p>
    <p><button type="button" (click)="uploadFile()">アップロード</button></p>
  `,
  styles: []
})
export class UploadFileComponent {
  /** アップロード対象ファイル */
  public file: File = null;
  
  constructor(private httpClient: HttpClient) { }
  
  /** `input[type="file"]` 操作時にアップロードされたファイル情報を控えておく */
  public handleFileInput(files: FileList) {
    if(files == null || !files.length) return this.file = null;
    this.file = files.item(0);
  }
  
  /** ファイルをアップロードする */
  public async uploadFile() {
    try {
      const formData: FormData = new FormData();
      formData.append('file'    , this.file, this.file.name);  // ファイル本体とファイル名を指定する
      formData.append('fileName', this.file.name);             // ファイル名のみを別で送信しているが、必須ではない
      const result: any = await this.httpClient.post('./api/upload-file', formData).toPromise();
      console.log('File Uploaded', result);
    }
    catch(error) {
      console.error(error);
    }
  }
}

バリデーションなどは省略している。重要なのは3点。

  1. input[type="file"] の変更時に、File オブジェクトを取得しておく : $event から辿れる
  2. ファイルアップロードの際は FormData に値を詰めて送信する
  3. ファイルのパラメータは、ファイルの内容とファイル名を両方指定する (formData.append('file') 部分)

コレに相当する実装ができれば、フロントエンドは Angular に限らず実装できるだろう。

Express サーバに Multer をインストールする

Express 側は v4.17.1 を使用。Express サーバは単体ではファイルアップロードを処理できないので、Multer というプラグインを入れる。

# 本稿執筆時点では v1.4.2 だった
$ npm install --save multer

そしたら次のように実装する。TypeScript で実装しているが、素の Node.js の場合は import 部分を require() に直すぐらいで動作すると思う。

import express from 'express';
import multer from 'multer';
import { promises as fs } from 'fs';  // アップロードされたファイルを保存するのに使用

const app = express();

// body-parser 相当
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

// Multer をミドルウェアとして挟む。今回は `file` というパラメータ名で単一ファイルがアップロードされてくるので、
// `single('file')` という形でそれを指定している
router.post('/api/upload-file', multer().single('file'), async (req, res) => {
  const file     = req.file;  // ファイルの内容はこのように受け取れる (ココで出てくる `file` というプロパティ名は Multer が指定してくるモノ
  const fileName = req.body.fileName;  // 別に指定したファイル名のパラメータは POST Body より取得できる
  await fs.writeFile(`./upload-files/${fileName}`, file.buffer);  // 指定の位置にファイルを保存する例
});

const server = app.listen(process.env.PORT, () => console.log('Server Started'));

ポイントは Multer をミドルウェアとして記述するところ。クライアント側で指定した file というパラメータ名と、単一ファイルが送られてくるのか複数ファイルが送られてくるのか、といった指定をしておく必要がある。

複数ファイルも扱える。詳しくは Multer の README を参照。

上の実装の場合、ファイルの内容はメモリに載るだけなので、自分でファイルを指定ディレクトリに保存したりしているが、Multer がディスク上に保存してくれる機能もあったりする。結構便利。