[シンプル] AngularFirebaseで画像をアップロードする!

Firebaseに画像アップロードやFirebaseの画像を表示させる方法を探しましたが、

わからない!

適当な情報に当たるよりも公式に当たったほうがいいだろうと思い公式を探したところありました。

Firebase Storage With AngularFire – DropZone File Uploader | AngularFirebase

では、導入方法について上記のサイトの流れの通り解説していきます。

英語が分かる方は動画で解説があるので上記のサイトを見たほうがいいと思います。

ここでは、使えればいいという方向けの解説になります。

どうなるのか気になる方は右のリンクを御覧ください。動作確認

また、いきなり現在作成中のプロジェクトに組込むよりも、新しくプロジェクトを作り、どんなことができるのか?実際に動くのか?を試してから組込むことをおすすめします。

プロジェクト作成

プロジェクト作成に関しては以前、解説したのでここでは割愛。

プロジェクト名は任意で構いません。次のサイトで解説しています。

[1ヶ月でAngular] はじめてのアプリ制作!メイン機能編 – まんくつ

AngularFire2の導入

導入に関しては以前、解説したのでここでは割愛。

解説したのは次のサイトです。AngularにFirebaseAPIキーを追加まで参考にしてください。

Angular5+Firebaseでチャットアプリを作る② Firebase連携編 – まんくつ

AngularFire2を導入済みなら、app moduleを次のようようにすればAngularFire2のストレージ関連の機能が使えます。

FirebaseStorageルール設定

アクセス権限を編集します。

FirebaseStorageを開くとルールというタブがあります。

そこを開き次のコードを記入します。

デフォルトと違うのはライトのところです。

ファイルタイプが画像ファイルに限り書き込みを許可します。

詳しい説明は公式を御覧ください。ファイルを保護する方法を学ぶ  |  Firebase

FirebaseStoreルール設定

公式ではdatabeseをデフォルト設定のRealtime Databaseではなく、ベータ版のCloud Firestoreを使っています。

もちろん、Realtime Databaseを使う方法もあると思いますが、今回は公式のコードを忠実に真似するためdatabeseCloud Firestoreに変更します。

変更方法は以下のとおりです。

DEVELOP>Database>Database右のドロップボックスからCloud Firestoreを選択

さらに、先ほどと同様にルールを変更します。

公式でルールが示されていなかったので、シンプルにアクセス制限なしにしています。

アクセス制限なしに抵抗がある方は公式を御覧ください。

セキュリティ ルールの記述条件  |  Firebase

これでFirebase側の設定は終わりました。

では、Angularで機能を実装させていきましょう。

bulmaをインストール

どうやら公式サイトで作られている画像アップロードページにはbulmaというスタイルテンプレートが使われているようです。

機能だけ実装できればいい方はインストール不要ですが、公式通りに表示させたいので、インストールします。

npm install bulma

.angular-cli.jsonのスタイルに次のコードを加えます。

.angular-cli.json(styles: [])
“../node_modules/bulma/css/bulma.css”,

これでbulma導入完了です。

では、画像のアップロードとダウンロード機能の実装をしていきましょう。

ドロップゾーンディレクティブ

ドラッグ/ドロップ操作を処理するカスタムディレクティブを生成します。

ng g directive directive/dropZone

作成したディレクティブでドラッグ/ドロップ操作で画像をアップロードできるように設定します。

カスタムドロップイベントとファイルリスト

解説は後回しにして、まずはファイルにコードを記入し、機能を実装させます。

drop-zone.directive.ts

ファイルアップロードコンポーネント

ファイルアップロードタスクを処理するコンポーネントを作成します。

ng g component file-upload
file-upload.component.ts

file-upload.component.html

file-upload.component.css

ファイルサイズ表示パイプを作成

ng g pipe pipe/fileSize
file-size.pipe.ts

サーバー起動

これで、機能を実装できました。

app.component.htmlに次のタグを書き込めばブラウザに表示されるようになります。

<file-upload></file-upload>

では、サーバーを起動してできているか確認してみましょう。

ng serve -o

次のように表示されていることでしょう。

エラーになる場合は、モジュールのdeclarationsにコンポーネント、デリバティブ、パイプが宣言されていることを確認してください。

動作確認

では試しにファイルをアップロードしてみます。

ファイルを選択する場合

ファイルをドロップする場合

このようにFirebaseを使った画像アップロードと表示ができるようになりました。

では、コードの中身を見ていきたいと思います。

ざっくりコード解説

htmlの構造からどんなスクリプトが動いているのか見ていきます。

アップロードのhtml構造はざっくりと次のようになります。(ブラウザ拡大推奨)

dropzoneにイベントが起きたら、スクリプトが動くということになっています。

ではひとつづつスクリプトを見ていきます。

toggleHover

dropzoneにマウスオーバーした時に動くスクリプトです。

イベントが送られてきます。ファイルをもったままマウスオーバーした時にイベント(”true”)が送られてきます。

isHovering”true”を格納することで、[class.hovering]=”isHovering”がアクティブになり、マウスオーバーしたことが分かるように装飾を変えることがになります。

