Angular FileAPIのonloadではアロー関数を使う

JavaScriptのFile APIを利用していてつまずいたことのメモです。

下記の例ではAngular 6.1.2, TypeScriptを利用しています。 FileAPI周りはJavaScriptとほぼ同様だと思います。

やりたいこと

  • FileAPIを用いてローカルから画像を取得する
  • 画像ファイルをサーバーに登録する(ここではダミーサービスを叩くことにする)
  • サーバーからレスポンスが返ってきたら、画像を表示する(同様にダミーサービス)

ここではフロント側の処理だけを考えたいので、サーバーとの接続は実際には行わず、 ダミーのサービスを作成してそれを正しく叩くことを行います。

Angularプロジェクトの作成

説明用に新規プロジェクトを作成します。

$ ng new file-api

ダミーサービスの作成

サーバーとの通信を担当するサービスを作成します。

$ ng generate service dummy

src/app/dummy.service.ts, src/app/dummy.service.spec.tsが自動で作成されます。

dummy.service.tsに記述をしていきます。

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class DummyService {

  constructor() { }

  public upload(image: any): Observable<any> {
    return new Observable(observer => {
      observer.next(image);
    })
  }
}

非同期通信のためのライブラリの名前はAngular5以下と6で異なるので注意が必要です。

// import { Observable } from 'rxjs/Observable'; // <= Angular 5
import { Observable } from 'rxjs'; // = Angular 6

これはダミーサービスなのでサーバーとの通信は行いませんが、本来は下記のような形になるでしょう。

export class DummyService {

  constructor(
    private http: HttpClient,
  ) {}

  public upload(...) {
    return this.http.post(...);
  }
}

HttpClient.postの返り値の型はObservableとなるので、 このダミーサービスではreturn new Observable(...)として入力の値をそのまま返す形にしました。

このupload()には画像のファイル名、MIMEタイプ, base64エンコードのファイルのデータを送る仕様とします。

(この仕様がのちにFileAPIでつまずくポイントになります)

FileAPIを用いてローカルから画像を取得する(1)

ここからが本題になります。

FileAPIの使用方法は

www.html5rocks.com

を参考にすれば良いでしょう。

ただし、この記事は2010年のものなので、少し古いJavaScriptの構文になっています。 またFileAPI独特の注意点もあります。

参考にする部分を引用します。

<input type="file" id="files" name="files[]" multiple />
<output id="list"></output>

<script>
  function handleFileSelect(evt) {
    var files = evt.target.files; // FileList object

    // Loop through the FileList and render image files as thumbnails.
    for (var i = 0, f; f = files[i]; i++) {

      // Only process image files.
      if (!f.type.match('image.*')) {
        continue;
      }

      var reader = new FileReader();

      // Closure to capture the file information.
      reader.onload = (function(theFile) {
        return function(e) {
          // Render thumbnail.
          var span = document.createElement('span');
          span.innerHTML = ['<img class="thumb" src="', e.target.result,
                            '" title="', escape(theFile.name), '"/>'].join('');
          document.getElementById('list').insertBefore(span, null);
        };
      })(f);

      // Read in the image file as a data URL.
      reader.readAsDataURL(f);
    }
  }

  document.getElementById('files').addEventListener('change', handleFileSelect, false);
</script>

まずFileList特有の注意点です。

var files = evt.target.files; // FileList object
for (var i = 0, f; f = files[i]; i++) {...}

画像選択時に取得できるイベントから、FileListオブジェクトを取得できます。 FileListオブジェクトは配列のように見えて実は違うため、下記ではエラーになります。

for (let file of files) {...} // error

FileListの中のそれぞれのfileでは、

  • ファイル名(f.name)
  • MIMEタイプ(f.type)
  • ファイルサイズ(f.size)

などを取得することが出来ます。

ファイルの内容については、ローカルから非同期で取得されます。上記の引用のうち、

var reader = new FileReader();

// Closure to capture the file information.
reader.onload = (function(theFile) {
  return function(e) {
    // Render thumbnail.
    var span = document.createElement('span');
    span.innerHTML = ['<img class="thumb" src="', e.target.result,
                      '" title="', escape(theFile.name), '"/>'].join('');
    document.getElementById('list').insertBefore(span, null);
  };
})(f);

// Read in the image file as a data URL.
reader.readAsDataURL(f);

の部分に該当します。

この例では、取得した画像を直接HTMLに描画しているため、この書き方で問題ありません。

画像の取得(2), サービスの利用

それでは、このサンプルを用いてAngularで書いていきます。 最初に作ったダミーサービスを叩くようにしてみます。

それにしても、上記の

reader.onload = (function(theFile) {
  return function(e) {
    // ...
  };
})(f);

の部分が冗長ですね。

eだけでなくtheFileを用いているのは、 reader.onloadの結果(e)にはFileオブジェクトに含まれるファイル名などの情報はなく、 ファイルのデータだけが存在しているからです。

また、この(function(theFile) { ... }の中では this.photosthis.dummyServiceにアクセスすることが出来ません。 これでは困ってしまうので、下記のように記述してみます。

reader.onload = (e) => {
  let photoData = e['target']['result'];
  let sendData = {
    photoName: photoName,
    photoType: photoType,
    photoData: photoData,
  };

  this.dummyService.upload(sendData).subscribe(
    data => {
      console.log(data);
      this.photos.push(data);
    },
    error => {
      console.log(error);
    }
  );
};

即時関数を避け、さらにアロー関数を用いました。 ファイル名などの情報を利用し、thisを束縛せずに記述できます。 だいぶスッキリ記述できました。

まとめ

ビューでの画像の描画の部分を記述し、主なファイルを下記に示します。

dummy.service.ts

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class DummyService {

  constructor() { }

  public upload(image: any): Observable<any> {
    return new Observable(observer => {
      observer.next(image);
    })
  }
}

app.component.ts

import { Component } from '@angular/core';
import { DummyService } from './dummy.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app';

  photos: any[] = [];

  constructor(
    private dummyService: DummyService,
  ) {}

  ngOnInit() {}

  addPhoto(event: Event) {
    var files = event['target']['files']; // FileList object

    for (var i = 0, f; f = files[i]; i++) {
      if (!f.type.match('image.*')) {
        continue;
      }
      let photoName = f.name;
      let photoType = f.type;

      var reader = new FileReader();

      reader.onload = (e) => {
        let photoData = e['target']['result'];
        let sendData = {
          photoName: photoName,
          photoType: photoType,
          photoData: photoData,
        };

        this.dummyService.upload(sendData).subscribe(
          data => {
            console.log(data);
            this.photos.push(data);
          },
          error => {
            console.log(error);
          }
        );
      };

      // Read in the image file as a data URL.
      reader.readAsDataURL(f);
    }
  }
}

app.component.html

<h1>File API Example</h1>
<input type="file" accept="image/*" multiple (change)="addPhoto($event)">

<div *ngFor="let photo of photos">
  <img src="{{photo.photoData}}">
  <ul>
    <li>{{photo.photoName}}</li>
    <li>{{photo.photoType}}</li>
  </ul>
</div>

f:id:mi12cp:20180810002905p:plain

参考