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の使用方法は
を参考にすれば良いでしょう。
ただし、この記事は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.photos
やthis.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>