toggleHover(event: boolean) {
this.isHovering = event;
}

カスタムデリバティブ

触れていませんでしたが、イベントはカスタムデリバティブで処理してから、スクリプトに渡しています。

ここはしっかりと見ていきますが、雰囲気多めで見ていきます。

参考:Angular – Directive

@Directive

クラスをAngularディレクティブとしてマークし、ディレクティブ設定メタデータを収集します。

今回は、セクター指定をしてhtmlのdropZoneでディレクティブを実行します。

@Directive({
selector: ‘[dropZone]’
})
<div class=”dropzone”
dropZone
(hovered)=”toggleHover($event)”
(dropped)=”startUpload($event)”
[class.hovering]=”isHovering”>

@Output()

Outputプロパティを定義します。

Outputプロパティは、Outputデコレータで注釈付けされた観察可能なプロパティです。このプロパティはほとんどの場合、Angular EventEmitterを返すために使います。値は、イベント・バインドされたイベントをコンポーネントに渡します。

入力と出力のプロパティでのみ、別のコンポーネントまたはディレクティブにバインドできます。

原文:Angular – Template Syntax

EventEmitter

ディレクティブやコンポーネントでカスタムイベントを生成するために使用します。

@Output() dropped = new EventEmitter<FileList>();
@Output() hovered = new EventEmitter<boolean>();

つまり、droppedOutputデコレータOutputプロパティとして定義し、そのdroppedにカスタムイベントとして入力されたファイルリストを格納します。

hoveredも同様に、カスタムイベントの真偽値を格納します。

参考:Angular – EventEmitter

蛇足ですが、カスタムイベントを駆使し、アコーディオンを作ることもできるようです。

@HostListener

AngularのHostListenerデコレータは2つの引数をとります。 1つ目は、リッスンするイベントの名前を指定します。 2番目の引数は、明示的にargsという名前の文字列のオプションの配列です。argsを省略する事もできますが、ここでは、[‘$event’]と引数を指定します。

$eventは、(hovered)=”toggleHover($event)”と同じように、イベントバインディングで使用するためのイベント値の予約名です。

参考:javascript – What’s the second argument to HostListener for and why is it often given as [‘$event’]? – Stack Overflow

カスタムイベントは、@HostListenerにイベント名を指定し、そのイベントが起こった時に、どんな処理をするかをその下に記述します。そして、その結果が、htmlを通して、コンポーネントに渡されます。

例えば、画像をドロップしたときは次のような処理になっています。

  1. 画像をドロップ
  2. カスタムデリバティブでドロップイベントをキャッチ
  3. this.droppedにドロップされたファイルリストを格納
  4. htmlの(dropped)=”startUpload($event)”startUploadにファイルリストを渡します

startUpload

dropzoneにファイルをドロップしたときに動くスクリプトです。

ファイル情報取得

まずは、受け取ったファイルリストの中で最初のリストをfileに格納します。

複数ファイル選択でも行けそうですが、ここではシンプルに一つにしているのでしょうか?もしくはアップロード制限的な意味合いもあるのかもしれません。

画像ファイル判断

次に、split(‘/’)を使って、ファイルタイプが画像かどうか判断します。

画像でなければエラーを返します。

firebaseのstorageに画像をアップしていればタイプを見てください。

例えば、jpegファイルであれば、image/jpegと表示されます。

これをsplit(‘/’)を使って、imageかどうか判断します。

格納先指定

パスに格納先を指定します。

次のコードで処理された日時を協定世界時(UTC) の 1970 年 1 月 1 日 00:00:00 からのミリ秒単位の数値で返します。名前が同じファイルで上書きすることを防止する意味で使っているのでしょう。

new Date().getTime()

どんなふうに表示されるのか参考にどうぞ ⇛ Tryit Editor v3.5

メタデータ

アップロードした画像ファイルにメタデータを保存できます。

ここでは、appという項目にMy AngularFire-powered PWA!というメタデータを指定しています。

AngularFireUploadTask

AngularFireUploadTaskにはアップロードのパーセンテージと最終的なダウンロードURLを監視するメソッドがあります。

taskを次のようにアップロードタスクとして定義します。

task: AngularFireUploadTask;

次のコードで、タスクにパス、ファイル名、メタデータをタスクに当てはめます。

this.task = this.storage.upload(path, file, { customMetadata })

この段階でファイルのアップロードが始まります。

これをクリックイベントに当てはめることで、アップロードステータスを制御できます。

次のように、task.pause()等でアップロードステータスをコントロールできます。

<button (click)=”task.pause()” [disabled]=”!isActive(snap)”>Pause</button>
<button (click)=”task.cancel()” [disabled]=”!isActive(snap)”>Cancel</button>
<button (click)=”task.resume()” [disabled]=”!(snap?.state === ‘paused’)”>Resume</button>

[disabled]=”!isActive(snap)”は次のスクリプトでアップロードステータスが実行中でかつ、ファイルサイズが転送されたサイズよりも大きい場合に、アクティブになります。

isActive(snapshot) {return snapshot.state === ‘running’ && snapshot.bytesTransferred < snapshot.totalBytes}

このようにAngularFireUploadTaskは、ファイル情報等を当てはめ、アップロード状況をコントロールすることができます。

また、最初に書いたようにパーセンテージと最終的なダウンロードURLを監視するメソッドがあります。

percentageChanges

次のコードでアップロード完了率を出力します。

this.percentage = this.task.percentageChanges();

precentageはObservableで定義されてアップロード完了率をリアルタイムで格納されます。

そして次のコードで表示します。

percentageに値がある時に同期し、値をpctに渡します。

pctの値によってプログレスバーが変化します。

さらに、パイプを使って数値化し、パーセント表記します。

<div *ngIf=”percentage | async as pct”>
<progress class=”progress is-info” [value]=”pct” max=”100″></progress>
{{ pct | number }}%</div>
downloadURL

アクセス可能であればダウンロードURLを取得します。

this.downloadURL = this.task.downloadURL();

これでアップロード関連のコード解説は以上になります。

ここからは本家でボーナスとされているあったらいいな機能の解説になります。

Firestoreデータベースにファイル情報を保存する

アップロード後にユーザーが画像データにどのようにアクセスするか疑問に思うかもしれません。 Firestoreのファイルへのパスを保存して、後で簡単にアクセスできるようにするには、task.snapshotChanges().pipeを使います。

snapshotChanges

ファイルのアップロードが進行すると、リアルタイムでアップロード状況を返します。

その状況がファイルサイズとアップロードしたファイルサイズが一致、つまり、アップロードが完了したときに、データベースにアップロードした画像のパスとサイズを格納します。

this.snapshot = this.task.snapshotChanges().pipe(
tap(snap => {if (snap.bytesTransferred === snap.totalBytes) {
this.db.collection(‘photos’).add( { path , size: snap.totalBytes })}}))

実際にデータベースは次のようになります。

使い方としては、データベースの画像格納フォルダーにアクセスして、画像リストを表示、そして使いたい画像を選択という風になるでしょう。

カスタムパイプ:サイズ表現

4338579

デフォルトではこのようにファイルサイズは表現されます。

これはだいたい4.2MBになります。

これをカスタムパイプを使って表現しています。

4338579 ⇛ 4.2MB

数学的処理が多めですが一応見ていきます。

値の受け取り

まず、次のコードでファイルサイズを受け取ります。ファイルサイズはsizeInBytesに格納されます。longFormは単位切り替えようの真偽値が格納されます。

今回は、指定していないので、longFormにはfalseが格納されます。

transform(sizeInBytes: number, longForm: boolean)

単位選択

次の三項演算子のコードで真偽値に単位を切り替えます。

今回は、短縮単位を使っています。

const units = longForm ? FILE_SIZE_UNITS_LONG : FILE_SIZE_UNITS;

1024の何乗?

次のコードで、ファイルサイズが1024の何乗が近いか見ます。Math.round() – JavaScript | MDN Math.log() – JavaScript | MDN

letを使うのは、そのまた次のコードでpowerの値が変わるからです。

constを使ってしまうと、値を変えられないのでletを使います。

let power = Math.round(Math.log(sizeInBytes) / Math.log(1024));

次のコードで、powerが予め用意していた、9つの単位の配列より小さいことを確認します。

Yottabytesより小さいことを見ています。Yottabytesって何?

power = Math.min(power, units.length – 1);

単位に合わせる

次のコードで、KBなら1024で、MBなら1024²で割って、ファイルサイズを単位に合うようにします。

const size = sizeInBytes / Math.pow(1024, power);

コンパクトに

次のコードで小数点以下の出力を2桁に限定します。

上のコードの結果は、循環小数になり、表示が小数点以下がずらっとならび、見づらくなるのを防ぎます。

const formattedSize = Math.round(size * 100) / 100;

単位呼び出す

powerには1024の何条かが格納されているので、unitsからその単位を呼び出します。

const unit = units[power];

出力

次のコートでこれまでの処理を返します。

カスタムパイプによって出力される値と単位です。

三項演算子によってsizeに値があればでなければ値を出力します。

sizeに何も格納されていなければ0を返します。

return size ? `${formattedSize} ${unit}` : ‘0’;

htmlコード

次のコードでカスタムパイプを使います。

<div *ngIf=”snapshot | async as snap”>
{{ snap.bytesTransferred | fileSize }} of {{ snap.totalBytes | fileSize }}

snapshotをsnapに同期します。そして転送ファイルサイズと元のファイルサイズをカスタムパイプを使って表示させます。

以上で、コード解説は終わります。

これでだいたいどんなふうな処理をしているかわかったことでしょう(^^♪

最後に

今回は、AngularでFirebaseを使った画像アップロードについて見てきました。

AngularFirebase本家様がサンプルを用意してもらえたので非常にたすかりました。

カスタムデリバティブにははじめて触れましたが、便利なものがあるものですね。積極的に流用していきます。

中身もだいたいどうなっているかがわかったので、実際にサービスに組込むのも難しくなさそうです。

では、素敵なAngularロギアを!

コメント